背景
使用情景介绍:前端使用Vue,后端语言使用C#。
一、前端
1、api
首先,我们需要在Vue项目中,定义一个api接口:
url指向x导入Excel的后端接口路由,
请求参数为post,
文本类型为“multipart/form-data”
具体格式参考下边代码。
export function ImportTest(data) {
return request({
url: '/api/leeway/ImportTest',
method: "post",
ContentType: "multipart/form-data",
data: data
});
}
2、上传文件对话框
定义一个上传文件的对话框(dialog)
把对话框代码放进需要导入文件的页面上,并写一个按钮绑定点击事件,实现点击触发打开对话框功能。
这里我用的是importTest()函数,点击实现将对话框属性importTestDialogVisible置为true。
代码放在下边第三点(相关调用方法)
<el-dialog
title="导入测试(请按照测试模板导入)"
:visible.sync="importTestDialogVisible"
:modal-append-to-body="false"
:append-to-body="true"
top="7vh"
align="center"
>
<div>
<el-upload
drag
:limit="limitNum"
:auto-upload="false"
accept=".xlsx"
:action="UploadUrl()"
:before-upload="beforeUploadFile"
:on-change="fileChange"
:on-exceed="exceedFile"
:on-success="handleSuccess"
:on-error="handleError"
:file-list="fileList"
:on-remove="removefile"
>
<i class="el-icon-upload" />
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<div slot="tip" class="el-upload__tip">
只能上传xlsx文件,且不超过10M
</div>
</el-upload>
<br>
<div style="margin-bottom: 10px">
<el-button icon="el-icon-download" size="small" type="primary" @click="exportTestTemplate">测试模板</el-button>
<el-button icon="el-icon-upload2" size="small" type="primary" @click="uploadFile">立即上传</el-button>
<el-button size="small" @click="canceluploadFile">取消</el-button>
</div>
</div>
</el-dialog>
3、相关数据定义和调用方法
这里提供给与本次实战的上传文件对话框相关的函数,另外还有导出模板函数exportTestTemplate
我们做一个导出Excel模板功能,只需两个步骤:
1、在本地新创建一个excel文件并把它命名为“导入测试模板”。
2、把该模板文件放在Vue项目的public文件夹下。
//导出模板调用方法,一般把模板放在项目的public目录下
exportTestTemplate(){
const aTag = document.createElement("a");
aTag.href = "./导入测试模板.xlsx"; //这里是放模板的地方
aTag.download = "测试模板.xlsx";
aTag.click();
},
importTest(){
this.importTestDialogVisible = true;
},
removefile() {
this.fileList = [];
},
canceluploadFile() {
this.importQuotaDialogVisible = false;
},
//上传文件调用方法
uploadFile(){
if (this.fileList.length === 0) {
this.$message.warning("请先选择文件");
} else {
const formData = new FormData();
formData.append("file", this.fileList[0]);
console.log(formData);
console.log(this.fileList);
//ImportTest对应api里的方法名
ImportTest(formData).then((res) => {
if (res.code == "0") {
this.$message.success("导入测试成功!");
} else {
this.$message.error(res.message);
}
this.importTestDialogVisible = false;
this.fileList = [];
//这里可以重新调用一边父页面的加载方法,可忽略
});
}
},
// 文件超出个数限制时的钩子
exceedFile(files, fileList) {
this.$message.warning(
`只能选择 ${this.limitNum} 个文件,当前共选择了 ${
files.length + fileList.length
} 个`
);
},
// 文件状态改变时的钩子
fileChange(file, fileList) {
console.log(file.raw);
this.fileList.push(file.raw);
console.log(this.fileList);
},
// 上传文件之前的钩子, 参数为上传的文件,若返回 false 或者返回 Promise 且被 reject,则停止上传
beforeUploadFile(file) {
console.log("before upload");
console.log(file);
const extension = file.name.substring(file.name.lastIndexOf(".") + 1);
const size = file.size / 1024 / 1024;
if (extension !== "xlsx") {
this.$message.warning("只能上传后缀是.xlsx的文件");
}
if (size > 10) {
this.$message.warning("文件大小不得超过10M");
}
},
// 文件上传成功时的钩子
handleSuccess(res, file, fileList) {
this.$message.success("文件上传成功!");
},
// 文件上传失败时的钩子
handleError(err, file, fileList) {
if (err) {
console.log(err);
}
this.$message.error("文件上传失败");
},
UploadUrl: function() {
// 因为action参数是必填项,我们使用二次确认进行文件上传时,直接填上传文件的url会因为没有参数导致api报404,所以这里将action设置为一个返回为空的方法就行,避免抛错
return "";
},
二、后端
1、简单粗暴写法
需要接收方式、路由,这里的route内容与第一部分api里的url相对应。
[HttpPost]
[Route("ImportTest")]
public async Task<ServiceResult> ImportTest([FromForm] IFormFile file)
{
var result = new ServiceResult<ImportTestDto>();
try
{
string webRootPath = _hostingEnvironment.ContentRootPath;
//定义存放上传文件的路径,需自己创建好文件夹
string filePath = webRootPath + $"/FileUpload/TestFiles/";
if (!Directory.Exists(filePath))
{
Directory.CreateDirectory(filePath);
}
var formFile = file;
if (formFile.Length > 0)
{
string fileExt = formFile.FileName.Split('.')[1]; //文件扩展名,不含“.”
long fileSize = formFile.Length; //获得文件大小,以字节为单位
string newFileName = Guid.NewGuid() + "." + fileExt; //随机生成新的文件名
string fileNamePath = Path.Combine(filePath, newFileName);
using (var stream = new FileStream(fileNamePath, FileMode.Create))
{
formFile.CopyTo(stream);
stream.Flush();
}
List<ImportTestDto> ListEntity = new List<ImportTestDto>();
//调用ExcelHelper方法
DataTable dt = NPOIHelper.ExcelToDataTable(fileNamePath, true);
if (dt.Rows.Count > 0)
{
int rowStart = 2;
string msg = string.Empty;
foreach (DataRow dr in dt.Rows)
{
var oneMsg = $"【数据校验】第{rowStart}行:";
ImportTestDto model = new ImportTestDto();
if (dr["姓名"].ToString() == "")
{
msg += "姓名为空<br/>";
}
else
{
model.Name = dr["姓名"].ToString();
}
if (dr["号码"].ToString() == "")
{
msg += "姓名为空<br/>";
}
else if (dr["号码"].ToString().Length != 11)
{
msg += "号码长度不是11位<br/>";
}
else
{
model.Number = dr["号码"].ToString();
}
if (ListEntity.Where(u => u.StudentNo == model.StudentNo).Count() > 0)
{
msg += "与Excel前面行的学号重复<br/>";
}
//校验系统是否已有导入过该学号,此方法需要自己写
if (await _studentService.IsExistStudent(model.StudentNo))
{
msg += "学号在系统中已存在,请确认<br/>";
}
if (dr["金额"].ToString() == "")
{
msg += "金额为空<br/>";
}
else if (decimal.TryParse(dr["金额"].ToString(), out decimal amount))
{
if (amount < 0)
{
msg += "金额不能为负数<br/>";
}
else
{
model.Amount = amount;
}
}
else
{
msg += "金额格式有误<br/>";
}
if (dr["日期"].ToString() == "")
{
msg += "日期为空<br/>";
}
else if (DateTime.TryParse(dr["日期"].ToString(), out DateTime invoiceDate))
{
model.Date = date;
}
else
{
msg += "日期格式有误<br/>";
}
if (oneMsg != $"【数据校验】第{rowStart}行:")
{
msg += $"{oneMsg}";
}
rowStart++;
ListEntity.Add(model);
}
if (msg != string.Empty)
{
result.IsFailed(msg);
}
else
{
if (ListEntity.Count > 0)
{
//InsertStudents为插入数据列表的方法,需要自己写
var SaveResult = await _studentService。InsertStudents(ListEntity);
if (SaveResult)
{
result.IsSuccess("导入成功");
}
else
{
result.IsFailed("导入失败");
}
}
else
{
result.IsFailed("导入失败,没有导入数据!");
}
}
}
else
{
result.IsFailed("导入失败,没有数据!");
}
}
}
catch (Exception ex)
{
result.IsFailed(ex.Message);
}
return result;
}
2、改进写法
在这,使用了反射做了一个小的优化,感兴趣的可以看看
[HttpPost]
[Route("ImportTest")]
public async Task<ServiceResult> ImportTest([FromForm] IFormFile file)
{
try
{
string webRootPath = _hostingEnvironment.ContentRootPath;
//定义存放上传文件的路径,需自己创建好文件夹
string filePath = webRootPath + $"/FileUpload/TestFiles/";
if (!Directory.Exists(filePath))
{
Directory.CreateDirectory(filePath);
}
var formFile = file;
if (formFile.Length > 0)
{
string fileExt = formFile.FileName.Split('.')[1]; //文件扩展名,不含“.”
long fileSize = formFile.Length; //获得文件大小,以字节为单位
string newFileName = Guid.NewGuid() + "." + fileExt; //随机生成新的文件名
string fileNamePath = Path.Combine(filePath, newFileName);
using (var stream = new FileStream(fileNamePath, FileMode.Create))
{
formFile.CopyTo(stream);
stream.Flush();
}
List<ImportTestDto> ListEntity = new List<ImportTestDto>();
//调用ExcelHelper方法, 后边会给出该类的代码
DataTable dt = NPOIHelper.ExcelToDataTable(fileNamePath, true);
#region 导入模板对应的字段配置(这里也可以使用反射,获取dto的中文注释,这么写是防止注释被改导致导入Excel出错)
var dataLabel = new Dictionary<string, string>
{
{ "Jan", "1月" },
{ "Feb", "2月" },
{ "Mar", "3月" },
{ "Apr", "4月" },
{ "May", "5月" },
{ "Jun", "6月" },
{ "Jul", "7月" },
{ "Aug", "8月" },
{ "Sep", "9月" },
{ "Oct", "10月" },
{ "Nov", "11月" },
{ "Dec", "12月" },
{ "Name", "姓名" },
{ "Scale", "比例(%)" },
{ "StudentNo", "学号" }
};
#endregion
if (dt.Rows.Count > 0)
{
int rowStart = 2;
string msg = string.Empty;
PropertyInfo[] propertyInfos = typeof(ImportTestDto).GetProperties();
foreach (DataRow dr in dt.Rows)
{
var oneMsg = $"【数据校验】第{rowStart}行:";
ImportTestDto model = new ImportTestDto();
foreach (var item in dataLabel)
{
//全部列统一进行空校验
if (dr[$"{item.Value}"].ToString() == "")
{
oneMsg += $"{item.Value}数据为空<br/>";
}
else
{
//对特殊列格式进行校验
if (item.Value.Contains("月"))
{
if (decimal.TryParse(dr[$"{item.Value}"].ToString(), out decimal amount))
{
if (amount < 0)
{
oneMsg += $"{item.Value}不能为负数<br/>";
}
else
{
propertyInfos.First(p => p.Name == item.Key).SetValue(model, amount);
}
}
else
{
oneMsg += $"{item.Value}格式有误<br/>";
}
}
else if (item.Value.Contains("%"))
{
decimal.TryParse(dr[$"{item.Value}"].ToString(), out decimal scaleLabel);
propertyInfos.First(p => p.Name == item.Key).SetValue(model, Convert.ToInt32(scaleLabel * 100).ToString());
}
else
{
if (ListEntity.Where(u => u.StudentNo== dr[$"{item.Value}"].ToString()).Count() > 0)
{
oneMsg += $"{item.Value}与Excel前面行的StudentNo重复<br/>";
}
propertyInfos.First(p => p.Name == item.Key).SetValue(model, dr[$"{item.Value}"].ToString());
}
}
}
if (oneMsg != $"【数据校验】第{rowStart}行:")
{
msg += $"{oneMsg}";
}
rowStart++;
ListEntity.Add(model);
}
if (msg != string.Empty)
{
result.IsFailed(msg);
}
else
{
if (ListEntity.Count > 0)
{
//InsertStudents为插入数据列表的方法,需要自己写
var SaveResult = await _studentService。InsertStudents(ListEntity);
if (SaveResult)
{
result.IsSuccess("导入成功");
}
else
{
result.IsFailed("导入失败");
}
}
else
{
result.IsFailed("导入失败,没有导入数据!");
}
}
}
else
{
result.IsFailed("导入失败,没有数据!");
}
}
}
catch (Exception ex)
{
result.IsFailed(ex.Message);
}
return result;
}
三、NPOI类
在把文件转成Datatabel时,需用到一下方法,代码如下所示:
public class NPOIHelper
{
/// <summary>
/// 将excel导入到datatable
/// </summary>
/// <param name="filePath">excel路径</param>
/// <param name="isColumnName">第一行是否是列名</param>
/// <returns>返回datatable</returns>
public static DataTable ExcelToDataTable(string filePath, bool isColumnName)
{
DataTable dataTable = null;
FileStream fs = null;
DataColumn column = null;
DataRow dataRow = null;
IWorkbook workbook = null;
ISheet sheet = null;
IRow row = null;
ICell cell = null;
int startRow = 0;
try
{
using (fs = System.IO.File.OpenRead(filePath))
{
// 2007版本
if (filePath.IndexOf(".xlsx") > 0)
workbook = new XSSFWorkbook(fs);
// 2003版本
else if (filePath.IndexOf(".xls") > 0)
workbook = new HSSFWorkbook(fs);
if (workbook != null)
{
sheet = workbook.GetSheetAt(0);//读取第一个sheet,当然也可以循环读取每个sheet
dataTable = new DataTable();
if (sheet != null)
{
int rowCount = sheet.LastRowNum;//总行数
if (rowCount > 0)
{
IRow firstRow = sheet.GetRow(0);//第一行
int cellCount = firstRow.LastCellNum;//列数
//构建datatable的列
if (isColumnName)
{
startRow = 1;//如果第一行是列名,则从第二行开始读取
for (int i = firstRow.FirstCellNum; i < cellCount; ++i)
{
cell = firstRow.GetCell(i);
if (cell != null)
{
if (cell.StringCellValue != null)
{
column = new DataColumn(cell.StringCellValue);
dataTable.Columns.Add(column);
}
}
}
}
else
{
for (int i = firstRow.FirstCellNum; i < cellCount; ++i)
{
column = new DataColumn("column" + (i + 1));
dataTable.Columns.Add(column);
}
}
//填充行
for (int i = startRow; i <= rowCount; ++i)
{
row = sheet.GetRow(i);
if (row == null) continue;
if (row.FirstCellNum == -1) continue;
dataRow = dataTable.NewRow();
for (int j = row.FirstCellNum; j < cellCount; ++j)
{
cell = row.GetCell(j);
if (cell == null)
{
dataRow[j] = "";
}
else
{
//CellType(Unknown = -1,Numeric = 0,String = 1,Formula = 2,Blank = 3,Boolean = 4,Error = 5,)
switch (cell.CellType)
{
case CellType.Blank:
dataRow[j] = "";
break;
case CellType.Numeric:
short format = cell.CellStyle.DataFormat;
//对时间格式(2015.12.5、2015 / 12 / 5、2015 - 12 - 5等)的处理
// if (format == 14 || format == 31 || format == 57 || format == 58 || format == 176 || format == 177
if (HSSFDateUtil.IsCellDateFormatted(cell))
{
dataRow[j] = cell.DateCellValue;
}
else { dataRow[j] = cell.NumericCellValue; }
break;
case CellType.String:
dataRow[j] = cell.StringCellValue;
break;
}
}
}
dataTable.Rows.Add(dataRow);
}
}
}
}
}
return dataTable;
}
catch (Exception ex)
{
if (fs != null)
{
fs.Close();
}
throw ex;
}
}
}
四、致谢
感谢大家的阅读,祝大家学习愉快,记得点赞关注支持一波!