起因

需求是这样的,有一种协议需要生成,协议的模板是可配置的,在生成过程中,模板中的内容可以根据约定的标记进行替换(就像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就能导出想要的效果了  

至此一个可自定义内容的模板生成功能就做好了。

基于ABP做一个简单的系统——实战篇:4.基于富文本编辑器,Razor模板引擎生成内容并导出Word 填坑记录的更多相关文章

  1. 基于ABP做一个简单的系统——实战篇:1.项目准备

    现阶段需要做一个小项目,体量很小,业务功能比较简单,就想到用最熟悉的.net来做,更何况现在.net core已经跨平台,也可以在linux服务器上部署.所以决定用.net core 3.1+mysq ...

  2. 基于ABP做一个简单的系统——实战篇:2.代码生成器

    上一篇正说着呢,代码生成器就来了. 1.适用于ABP官网的Startup Template V3.x的包含了登录.用户等页面的MPA应用模板2.当前view仅支持文本框生成,远期规划根据字段类型生成不 ...

  3. web开发实战--弹出式富文本编辑器的实现思路和踩过的坑

    前言: 和弟弟合作, 一起整了个智慧屋的小web站点, 里面包含了很多经典的智力和推理题. 其实该站点从技术层面来分析的话, 也算一个信息发布站点. 因此在该网站的后台运营中, 富文本的编辑器显得尤为 ...

  4. python 全栈开发,Day83(博客系统子评论,后台管理,富文本编辑器kindeditor,bs4模块)

    一.子评论 必须点击回复,才是子评论!否则是根评论点击回复之后,定位到输入框,同时加入@评论者的用户名 定位输入框 focus focus:获取对象焦点触发事件 先做样式.点击回复之后,定位到输入框, ...

  5. 富文本编辑器UEditor自定义工具栏(三、自定义工具栏功能按钮图标及工具栏样式简单修改)

    导读 富文本编辑器UEditor提供丰富了定制配置项,如果想设置个性化的工具栏按钮图标有无办法呢?答案是肯定的!前两篇博文简要介绍了通过将原工具栏隐藏,在自定义的外部按钮上,调用UEditor各命令实 ...

  6. 使用React并做一个简单的to-do-list

    1. 前言 说到React,我从一年之前就开始试着了解并且看了相关的入门教程,而且还买过一本<React:引领未来的用户界面开发框架 >拜读.React的轻量组件化的思想及其virtual ...

  7. 用Qt写软件系列三:一个简单的系统工具(上)

    导言 继上篇<用Qt写软件系列二:QIECookieViewer>之后,有一段时间没有更新博客了.这次要写的是一个简单的系统工具,需求来自一个内部项目.功能其实很简单,就是查看当前当前系统 ...

  8. Jmeter初步使用二--使用jmeter做一个简单的性能测试

    经过上一次的初步使用,我们懂得了Jmeter的安装与初步使用的方法.现在,我们使用Jmeter做一个简单的性能测试.该次测试,提交的参数不做参数化处理,Jmeter各元件使用将在介绍在下一博文开始介绍 ...

  9. 使用Multiplayer Networking做一个简单的多人游戏例子-3/3(Unity3D开发之二十七)

    使用Multiplayer Networking做一个简单的多人游戏例子-1/3 使用Multiplayer Networking做一个简单的多人游戏例子-2/3 使用Multiplayer Netw ...

随机推荐

  1. vue-resource(搬运)

    一.vue-resource特点vue-resource插件具有以下特点: 1. 体积小vue-resource非常小巧,在压缩以后只有大约12KB,服务端启用gzip压缩后只有4.5KB大小,这远比 ...

  2. Python os.fstatvfs() 方法

    概述 os.fstatvfs() 方法用于返回包含文件描述符fd的文件的文件系统的信息,类似 statvfs().高佣联盟 www.cgewang.com Unix上可用. fstatvfs 方法返回 ...

  3. PHP array_map() 函数

    实例 将函数作用到数组中的每个值上,每个值都乘以本身,并返回带有新的值的数组: <?phpfunction myfunction($v){return($v*$v);} $a=array(1,2 ...

  4. PHP key() 函数

    ------------恢复内容开始------------ 实例 从当前内部指针位置返回元素键名: <?php$people=array("Peter","Joe ...

  5. darkbzoj #3759. Hungergame 博弈论 线性基 NIM

    LINK:Hungergame 放上一道简单题 复习一下. 考虑每次可以打开任意多个盒子 如果全打开了 那么就是一个NIM游戏了. 如果发现局面是异或为0的时候此时先手必胜了. 考虑局面不全体异或为0 ...

  6. luogu P1446 [HNOI2008]Cards burnside引理 置换 不动点

    LINK:Cards 不太会burnside引理 而这道题则是一个应用. 首先 一个非常舒服的地方是这道题给出了m个本质不同的置换 然后带上单位置换就是m+1个置换. burnside引理: 其中D( ...

  7. BZOJ3772精神污染&BZOJ3488&luogu3242接水果

    LINK1:精神污染 LINK2:[ONTAK2010Highways](http://www.lydsy.com/JudgeOnline/problem.php?id=3488) LINK3:[接水 ...

  8. Springboot开发web项目

    当前,Spring毫无疑问已经成为java后台对象管理标准框架,除了通过IOC能够管理我们的自定义对象的生命周期之外还提供了众多功能繁复的可配置功能模块.但同时带来了复杂的配置项,这对初学者而言简直是 ...

  9. MyBatis-Plus使用(3)-条件构造器

    说明: 以下出现的第一个入参boolean condition表示该条件是否加入最后生成的sql中 以下代码块内的多个方法均为从上往下补全个别boolean类型的入参,默认为true 以下出现的泛型P ...

  10. org.hibernate.AssertionFailure: null id 错误

    对象属性有Blob类型: 而Blob需在输入流中读取: InputStream in = new FileInputStream(url.getFile()); Blob bookPic = lobH ...