基于ABP做一个简单的系统——实战篇:4.基于富文本编辑器,Razor模板引擎生成内容并导出Word 填坑记录
起因
需求是这样的,有一种协议需要生成,协议的模板是可配置的,在生成过程中,模板中的内容可以根据约定的标记进行替换(就像mvc的razor模板一样)。生成后的内容还需要导出成word或pdf。
常见的使用场景比如租赁协议生成,邮件内容模板生成等等,不要傻傻的hard-code像‘#name#’这样的标记了。
优势就是可自定义模板,灵活匹配可获取到对象的任何字段,解除开发侧的包袱
开源框架
wangEditor 简单的富文本编辑器,基本功能够用,使用方便
RazorLight.NetCore3 基于Razor模板动态生成内容
html2openxml 一个把html转换为Xml的组件,依赖于DocumentFormat.OpenXml
富文本编辑器
wangEditor已经更新到V3了,功能简洁高效,配置简单,爽的飞起,简单配置后就用起来。
注意一点就是,想要用razor生成,模板的内容要尽量保持干净,不要混入html代码之外的内容。如果直接把word文档粘贴到editor中,界面上看起来是完好的,但其实会混进来很多xml的东西,像这样的成千上万行:
<w:LsdException Locked="false" Priority="99" SemiHidden="false" Name="Colorful Grid Accent 6" ></w:LsdException>
然后导致razor模板运行失败。解决方法如下:
用wangEditor的pastTextHandle,在文本内容被帖进去之前,把影响razor的字符处理掉。 var E = window.wangEditor;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <em id= "__mceDel" > var editor2 = new E( '#demo' ); editor2.customConfig.pasteTextHandle = function (content) { return setEditor(content); } //设置wangeditor格式,去掉word的xml内容 function setEditor(content) { // content 即粘贴过来的内容(html 或 纯文本),可进行自定义处理然后返回 if (content == '' && !content) return '' ; var str = content; str = str.replace(/<xml>[\s\S]*?<\/xml>/ig, '' ); str = str.replace(/<style>[\s\S]*?<\/style>/ig, '' ); str = str.replace(/<\/?[^>]*>/g, '' ); str = str.replace(/[ | ]*\n/g, '\n' ); str = str.replace(/ /ig, '' ); console.log( '****' , content); console.log( '****' , str); return str; }<br> </em> |
实际效果是这样的。里面的@() 就是常见的mvc里的razor语法
模板引擎
之前用过很多次了,只不过之前是.net framework下的RazorEngine,这次找了个.net core下的RazorLight.NetCore3.
原理上很简单,比如我有一个模板 “我今天买了一本书,书名叫《@(Model.BookName)》,花了@(Model.Price)元钱”,然后我又拿到这么个对象
1 2 3 | var order = new Order(); order.BookName= "Lucky Day" ; order.Price = 100; |
我想根据模板生成实际内容就是:“我今天买了一本书,书名叫《Lucky Day》,花了100元钱”,只需要几行代码,就能拿到想要的结果
1 2 3 4 5 6 7 8 9 10 | var engine = new RazorLightEngineBuilder() .UseEmbeddedResourcesProject( typeof (SysConfigAppService)) .UseMemoryCachingProvider() .Build(); template = "我今天买了一本书,书名叫《@(Model.BookName)》,花了@(Model.Price)元钱" ; string result = await engine.CompileRenderStringAsync( "RazorId" , template, order); Logger.Info($ "razor result: {result}" ); return result; |
遇到的坑:如果对象属性值含有中文,会被编码成字符,解决办法是在模板最前面加上“@{DisableEncoding = true; }”就可以了
1 | template = "@{DisableEncoding = true; }" + template; |
Html转Word
最后一个需求是导出文件并下载,html导出成word,必须依赖openxml,搜遍全网找到这个html2openxml
支持.Net Core (netstandard2.1) 以及 .Net Framework 4.8
这里把代码先贴一下,env是用来获取程序根目录的,因为我需要在Linux上跑,这种方式比较稳妥。过程是这样,生成一个随机的文件名,并放在根目录/ExportFile/文件夹下,导出word并写入文件后,返回文件路径。
这里我采用的是服务端生成文件,把地址返回客户端再下载的方式,当然你也可以写文件流到客户端,根据业务需要自行选择。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | public static string ExportToWord( string html, IWebHostEnvironment env) { string file = SnowHelper.Instance.NextId() + ".docx" ; string fileDir = env.WebRootPath + "/ExportFile/" ; string filename = fileDir + file; if (!Directory.Exists(fileDir)) Directory.CreateDirectory(fileDir); if (File.Exists(filename)) File.Delete(filename); using (MemoryStream generatedDocument = new MemoryStream()) { using (WordprocessingDocument package = WordprocessingDocument.Create(generatedDocument, WordprocessingDocumentType.Document)) { MainDocumentPart mainPart = package.MainDocumentPart; if (mainPart == null ) { mainPart = package.AddMainDocumentPart(); new Document( new Body()).Save(mainPart); } HtmlConverter converter = new HtmlConverter(mainPart); converter.ParseHtml(html); mainPart.Document.Save(); } File.WriteAllBytes(filename, generatedDocument.ToArray()); } return "/ExportFile/" + file; //System.Diagnostics.Process.Start(filename); } |
然后在appService层组织一下返回数据,把下载文件名和文件路径返回给前端。这里下载文件名是和实际文件名不一样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | [HttpPost] public async Task<FileOutDto> ExportProtocal(FileInputDto inputDto) { var order = await this .GetAsync( new EntityDto< long >(inputDto.Id)); string path = FileHelper.ExportToWord(inputDto.Content, _environment); var result = new FileOutDto() { FileName = $ "租赁协议-{order.RentUser.Name}-{order.House.RoomNumber}.docx" , FilePath = path }; return result; } |
最后到前端,加一个按钮并绑定事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | function exportFile() { abp.ui.setBusy(_$form); var d = { Content: editor2.txt.html(), Id: $( "#orderId" ).val() }; _orderService.exportProtocal(d).done(function (res) { console.log(res); abp.notify.info(l( 'SuccessfullyExported' )); var url = res.filePath; var link = document.createElement( 'a' ); // 设置导出的文件名 link.download = res.fileName; link.href = url; // 点击获取文件 link.click(); }).always(function () { abp.ui.clearBusy(_$form); }); } |
导出的word文件格式会和html有些许差别,微调下html就能导出想要的效果了
至此一个可自定义内容的模板生成功能就做好了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构