在我们设计软件的很多地方,都看到需要对表格数据进行导入和导出的操作,主要是方便客户进行快速的数据处理和分享的功能,本篇随笔介绍基于WPF实现DataGrid数据的导入和导出操作。

1、系统界面设计

在我们实现数据的导入导出功能之前,我们在主界面需要提供给客户相关的操作按钮,如下界面所示,在列表的顶端提供导入Excel、导出PDF、导出Excel。

由于这些操作功能基本上在各个页面模块,可能都会用到,因此尽可能的抽象到基类,以及提供通用的处理操作,实在有差异的,也可以通过一些属性或者事件方法的覆盖方式来实现即可。

因此我们在Xaml里面定义按钮的时候,基本上是调用视图模型的方法来通用化的处理,如下代码所示。

<Button
Margin="5"
hc:IconElement.Geometry="{StaticResource t_import}"
Command="{Binding ImportExcelCommand}"
Content="导入Excel"
Style="{StaticResource ButtonWarning}" />
<Button
Margin="5"
hc:IconElement.Geometry="{StaticResource SaveGeometry}"
Command="{Binding ViewModel.ExportPdfCommand}"
CommandParameter="用户信息列表"
Content="导出PDF"
Style="{StaticResource ButtonSuccess}" />
<Button
Margin="5"
hc:IconElement.Geometry="{StaticResource SaveGeometry}"
Command="{Binding ViewModel.ExportExcelCommand}"
CommandParameter="用户信息列表"
Content="导出Excel"
Style="{StaticResource ButtonSuccess}" />

而导入的处理操作函数ImportExcelComand的定义如下所示(注意这里声明了RelayCommand)代码会自动生成Command的后缀Command方法的。

    /// <summary>
/// 导出内容到Excel
/// </summary>
[RelayCommand]
private void ImportExcel()
{
var page = App.GetService<ImportExcelData>();
page!.ViewModel.Items?.Clear();
page!.ViewModel.TemplateFile = $"系统用户信息-模板.xls";
page!.OnDataSave -= ExcelData_OnDataSave;
page!.OnDataSave += ExcelData_OnDataSave; //导航到指定页面
ViewModel.Navigate(typeof(ImportExcelData));
}

而其中 ImportExcelData 是我们定义的通用导入页面窗体类,这里只需要实现一些属性的设置(根据子类的不同而调整,后期可以用代码生成工具生成),以及一些事件用于子类延后实现,从而可以实现自定义的数据处理的功能。

我们在下面再细说批量导入的处理细节。

2、数据导出到Excel

数据导出到Excel,在我们的Winform端中很常见,而WPF这里也是一样的处理方式,通用利用Excel的操作组件的封装类来实现,可以基于NPOI,也可以基于Aspose.Cell实现,根据自己的需要实现简单的封装调用即可。

导出到Excel,首先需要弹出选择目录的对话框进行选取目录,然后用于生成Excel的文件,如下界面所示。

这个处理,由于WPF可以调用.net里面的System.Windows.Forms,因此我们直接调用里面的对话框处理封装即可,这个类来自于我们的Winform的UI公用类库部分。

在前面随笔,我们介绍过为了WPF开发的方便,我们设计了几个视图基类,用于减少代码的处理。

对于不同的业务类,我们也只需要根据实际情况,生成对应的业务视图模型类即可。

我们把通用的导出操作放到了这个视图基类BaseListViewModel 里面即可,如下代码所示。

/// <summary>
/// 触发导出Excel处理命令
/// </summary>
[RelayCommand]
protected virtual async Task ExportExcel(string title = "列表数据")
{
var table = await this.ConvertItems(this.Items);
BaseExportExcel(table, title);
}

而其中对于DataTable的处理Excel,提供一个通用的方法。

    /// <summary>
