[转载]巧用Razor生成Word文档

之前有个业务需要根据用户的数据生成一张word的报表,

按照我之前的做法,

  1. 首先弄一个word文档,把这个word文档编辑成我所需要的表格,
  2. 然后在每个填写项内部添加word书签,
  3. 后端读取这个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,实现热更新。

原文地址:https://www.cnblogs.com/cyqdeshenluo/p/12123433.html