基於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;
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; }
實際效果是這樣的。裡面的@() 就是常見的mvc里的razor語法
模板引擎
之前用過很多次了,只不過之前是.net framework下的RazorEngine,這次找了個.net core下的RazorLight.NetCore3.
原理上很簡單,比如我有一個模板 「我今天買了一本書,書名叫《@(Model.BookName)》,花了@(Model.Price)元錢」,然後我又拿到這麼個對象
var order = new Order(); order.BookName="Lucky Day"; order.Price = 100;
我想根據模板生成實際內容就是:「我今天買了一本書,書名叫《Lucky Day》,花了100元錢」,只需要幾行程式碼,就能拿到想要的結果
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; }」就可以了
template = "@{DisableEncoding = true; }" + template;
Html轉Word
最後一個需求是導出文件並下載,html導出成word,必須依賴openxml,搜遍全網找到這個html2openxml
支援.Net Core (netstandard2.1) 以及 .Net Framework 4.8
這裡把程式碼先貼一下,env是用來獲取程式根目錄的,因為我需要在Linux上跑,這種方式比較穩妥。過程是這樣,生成一個隨機的文件名,並放在根目錄/ExportFile/文件夾下,導出word並寫入文件後,返迴文件路徑。
這裡我採用的是服務端生成文件,把地址返回客戶端再下載的方式,當然你也可以寫文件流到客戶端,根據業務需要自行選擇。
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層組織一下返回數據,把下載文件名和文件路徑返回給前端。這裡下載文件名是和實際文件名不一樣的。
[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; }
最後到前端,加一個按鈕並綁定事件
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就能導出想要的效果了
至此一個可自定義內容的模板生成功能就做好了。