/// 可供重写的基类函数,导出Excel
/// </summary>
public virtual void BaseExportExcel(DataTable table, string title = "列表数据")
{
string file = FileDialogHelper.SaveExcel(string.Format("{0}.xls", title));
if (!string.IsNullOrEmpty(file))
{
try
{
string error = "";
AsposeExcelTools.DataTableToExcel2(table, file, out error); if (!string.IsNullOrEmpty(error))
{
MessageDxUtil.ShowError(string.Format("导出Excel出现错误:{0}", error));
}
else
{
if (MessageDxUtil.ShowYesNoAndTips("导出成功,是否打开文件?") == System.Windows.MessageBoxResult.Yes)
{
Process.Start("explorer.exe", file);
}
}
}
catch (Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}
}

其中FileDialogHelper.SaveExcel 的代码如下所示。

/// <summary>
/// 保存Excel对话框,并返回保存全路径
/// </summary>
/// <returns></returns>
public static string SaveExcel(string filename, string initialDirectory)
{
return Save("保存Excel", ExcelFilter, filename, initialDirectory);
} /// <summary>
/// 以指定的标题弹出保存文件对话框
/// </summary>
/// <param name="title">对话框标题</param>
/// <param name="filter">后缀名过滤</param>
/// <param name="filename">默认文件名</param>
/// <param name="initialDirectory">初始化目录</param>
/// <returns></returns>
public static string Save(string title, string filter, string filename, string initialDirectory)
{
//多语言支持
title = JsonLanguage.Default.GetString(title); var dialog = new SaveFileDialog();
dialog.Filter = filter;
dialog.Title = title;
dialog.FileName = filename;
dialog.RestoreDirectory = true;
if (!string.IsNullOrEmpty(initialDirectory))
{
dialog.InitialDirectory = initialDirectory;
} if (dialog.ShowDialog() == DialogResult.OK)
{
return dialog.FileName;
}
return string.Empty;
}

而其中SaveFileDialog是属于.net 中System.Windows.Forms里面的内容,WPF可以直接调用。

而DataTableToExcel2 方法,这是我们封装的一个使用Aspose.Cell的调用,主要用于快速处理DataTable到Excel的操作封装,我们也可可以利用其它操作Excel的封装,如NPOI等都可以实现。

代码如下所示。

/// <summary>
/// 把DataTabel转换成Excel文件
/// </summary>
/// <param name="datatable">DataTable对象</param>
/// <param name="filepath">目标文件路径,Excel文件的全路径</param>
/// <param name="error">错误信息:返回错误信息,没有错误返回""</param>
/// <returns></returns>
public static bool DataTableToExcel2(DataTable datatable, string filepath, out string error)
{
error = "";
var wb = new Aspose.Cells.Workbook(); try
{
if (datatable == null)
{
error = "DataTableToExcel:datatable 为空";
return false;
} //为单元格添加样式
var style = wb.CreateStyle();
//设置居中
style.HorizontalAlignment = Aspose.Cells.TextAlignmentType.Center;
//设置背景颜色
style.ForegroundColor = System.Drawing.Color.FromArgb(153, 204, 0);
style.Pattern = BackgroundType.Solid;
style.Font.IsBold = true; int rowIndex = 0;
for (int i = 0; i < datatable.Columns.Count; i++)
{
DataColumn col = datatable.Columns[i];
string columnName = col.Caption ?? col.ColumnName;
wb.Worksheets[0].Cells[rowIndex, i].PutValue(columnName);
wb.Worksheets[0].Cells[rowIndex, i].SetStyle(style);
}
rowIndex++; foreach (DataRow row in datatable.Rows)
{
for (int i = 0; i < datatable.Columns.Count; i++)
{
wb.Worksheets[0].Cells[rowIndex, i].PutValue(row[i].ToString());
}
rowIndex++;
} for (int k = 0; k < datatable.Columns.Count; k++)
{
wb.Worksheets[0].AutoFitColumn(k, 0, 150);
}
wb.Worksheets[0].FreezePanes(1, 0, 1, datatable.Columns.Count);
wb.Save(filepath);
return true;
}
catch (Exception e)
{
error = error + " DataTableToExcel: " + e.Message;
return false;
} }

导出Excel的内容如下界面所示。另外导出文档的内容,我们可以用于导入的数据模板的。

我们可以根据需要设置要导出的列即可。

3、数据导出到PDF

同样,数据导出到PDF的处理操作类似,也是通过视图基类的封装方法,实现快速的导出到PDF处理,如下是视图基类里面的实现方法。

/// <summary>
/// 触发导出PDF处理命令
/// </summary>
[RelayCommand]
protected virtual async Task ExportPdf(string title = "列表数据")
{
var table = await this.ConvertItems(this.Items);
BaseExportPdf(table, title);
} /// <summary>
/// 可供重写的基类函数,导出PDF
/// </summary>
public virtual void BaseExportPdf(DataTable table, string title = "列表数据")
{
var pdfFile = FileDialogHelper.SavePdf();
if (!pdfFile.IsNullOrEmpty())
{
bool isLandscape = true;//是否为横向打印,默认为true
bool includeHeader = true;//是否每页包含表头信息
var headerAlignment = iText.Layout.Properties.HorizontalAlignment.CENTER;//头部的对其方式,默认为居中
float headerFontSize = 9f;//头部字体大小
float rowFontSize = 9f;//行记录字体大小
float? headerFixHeight = null;//头部的固定高度,否则为自适应 var success = TextSharpHelper.ExportTableToPdf(title, table, pdfFile, isLandscape, includeHeader, headerAlignment, headerFontSize, rowFontSize, headerFixHeight); //提示信息
var message = success ? "导出操作成功" : "导出操作失败";
if (success)
{
Growl.SuccessGlobal(message);
Process.Start("explorer.exe", pdfFile);
}
else
{
Growl.ErrorGlobal(message);
}
}
}

通过把List<T>的列表转换为常规的DataTable来处理,我们就可以利用之前我们随笔《在Winform分页控件中集成导出PDF文档的功能》介绍到的PDF导出函数来实现WPF数据导出到PDF的处理。

上面的 TextSharpHelper 就是对于itext7进行的封装,实现PDF的导出处理。

引入相关的Nugget类后,封装它的辅助类代码如下所示。

    /// <summary>
/// 基于iText7对PDF的导出处理
/// </summary>
public static class TextSharpHelper
{
/// <summary>
/// datatable转PDF方法
/// </summary>
/// <param name="title">标题内容</param>
/// <param name="data">dataTable数据</param>
/// <param name="pdfFile">PDF文件保存的路径</param>
/// <param name="isLandscape">是否为横向打印,默认为true</param>
/// <param name="includeHeader">是否每页包含表头信息</param>
/// <param name="headerAlignment">头部的对其方式,默认为居中对其</param>
/// <param name="headerFontSize">头部字体大小</param>
/// <param name="rowFontSize">行记录字体大小</param>
/// <param name="headerFixHeight">头部的固定高度,否则为自适应</param>
/// <returns></returns>
public static bool ExportTableToPdf(string title, DataTable data, string pdfFile, bool isLandscape = true, bool includeHeader = true, iText.Layout.Properties.HorizontalAlignment headerAlignment = iText.Layout.Properties.HorizontalAlignment.CENTER, float headerFontSize = 9f, float rowFontSize = 9f, float? headerFixHeight = null)
{var writer = new PdfWriter(pdfFile);
PdfDocument pdf = new PdfDocument(writer);
pdf.SetDefaultPageSize(isLandscape ? PageSize.A4.Rotate() : PageSize.A4); //A4横向
var doc = new Document(pdf);//设置标题
if (!string.IsNullOrEmpty(title))
{
var param = new Paragraph(title)
.SetFontColor(iText.Kernel.Colors.ColorConstants.BLACK)
.SetBold() //粗体
.SetFontSize(headerFontSize + 5)
.SetTextAlignment(TextAlignment.CENTER); //居中
doc.Add(param);
}
var table = new Table(data.Columns.Count)
.SetTextAlignment(TextAlignment.CENTER)
.SetVerticalAlignment(VerticalAlignment.MIDDLE)
.SetWidth(new UnitValue(UnitValue.PERCENT, 100));//缩放比例
table.UseAllAvailableWidth();
//添加表头
foreach (DataColumn dc in data.Columns)
{
var caption = !string.IsNullOrEmpty(dc.Caption) ? dc.Caption : dc.ColumnName;
var cell = new Cell().Add(new Paragraph(caption))
.SetBold()
.SetVerticalAlignment(VerticalAlignment.MIDDLE)
.SetHorizontalAlignment(headerAlignment)
.SetPadding(1)
.SetFontSize(headerFontSize);
if (headerFixHeight.HasValue)
{
cell.SetHeight(new UnitValue(UnitValue.POINT, headerFixHeight.Value));
}
table.AddHeaderCell(cell);
}
//插入数据
var colorWhite = Color.ConvertRgbToCmyk(iText.Kernel.Colors.WebColors.GetRGBColor("White"));// System.Drawing.Color.White;
var colorEvent = iText.Kernel.Colors.WebColors.GetRGBColor("LightCyan");// System.Drawing.Color.LightCyan;
var EventRowBackColor = Color.ConvertRgbToCmyk(colorEvent);
for (int i = 0; i < data.Rows.Count; i++)
{
table.StartNewRow();//第一列开启新行
var backgroudColor = ((i % 2 == 0) ? colorWhite : EventRowBackColor);
for (int j = 0; j < data.Columns.Count; j++)
{
var text = data.Rows[i][j].ToString();
var cell = new Cell()
.SetBackgroundColor(backgroudColor)
.SetFontSize(rowFontSize)
.SetVerticalAlignment(VerticalAlignment.MIDDLE)
.Add(new Paragraph(text));
table.AddCell(cell);
}
}
doc.Add(table);
pdf.Close();
writer.Close();
return true;
}
}

导出PDF的文档效果如下所示。

4、导入Excel数据

Excel数据的导入,可以降低批量处理数据的难度和繁琐的界面一个个录入,这种是一种常见的操作方式,我们主要提供固定的模板给客户下载录入数据,然后提交进行批量的导入即可。

导入的界面处理,我们这里涉及一个通用的导入界面(和WInform端的界面类似),这样我们每个不同的业务导入处理都可以重用,只需要设置一些不同的属性,以及一些事件的处理即可,如下是通用的界面效果。

我们这里主要针对性的介绍它的设计方式,前面我们介绍,在业务界面里面调用它的时候,如下代码所示。

/// <summary>
/// 导出内容到Excel
/// </summary>
[RelayCommand]
private void ImportExcel()
{
var page = App.GetService<ImportExcelData>();
page!.ViewModel.Items?.Clear();
page!.ViewModel.TemplateFile = $"系统用户信息-模板.xls";
page!.OnDataSave -= ExcelData_OnDataSave;
page!.OnDataSave += ExcelData_OnDataSave; //导航到指定页面
ViewModel.Navigate(typeof(ImportExcelData));
}

这个通用的窗体里面的视图模型,定义了一个模板的文件名称,以及一个通用的数据DataTable的集合,以及一个事件用于子类的导入转换的实现,它的视图模型类代码如下所示。

    /// <summary>
/// 批量导入Excel数据的视图模型基类
/// </summary>
public partial class ImportExcelDataViewModel : BaseViewModel
{
[ObservableProperty]
private string templateFile; [ObservableProperty]
private string importFilePath; [ObservableProperty]
private DataTable items;

我们为了给客户打开模板文件,方便用于录入Excel数据,因此我们在本地打开模板文件即可。

        /// <summary>
/// 打开模板文件
/// </summary>
[RelayCommand]
private void OpenFile()
{
if (!this.TemplateFile.IsNullOrEmpty())
{
var realFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, this.TemplateFile);
if (File.Exists(realFilePath))
{
Process.Start("explorer.exe", realFilePath);
}
else
{
MessageDxUtil.ShowError($"没有找到该模板文件:{realFilePath}");
}
}
}

在通用的导入页面的后台代码里面,我们需要实现一些如选择Excel后,显示数据到DataGrid的操作,以及批量保存数据的处理。

        /// <summary>
/// 选择Excel文件后,显示Excel里面的表格数据
/// </summary>
[RelayCommand]
private void BrowseExcel()
{
string file = FileDialogHelper.OpenExcel();
if (!string.IsNullOrEmpty(file))
{
this.ViewModel.ImportFilePath = file;
ViewData();
}
}
        /// <summary>
/// 查看Excel文件并显示在界面上操作
/// </summary>
private void ViewData()
{
if (this.txtFilePath.Text == "")
{
MessageDxUtil.ShowTips("请选择指定的Excel文件");
return;
} try
{
var myDs = new DataSet(); string error = "";
AsposeExcelTools.ExcelFileToDataSet(this.txtFilePath.Text, out myDs, out error);
this.ViewModel.Items = myDs.Tables[0];
}
catch (Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}

导入处理的操作代码如下所示。

/// <summary>
/// 批量保存数据到数据库
/// </summary>
/// <returns></returns>
[RelayCommand]
private async Task<CommonResult> SaveData()
{
if (ViewModel.Items == null || ViewModel.Items?.Rows?.Count == 0)
return new CommonResult(false); if (MessageDxUtil.ShowYesNoAndWarning("该操作将把数据导入到系统数据库中,您确定是否继续?") == System.Windows.MessageBoxResult.Yes)
{
var dt = this.ViewModel.Items;
foreach (DataRow dr in dt.Rows)
{
try
{
await OnDataSave(dr);
}
catch (Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}
return new CommonResult(true, "操作成功");
}
return new CommonResult(false);
}

注意,我们这里使用了事件的处理,把数据的转换逻辑留给子类去实现的。

        /// <summary>
/// 数据保存的事件
/// </summary>
public event SaveDataHandler OnDataSave;

这样我们在用户信息的导入页面UserListPage.xaml.cs里面的代码就可以根据实际的情况进行实现事件了。

    /// <summary>
/// 导出内容到Excel
/// </summary>
[RelayCommand]
private void ImportExcel()
{
var page = App.GetService<ImportExcelData>();
page!.ViewModel.Items?.Clear();
page!.ViewModel.TemplateFile = $"系统用户信息-模板.xls";
page!.OnDataSave -= ExcelData_OnDataSave;
page!.OnDataSave += ExcelData_OnDataSave; //导航到指定页面
ViewModel.Navigate(typeof(ImportExcelData));
}

这个事件的实现,主要就是把个性化的用户信息(用户信息模板里面定义的字段),转换为DataTable的行信息即可,如下代码所示。具体根据模板设计的情况进行修改即可。

具体就不再一一赘述,主要就是基类逻辑和具体实现分离,实现不同的业务功能处理即可。

以上即是我们一个列表通用页面里面,往往需要用到的通用性的导入、导出操作的介绍,希望对读者在开发WPF应用功能上有所启发,有所参考,善莫大焉。

循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(4) -- 实现DataGrid数据的导入和导出操作的更多相关文章

  1. 基于Metronic的Bootstrap开发框架经验总结(7)--数据的导入、导出及附件的查看处理

    在很多系统模块里面,我们可能都需要进行一定的数据交换处理,也就是数据的导入或者导出操作,这样的批量处理能给系统用户更好的操作体验,也提高了用户录入数据的效率.我在较早时期的EasyUI的Web框架上, ...

  2. (转)基于Metronic的Bootstrap开发框架经验总结(7)--数据的导入、导出及附件的查看处理

    http://www.cnblogs.com/wuhuacong/p/4777720.html 在很多系统模块里面,我们可能都需要进行一定的数据交换处理,也就是数据的导入或者导出操作,这样的批量处理能 ...

  3. 基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发

    我喜欢在一个项目开发模式成熟的时候,使用代码生成工具Database2Sharp来配套相关的代码生成,对于我介绍的基于SqlSugar的开发框架,从整体架构确定下来后,我就着手为它们量身定做相关的代码 ...

  4. 基于MVC4+EasyUI的Web开发框架经验总结(10)--在Web界面上实现数据的导入和导出

    数据的导入导出,在很多系统里面都比较常见,这个导入导出的操作,在Winform里面比较容易实现,我曾经在之前的一篇文章<Winform开发框架之通用数据导入导出操作>介绍了在Winform ...

  5. (转)基于MVC4+EasyUI的Web开发框架经验总结(10)--在Web界面上实现数据的导入和导出

    http://www.cnblogs.com/wuhuacong/p/3873498.html 数据的导入导出,在很多系统里面都比较常见,这个导入导出的操作,在Winform里面比较容易实现,我曾经在 ...

  6. 基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理

    在SqlSugar的开发框架的后端,我们基于Web API的封装了统一的返回结果,使得WebAPI的接口返回值更加简洁,而在前端,我们也需要统一对返回的结果进行解析,并获取和Web API接口对应的数 ...

  7. 推荐一个基于Vue2.0的的一款移动端开发的UI框架,特别好用。。。

    一丶YDUI 一只注重审美,且性能高效的移动端&微信UI. 下面为地址自己研究去吧! 我的项目正在用,以前用的Mint-ui但是现在感觉还是这个好一点,官方给出的解释很清楚,很实用. 官方地址 ...

  8. 基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理

    我们在设计数据库表的时候,往往为了方便,主键ID一般采用字符串类型或者GUID类型,这样对于数据库表记录的迁移非常方便,而且有时候可以在处理关联记录的时候,提前对应的ID值.但有时候进行数据记录插入的 ...

  9. 基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转

    在前面随笔,我们介绍过这个基于SqlSugar的开发框架,我们区分Interface.Modal.Service三个目录来放置不同的内容,其中Modal是SqlSugar的映射实体,Interface ...

  10. 基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口

    在基于SqlSugar的开发框架中,我们设计了一些系统服务层的基类,在基类中会有很多涉及到相关的数据处理操作的,如果需要跟踪具体是那个用户进行操作的,那么就需要获得当前用户的身份信息,包括在Web A ...

随机推荐

  1. CF1583H Omkar and Tours 题解

    题意: 给定一个 \(n\) 个点的树,每条边有权值 \(t\) 和 \(c\).一条路径的权值为所经过节点的 \(\max(c)\). 每个点有权值 \(e\). 给出 \(q\) 个询问,每次询问 ...

  2. windows10环境下安装RabbitMQ以及延时插件(图文)

    安装转载:https://www.cnblogs.com/saryli/p/9729591.html 插件转载:https://blog.csdn.net/nbdclw/article/details ...

  3. go 常用命令总结

    转载请注明出处: go build:编译包和依赖项,生成可执行文件.命令用于编译包和依赖项,生成可执行文件.当对Go程序进行修改后,需要使用go build命令重新编译程序,以生成新的可执行文件.该命 ...

  4. 前端学习C语言 - 开篇

    前端学习C语言 - 开篇 前端学习C语言有很多理由:工作.兴趣或其他. C 语言几个常见的使用场景: 操作系统开发:Linux 操作系统的内核就是主要由 C 语言编写的.其他操作系统也广泛使用 C 语 ...

  5. mysql截取函数,拼接函数,大写函数例子

    题目:这题目是牛客网sql题,因为牵扯到3个函数,都是自己没怎么用过的,所以记录一下. 答案:是别人的解题思路 可以看出在mysql中提供的函数可以供我们使用来操作字段,非常的方便

  6. CentOS 7 搭建NFS服务器

    服务端安装 # 创建挂载目录 cd ~ cd data/ mkdir www-content cd www-content/ pwd # 安装软件 yum install nfs-utils yum ...

  7. U8接口开发

    https://console-docs.apipost.cn/preview/b9674fcd9949865b/a5a249fb27736c15 模块 单据 功能说明 库存管理       其他出库 ...

  8. Redis持久化机制 RDB、AOF、混合持久化详解!如何选择?

    本文已经收录进 JavaGuide(「Java学习+面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识.) Redis 持久化机制属于后端面试超高频的面试知识点,老生常谈了,需要重点花时间 ...

  9. 如何用 Java 写一个 Java 虚拟机

    项目链接 https://github.com/FranzHaidnor/haidnorJVM haidnorJVM 使用 Java17 编写的 Java 虚拟机 意义 纸上得来终觉浅,绝知此事要躬行 ...

  10. React: React-Router嵌套路由 exact问题

    说明 当使用嵌套路由时,不能在父路由中添加exact,因为要先匹配父路由才能匹配子路由 父路由 子路由 效果如下所示 参考链接 https://www.jianshu.com/p/8bc3251079 ...