从零开始搭建报表工具(一)在线填报及报告生成

前言

本篇文章是从零开始搭建报表工具系列文章的第一篇,这一篇主要实现一下实际工作环境中真实遇到的问题,在实际工作中经常会存在需要把 Excel 的数据转化为Word文档的操作,利用 Excel中作为数据源为报表提供数据支撑。为此,本章将优先开发数据填报部分。

可能适用的场景

  1. 数据汇总场景,多次填报的数据进行汇总整理,以便后续分析,报表展示。
  2. 文件转换场景,可以在Excel中配置整个文件需要的数据,直接生成合同、委托单、审批表等文件。
  3. 检测报告的制作,设备采集的数据往往都是类Excel的形式,同样可转换成检测报告。
  4. 替代无纸化,无纸化不仅录入效率低,归档数据不容易复用,后期分析需进行数据整理。
  5. 去除每日重复的工作,把工作量向数据源整理偏移,减少反复操作Word样式。
  6. 利用Excel进行公式计算后将结果输出或呈现到Word中的场景,此功能将采用Excel导入方式实现。

实现原理

导入表格
转化成HTML
在线渲染
采集数据
保存到数据库
导入Word
识别标签
标签与Excel录入位置绑定
数据匹配
生成报告

使用流程图

开始
制作输入模板
制作输出模板
数据填报
生成报告
结束

体验网址

体验网址:http://121.41.170.62/login

项目还在一点点完善中,仅供学习参考!可能无法访问。

实现效果

当前Excel数据源仅支持静态表格,如果你的数据源是列表方式,请查看后续文章动态表单部分。

静态表格:不存在任何新增行或插入列的表格,所有的位置都是固定不变的。

制作输入模板

  1. 准备一个Excel静态表格作为数据源,这里以一个授权委托书.xlsx为例,内容像这样
    在这里插入图片描述

  2. 进入模板制作\输入模板\新增模板,输入模板名称并上传此授权委托书.xlsx,提交保存。

制作输出模板

  1. 准备一个Word文件作为输出,这里以一个授权委托书.docx为例,内容像这样

在这里插入图片描述

  1. 当然这不是最终形态,我们需要在输出模板中加上标记,方便系统找到位置,系统采用双英文括号作为标记,像这样:{{标记}},做完标记的模板长这样
    在这里插入图片描述

  2. 进入模板制作\输出模板\找到刚上传的授权委托书,点击上传,提交保存。
    在这里插入图片描述

  3. 点击绑定,绑定相对应的位置,可点击静态按钮唤醒输入模板直接选择。应用完成绑定。
    在这里插入图片描述

数据填报并生成报告

  1. 在线填报\选择使用模板,选择授权委托书-【单次采集】,在绑定的对应位置添加数据
    在这里插入图片描述

  2. 点击开始生成按钮等待成功,成功会自动下载。
    在这里插入图片描述

最终报告展示

在这里插入图片描述

关键源码(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));
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值