之前有个业务需要根据用户的数据生成一张word的报表,
按照我之前的做法,
- 首先弄一个word文档,把这个word文档编辑成我所需要的表格,
- 然后在每个填写项内部添加word书签,
- 后端读取这个word,根据对应的书签进行属性插入
这样也可以解决问题,但是有以下几个不足:
- 需要强大的word编辑能力,能够手撸复杂的word表格
- 书签必须和后端代码严格对应,书签的录入需要大量时间
- 如果需要修改word,需要人为重新校验对应的书签,如果是复杂的word,书签多达到几十个甚至上百个,简直要人命
- 面向书签编程,这不够OOP,代码无法复用。
这种硬编码对于开发不是很友好,所以如果有一种面向对象编程,在编写过程中可以完全控制word的方法,岂不是可以减少很大的开发量?
这里我想到的办法是:
- 前端编写页面能力 > 开发人员的Word编辑能力,所以由前端输出和word样式一样的静态页面
- 静态页面转Word
那么就需要后端针对这个静态页面进行绑定操作,这里微软提供了一种技术,
Razor 是一种允许您向网页中嵌入基于服务器的代码(Visual Basic 和 C#)的标记语法。
只需要将静态页面转换成cshtml页面(支持C#语法的解析和编写),这个很简单;然后通过在后端接口调用Engine.Razor来操作这个cshtml页面,实现数据绑定。
具体代码如下:
DetailProjectDto.cs
using RunGo.ToolsAttribute; using System; using System.ComponentModel.DataAnnotations; namespace RunGo.ProjectsManager { /// <summary> /// 工程详细实体 /// </summary> public class DetailProjectDto { /// <summary> /// 主键 /// </summary> public string Id { get; set; } /// <summary> /// 工程名称 /// </summary> [StringLength(250)] [Required] [Export("工程名称")] public string ProjectName { get; set; } /// <summary> /// 工程编号 /// </summary> [StringLength(250)] [Required] [Export("工程编号")] public string ProjectNo { get; set; } } }
test.cshtml
@using Microsoft.AspNetCore.Html; @using RunGo.ProjectsManager; <h2 style="text-align:center;font-family: STSong;">工程基本信息</h2> <table style="table-layout: fixed;word-break: break-all;border: 1px solid #000000;border-collapse: collapse;"> <tr style="height: 60px;font-size: 12px;"> <td style="border: 1px solid #000000;border-collapse: collapse;font-family: STSong;text-align: center; font-size:14px;font-weight:600;" colspan="2">工程名称</td> <td style="border: 1px solid #000000;border-collapse: collapse;font-family: STSong;text-align: center;"> @Model.ProjectName </td> <td style="border: 1px solid #000000;border-collapse: collapse;font-family: STSong;text-align: center; font-size:14px;font-weight:600;" colspan="2">工程编号</td> <td style="border: 1px solid #000000;border-collapse: collapse;font-family: STSong;text-align: center;"> @Model.ProjectNo </td> </tr> </table>
接口
注:Nuget需要引入RazorEngine.NetCore
using RazorEngine; using RazorEngine.Templating; /// <summary> /// 工程基本信息报告下载 /// </summary> /// <param name="projectId">工程id</param> /// <returns></returns> [HttpGet] public FileResult UploadProjectBaseInfo(string projectId) { string memi = string.Empty; Stream outData = null; outData = GetBaseInfo(projectId, out memi); return File(outData, memi, $"工程基本信息.docx"); } /// <summary> /// 工程基本信息 /// </summary> /// <param name="projectId"></param> /// <param name="memi"></param> /// <returns></returns> [RemoteService(false)] public Stream GetBaseInfo(string projectId, out string memi) { var model = _projectManagerService.Detail(projectId).Result; if (model == null) { throw new Exception(""); } var template = System.IO.File.ReadAllText($"{_hostingEnvironment.WebRootPath}\test.cshtml"); var html = Engine.Razor.RunCompile(template, Guid.NewGuid().ToString(), typeof(DetailProjectDto), model); var op = _SpireDocHelper.SwaggerHtmlConvers(html, ".docx", out memi); if (op == null) throw new Exception("转换失败"); return op; }
相关工具方法:
注:Nuget需要引入 Spire.Doc
public Stream SwaggerHtmlConvers(string html, string type, out string memi) { string fileName = Guid.NewGuid().ToString() + type; string webRootPath = _hostingEnvironment.WebRootPath; string path = webRootPath + @"FilesTempFiles"; var addrUrl = path + $"{fileName}"; FileStream fileStream = null; var provider = new FileExtensionContentTypeProvider(); memi = provider.Mappings[type]; try { if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } var data = System.Text.Encoding.Default.GetBytes(html); var stream = new MemoryStream(data); //创建Document实例 Document document = new Document(); //加载HTML文档 document.LoadFromStream(stream, FileFormat.Html, XHTMLValidationType.None); document.SaveToFile(addrUrl, FileFormat.Docx); document.Close(); fileStream = System.IO.File.Open(addrUrl, FileMode.OpenOrCreate); var filedata = ByteHelper.StreamToBytes(fileStream); var outdata = ByteHelper.BytesToStream(filedata); return outdata; } catch (Exception e) { return null; } finally { if (fileStream != null) fileStream.Close(); if (System.IO.File.Exists(addrUrl)) System.IO.File.Delete(addrUrl);//删掉文件 } } public static byte[] StreamToBytes(Stream stream) { byte[] bytes = new byte[stream.Length]; stream.Read(bytes, 0, bytes.Length); // 设置当前流的位置为流的开始 stream.Seek(0, SeekOrigin.Begin); return bytes; } /// 将 byte[] 转成 Stream public static Stream BytesToStream(byte[] bytes) { Stream stream = new MemoryStream(bytes); return stream; }
总结:
这样可以有效利用前端开发编写页面的速度远大于操作word书签的速度,使得后端开发只需针对完成后的页面进行实体绑定即可生成Word,大大提高了开发效率,接口成型之后,只需要提供cshtml文件以及实体即可实现绑定;
甚至可以将对应的cshtml路径以及对应的实体写入配置文件,通过反射来控制接口生成的Word,实现热更新。