前言
本篇文章是从零开始搭建报表工具系列文章的第一篇,这一篇主要实现一下实际工作环境中真实遇到的问题,在实际工作中经常会存在需要把 Excel
的数据转化为Word
文档的操作,利用 Excel
中作为数据源为报表提供数据支撑。为此,本章将优先开发数据填报部分。
可能适用的场景
- 数据汇总场景,多次填报的数据进行汇总整理,以便后续分析,报表展示。
- 文件转换场景,可以在Excel中配置整个文件需要的数据,直接生成合同、委托单、审批表等文件。
- 检测报告的制作,设备采集的数据往往都是类Excel的形式,同样可转换成检测报告。
- 替代无纸化,无纸化不仅录入效率低,归档数据不容易复用,后期分析需进行数据整理。
- 去除每日重复的工作,把工作量向数据源整理偏移,减少反复操作Word样式。
- 利用Excel进行公式计算后将结果输出或呈现到Word中的场景,此功能将采用
Excel
导入方式实现。
实现原理
使用流程图
体验网址
体验网址:http://121.41.170.62/login
项目还在一点点完善中,仅供学习参考!可能无法访问。
实现效果
当前Excel数据源仅支持静态表格
,如果你的数据源是列表方式,请查看后续文章动态表单
部分。
静态表格:不存在任何新增行或插入列的表格,所有的位置都是固定不变的。
制作输入模板
-
准备一个
Excel静态表格
作为数据源,这里以一个授权委托书.xlsx
为例,内容像这样
-
进入模板制作\输入模板\新增模板,输入模板名称并上传此
授权委托书.xlsx
,提交保存。
制作输出模板
- 准备一个
Word文件
作为输出,这里以一个授权委托书.docx
为例,内容像这样
-
当然这不是最终形态,我们需要在输出模板中加上标记,方便系统找到位置,系统采用双英文括号作为标记,像这样:
{{标记}}
,做完标记的模板长这样
-
进入模板制作\输出模板\找到刚上传的
授权委托书
,点击上传,提交保存。
-
点击绑定,绑定相对应的位置,可点击静态按钮唤醒输入模板直接选择。应用完成绑定。
数据填报并生成报告
-
在线填报\选择使用模板,选择
授权委托书-【单次采集】
,在绑定的对应位置添加数据
-
点击
开始生成
按钮等待成功,成功会自动下载。
最终报告展示
关键源码(C#部分)
使用Excel文件获取Html代码,再使用前端进行渲染。
public static clsGeneralResponse ConvertExcelToHtml(string filePath)
{
clsGeneralResponse r = new clsGeneralResponse();
try
{
StringBuilder sb = new StringBuilder();
using (FileStream file = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
var workbook = new XSSFWorkbook(file);
var sheet = workbook.GetSheetAt(0);
sb.Append("<table><tbody>");
int maxRowCount = 0;
int maxColumnCount = 0;
for (int i = 0; i <= sheet.LastRowNum; i++)
{
maxRowCount++;
var row = sheet.GetRow(i);
if (row == null)
{
continue;
}
int columnCount = row.LastCellNum;
if (columnCount > maxColumnCount)
{
maxColumnCount = columnCount;
}
}
int mergedRegionsCount = sheet.NumMergedRegions;
bool[,] mergedCells = new bool[maxRowCount, maxColumnCount];
for (int i = 0; i < mergedRegionsCount; i++)
{
var mergedRegion = sheet.GetMergedRegion(i);
int startRow = mergedRegion.FirstRow;
int endRow = mergedRegion.LastRow;
int startColumn = mergedRegion.FirstColumn;
int endColumn = mergedRegion.LastColumn;
for (int row = startRow; row <= endRow; row++)
{
for (int col = startColumn; col <= endColumn; col++)
{
mergedCells[row, col] = true;
}
}
}
for (int i = 0; i < maxRowCount; i++)
{
var row = sheet.GetRow(i);
sb.Append("<tr style=\"text-align:center\">");
for (int j = 0; j < maxColumnCount; j++)
{
var col_name = GetExcelColumnHeader(j);
var cellvalue = "";
if (row == null)
{
}
else
{
var cell = row.GetCell(j);
cellvalue = (cell?.ToString() ?? "");
}
var td = "<td class=\"selectTd\" location=\"" + (col_name + (i + 1)) + "\" lable style=\"display:none;\">" + cellvalue + "</td>";
if (cellvalue.Contains("Alert:"))
{
td = "<td οnclick=\"EventAction(this,'" + cellvalue + "')\" class=\"selectTd\" location=\"" + (col_name + (i + 1)) + "\" lable style=\"display:none;\"></td>";
}
if (!mergedCells[i, j])
{
td = td.Replace("display:none;", "").Replace("lable", "");
}
else
{
int colspan = 1;
int rowspan = 1;
for (int k = 0; k < mergedRegionsCount; k++)
{
var mergedRegion = sheet.GetMergedRegion(k);
int startRow = mergedRegion.FirstRow;
int endRow = mergedRegion.LastRow;
int startColumn = mergedRegion.FirstColumn;
int endColumn = mergedRegion.LastColumn;
if (i == startRow && j == startColumn)
{
rowspan = mergedRegion.LastRow - mergedRegion.FirstRow + 1;
colspan = mergedRegion.LastColumn - mergedRegion.FirstColumn + 1;
td = td.Replace("display:none;", "").Replace("lable", "colspan=\"" + colspan + "\" rowspan=\"" + rowspan + "\"");
}
else
{
continue;
}
}
}
sb.Append(td);
}
sb.Append("</tr>");
}
}
HtmlDocument doc = new HtmlDocument();
doc.LoadHtml(sb.ToString());
var rows = doc.DocumentNode.SelectNodes("//tr");
var rowscount = rows.Count;
int maxCols = 0;
if (rows != null)
{
foreach (var row in rows)
{
int cols = row.SelectNodes(".//td")?.Count ?? 0;
maxCols = Math.Max(cols, maxCols);
}
}
if (rowscount < 10)
{
for (int i = 0; i < 10; i++)
{
var td = "";
for (int j = 0; j < maxCols; j++)
{
td += "<td></td>";
}
sb.Append("<tr>" + td + "</tr>");
}
}
sb.Append("</tbody></table>");
r.Code = 200;
r.Msg = "ok";
r.Data = sb.ToString();
}
catch (Exception ex)
{
r.Code = 500;
r.Msg = ex.Message;
r.Data = "error";
}
return r;
}
前端异步获取HTML并渲染,后端返回的HTML为Table 组件部分
。
$.ajax({
url: '/api/xxx/getHtml', // 你的处理HTML的服务器端url
type: 'POST',
data: JSON.stringify({ temid: '模板id' }),
dataType: 'json',
contentType: 'application/json',
success: function (response) {
if (response.code == 200) {
console.log(response.data)
//此处需自行渲染HTML到你的容器中,并处理允许编辑TD,方便采集录入数据
.....
} else {
alert(response.msg, "error");
}
},
error: function (error) {
console.log(error);
}
});
提交数据逻辑,使用Td
标签的location
标记作为name
进行提交,value
为当前Td
的内容。
var cellData = [];
$("tr").each(function (rowIndex, rowElement) {
if (rowIndex != 0) {
$(rowElement).find("td").each(function (cellIndex, cellElement) {
//location标记了表格原始位置 A1
var cellLocation = $j(cellElement).attr("location");
if (cellLocation !== undefined) {
var cellValue = $(cellElement).text();
cellData.push({
name: cellLocation,
value: cellValue
})
}
});
}
});
接收前端提交数据,后端把采集的数据放到绑定位置中,并返回文件。
此处使用了DocumentFormat.OpenXml及MiniWord,用来完成读取Word标记及替换标记内容。
[Authorize]
[HttpPost("/api/xxx/start")]
[ServiceFilter(typeof(RequestAuditFilter))]
public async Task<IActionResult> StartReportGeneration([FromBody] GenerateReportsParam generateParams)
{
try
{
var template = await _context.TemplateTable.Find(generateParams.TemplateId);
if (template == null || string.IsNullOrEmpty(template.OutputBind))
{
var templateErrorMsg = template == null ? "未找到该模板." : "检测到输出模板未进行绑定操作.";
return Json(SetResult(500, templateErrorMsg, "error"));
}
var keyValues = JsonConvert.DeserializeObject<List<KeyValue>>(template.OutputBind ?? "");
var renderItems = generateParams.Data.Select(item => new RenderItems { Key = item.Name, Value = item.Value }).ToList();
var renderDictionary = keyValues.ToDictionary(kv => kv.Key, kv => (object)(renderItems.FirstOrDefault(ri => ri.Key == kv.Value)?.Value ?? ""));
var templateDirectory = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", template.OutputFilepath);
var datePath = DateTime.Now.ToString("yyyy-MM-dd");
var newFile = Path.GetFileNameWithoutExtension(Path.GetRandomFileName()) + ".docx";
var reportDirectory = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", datePath, UserId, "report");
var reportPath = Path.Combine(reportDirectory, newFile);
Directory.CreateDirectory(reportDirectory);
MiniWord.SaveAsByTemplate(reportPath, templateDirectory, renderDictionary);
var memoryStream = new MemoryStream();
using (var fileStream = new FileStream(reportPath, FileMode.Open))
{
fileStream.CopyTo(memoryStream);
}
memoryStream.Position = 0;
var reportFileName = (string.IsNullOrEmpty(template.Name) ? "" : template.Name) + DateTime.Now.ToString("yyyyMMddHHmmss") + ".docx";
var reportRelativePath = $"uploads/{datePath}/{UserId}/report/{newFile}";
_context.Add(new TemplateRecordTable
{
Id = Guid.NewGuid().ToString(),
Userid = Convert.ToInt32(UserId),
Tmeid = template.Id,
Name = reportFileName,
Data = JsonConvert.SerializeObject(generateParams.Data),
Inputpath = template.InputFilepath,
Outputpath = template.OutputFilepath,
OutputType = 0,
Outputfile = reportRelativePath,
Addtime = DateTime.Now
});
await _context.SaveChangesAsync();
return File(memoryStream, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", reportFileName);
}
catch (Exception ex)
{
return Json(SetResult(500, ex.Message, "error"));
}
}
开始发送请求,等待后端返回文件。
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/xxx/start', true);
xhr.responseType = 'blob';
xhr.setRequestHeader('Authorization', 'Bearer ' + localStorage.getItem('token'));
xhr.setRequestHeader('Content-type', 'application/json');
xhr.onload = function (e) {
if (this.status == 200) {
var blob = this.response;
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
//处理文件名称
const now = new Date();
const date = now.getDate().toString();
const time = now.toTimeString().substring(0, 5);
var contentDisposition = xhr.getResponseHeader('Content-Disposition');
var filename = '默认文件名' + date + time + '.docx'; // 这是默认文件名
if (contentDisposition) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(contentDisposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
}
}
link.download = filename;
link.click();
} else if (this.status == 401) {
//登录失效...
} else if (this.status == 403) {
//请求受限,今日机会已用尽.
}
else if (this.status == 500) {
//生成失效...
}
};
xhr.send(JSON.stringify(json));