从零开始搭建报表工具(一)在线填报及报告生成_自己搭建在线表格-程序员宅基地

技术标签: c#  .net  从零开始搭建报表工具  开源  程序人生  云计算  产品经理  

前言

本篇文章是从零开始搭建报表工具系列文章的第一篇,这一篇主要实现一下实际工作环境中真实遇到的问题,在实际工作中经常会存在需要把 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));
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_42904425/article/details/136216705

智能推荐

.xsa文件生成-程序员宅基地

文章浏览阅读6.2k次。.xsa文件的生成Vivado工程建立你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。1.我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:全新的界面设计 ,将会带来全新的写作体验;在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;增加_.xsa

初识HtmlParser.Net-程序员宅基地

文章浏览阅读475次。一、HTMLParser.net是什么?HTMLParser.net是HtmlParser的JAVA版本的dot net版本。二、HTMLParser可以用来做什么?HtmlParser是用来改造或者提取HTML,通过HtmlParser可以高速,快捷的从Html页面中分离出你想要的内容。三、HTMLParser的核心模块是org.htmlparser.Parser类,这个类实际完成了对于HTML_htmlparser.net

模拟神器之QEMU-程序员宅基地

文章浏览阅读1.3w次,点赞6次,收藏48次。1. 简介QEMU(quick emulator)是一款由法布里斯·贝拉(Fabrice Bellard)等人编写的免费的可执行硬件虚拟化的(hardware virtualization)开源托管虚拟机(VMM)。QEMU 是一个托管的虚拟机镜像,它通过动态的二进制转换,模拟CPU,并且提供一组设备模型,使它能够运行多种未修改的客户机OS,可以通过与KVM一起使用进而接近本地速度运行虚拟机(接近真实电脑的速度)。QEMU还可以为user-level的进程执行CPU仿真,进而允许了为一种架构编译的程序_qemu

【最详细|附源码】Visual C++(VC)6.0最新安装教程_visual c++安装教程-程序员宅基地

文章浏览阅读1.4w次,点赞14次,收藏78次。软件:Visual C++版本:6.0语言:简体中文大小:34.26M安装环境:Win11/Win10/Win8/Win7硬件要求:[email protected] 内存@4G(或更高)下载通道①百度网盘丨下载链接:提取码:dg2n[更多软件]:点击进入管家「软件目录」!_visual c++安装教程

新路由3 高恪魔改固件+底包_新路由3高恪5.0nat1-程序员宅基地

文章浏览阅读2.7w次,点赞2次,收藏8次。新路由3 newifi3 d2 高恪魔改固件,请在breed中先刷入底包,然后启动路由器进入底包系统后,再在底包系统里面网页web升级固件,选择魔改进行升级,切记必须这样操作。压缩包包含了底包和固件解压密码 123下载地址:https://u13909188.pipipan.com/fs/13909188-384246318..._新路由3高恪5.0nat1

戳破“砖家”假面:唯快不破的时代,为什么这件事一定要慢慢做?-程序员宅基地

文章浏览阅读298次。导读:我们生活在一个嘈杂、混乱的世界中。生活中,我们有很多“权威”和“专家”,他们标榜自己是内行人,宣称自己掌握着该领域的真理,而我们需要做的只有两个字——接受。但事实上..._唯快不破的人为什么定

随便推点

Dyninst学习笔记-程序员宅基地

文章浏览阅读7.6k次,点赞2次,收藏9次。Instrumentation是一种直接修改程序二进制文件的方法。其可以用于程序的调试,优化,安全等等。对这个词一般的翻译是“插桩”,但这更多使用于软件测试领域。【找一些相关的例子】Dyninst可以动态或静态的修改程序的二进制代码。动态修改是在目标进程运行时插入代码(dynamic binary instrumentation)。静态修改则是直接向二进制文件插入代码(static b_dyninst

在服务器上部署asp网站,部署asp网站到云服务器-程序员宅基地

文章浏览阅读2.9k次。部署asp网站到云服务器 内容精选换一换通常情况下,需要结合客户的实际业务环境和具体需求进行业务改造评估,建议您进行服务咨询。这里仅描述一些通用的策略供您参考,主要分如下几方面进行考虑:业务迁移不管您的业务是否已经上线华为云,业务迁移的策略是一致的。建议您将时延敏感型,有快速批量就近部署需求的业务迁移至IEC;保留数据量大,且需要长期稳定运行的业务在中心云上。迁移方法请参见如何计算隔离独享计算资源..._nas asp网站

android开发之bitmap转数组的方法-程序员宅基地

文章浏览阅读4.7k次。/** 方法一 * 将bitmap转为数组的方法 * * @param bitmap 图片 * @return 返回数组 */ public byte[] getBytesByBitmap(Bitmap bitmap) { ByteBuffer buffer = ByteBuffer.allocate(bitmap.ge..._bitmap转数组

IDEA修改SVN地址-程序员宅基地

文章浏览阅读6.6k次,点赞2次,收藏6次。IDEA修改SVN地址 SVN地址改变了,在IDEA上的项目地址还没有修改 第一步:选中项目,右键Subversion --> Relocate第二步:From URL路径保持不变(修改To URL为最新路径)第三步:选中项目,右键Subversion --> Update Directory第四步:勾选Update修改URL为最新的即可SVN地址改变了,在IDEA上的项目地址还没有修改_idea修改svn地址

欧拉图和哈密顿图_哈密顿通路度为偶数-程序员宅基地

文章浏览阅读3.8k次。欧拉图及欧拉路径欧拉图 如果图G上有一条经过所有顶点、所有边的闭路径(边不重复,顶点可以重复)充分必要条件 无向图:G连通,所有顶点的度都是偶数有向图:G弱连通,每个顶点出度与入度相等欧拉路径 如果图G上有一条经过所有顶点、所有边的路径(边不重复,顶点可以重复)充分必要条件 无向图:G连通,恰有两个顶点的度是奇数有向图:G连通,恰有两个顶点的出度与入度不相等,其中一个出度比入度多_哈密顿通路度为偶数

Linux下SVN安装配置和使用中遇到的问题-程序员宅基地

文章浏览阅读68次。两个命令:svn info :显示版本库信息,svn的下载url等。svn co https://xxxxx/xxx wodemulu (通过我的目录制定co的文件夹)svn st:显示修改的文件。=-=========================================第一章 安装1. 采用源文件编译安装。源文件共两个(可下载完传入linux),为:s..._can't lunch modelsim make sure

推荐文章

热门文章

相关标签