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

前言

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

可能适用的场景

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

实现原理

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

使用流程图

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

体验网址

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

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

实现效果

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

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

制作输入模板

  1. 准备一个Excel静态表格作为数据源,这里以一个授权委托书.xlsx为例,内容像这样
  1. 进入模板制作\输入模板\新增模板,输入模板名称并上传此授权委托书.xlsx,提交保存。

制作输出模板

  1. 准备一个Word文件作为输出,这里以一个授权委托书.docx为例,内容像这样
  1. 当然这不是最终形态,我们需要在输出模板中加上标记,方便系统找到位置,系统采用双英文括号作为标记,像这样:{{标记}},做完标记的模板长这样
  1. 进入模板制作\输出模板\找到刚上传的授权委托书,点击上传,提交保存。
  1. 点击绑定,绑定相对应的位置,可点击静态按钮唤醒输入模板直接选择。应用完成绑定。

数据填报并生成报告

  1. 在线填报\选择使用模板,选择授权委托书-【单次采集】,在绑定的对应位置添加数据
  1. 点击开始生成按钮等待成功,成功会自动下载。

报告展示

关键源码(C#部分)

使用Excel文件获取Html代码,再使用前端进行渲染。

csharp 复制代码
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 onclick=\"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 组件部分

js 复制代码
 $.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的内容。

js 复制代码
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标记及替换标记内容。

csharp 复制代码
[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"));
    }
}

开始发送请求,等待后端返回文件。

js 复制代码
   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));
相关推荐
ai小鬼头5 小时前
Ollama+OpenWeb最新版0.42+0.3.35一键安装教程,轻松搞定AI模型部署
后端·架构·github
萧曵 丶5 小时前
Rust 所有权系统:深入浅出指南
开发语言·后端·rust
老任与码6 小时前
Spring AI Alibaba(1)——基本使用
java·人工智能·后端·springaialibaba
华子w9089258596 小时前
基于 SpringBoot+VueJS 的农产品研究报告管理系统设计与实现
vue.js·spring boot·后端
星辰离彬7 小时前
Java 与 MySQL 性能优化:Java应用中MySQL慢SQL诊断与优化实战
java·后端·sql·mysql·性能优化
GetcharZp8 小时前
彻底告别数据焦虑!这款开源神器 RustDesk,让你自建一个比向日葵、ToDesk 更安全的远程桌面
后端·rust
jack_yin9 小时前
Telegram DeepSeek Bot 管理平台 发布啦!
后端
小码编匠9 小时前
C# 上位机开发怎么学?给自动化工程师的建议
后端·c#·.net
库森学长9 小时前
面试官:发生OOM后,JVM还能运行吗?
jvm·后端·面试
转转技术团队9 小时前
二奢仓店的静默打印代理实现
java·后端