基於ABP做一個簡單的系統——實戰篇:4.基於富文字編輯器,Razor模板引擎生成內容並匯出Word 填坑記錄

rockcode777發表於2020-08-19

起因

需求是這樣的,有一種協議需要生成,協議的模板是可配置的,在生成過程中,模板中的內容可以根據約定的標記進行替換(就像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就能匯出想要的效果了  

至此一個可自定義內容的模板生成功能就做好了。

相關文章