目录

背景

思考过程

word转markdown

用node的express框架搭建服务器

展示md文件到页面上

提取目录

引入element树

纯前端展示

完整目录结构


背景

当前所做项目(vue-cli+element)有一个需求,做一个帮助文档,把word文档在页面中展示出来,要求能目录跳转。

思考过程

拿到这个需求,感觉这个难点在于目录跳转,word就算读取也全是字,无法识别哪里是目录,然后去提取目录和做定位。年前做项目时,因为项目中公共组件比较多,就想写个使用说明,专门去学过markdown的语法,在markdown中,1个到6个#号分别对应html标签中h1-h6,如果能把word转成markdown,不就可以根据#提取出一个目录了吗?所以要做的第一部就是把word转成markdown。

word转markdown

word转markdown的首先想的是找找有没在线工具,不用下载直接用还是比较舒服的。在网上倒是找到了一个,但是说实话,效果不太好。之前玩游戏要自己做一个筛选页面,因为excel看着实在太费劲了,需要把ecexl表格转JSON数据,然后再处理成组件需要的格式,那会也是找了好多在线的都不好用,最后用的是一个excel自带的插件,名字叫excel to json,还是非常好用的。对了,那个筛选页面我扔到码云上了,已发布到网上,点击这里查看,上面那个筛选组件(我叫选项卡)是我手写的,有需要的可以联系我。所以最后考虑word有没自带的插件能把它转成一个md文件,最后还真找到了一个插件 Writage 转md文件效果特别好,会把word文档里的图片提取到一个文件夹里,打开md文件,所有图片均能正常显示,15000+字的文档只有一处错误,手动改了。Writage 安装好之后,最上面就能看到。

安装好之后点文件,另存为,选择存放位置,选择格式,这里格式一定要选md。

然后你就可以在之前选择的位置找到它的md格式版,同时还会有一个目录,用来存放文档中所有涉及到的图片,至于help.js是干什么的后面会解释。

到这里有两种选择,一种是md文件和media文件夹都放在服务器上,通过请求后端接口来展示,另一种是都放在前端,纯前端展示。这里先说第一种,都放在服务器上,通过接口请求md内容,然后再做展示。

用node的express框架搭建服务器

这里的代码啥意思就不说了,直接上代码

// express-static得用npm下载 npm i express-static --save
const express = require("express");
const expressStatic = require("express-static");
const fs = require('fs');var server = express();
server.listen(5566);
console.log('服务器启动成功地址是: localhost:5566');// 配置请求头
server.all('*', function (req, res, next) {res.header("Access-Control-Allow-Origin", "*")res.header("Access-Control-Allow-Headers", "X-Requested-With")res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS")res.header("X-Powered-By", ' 3.2.1')res.header("Content-Type", "application/json;charset=utf-8")next()
});// 读取help.md里的内容并返回给前端
server.use("/getHelp", function (req, res) {// 同步读取文件let helpData = fs.readFileSync("./document/help.md");res.send(helpData);
});// 静态文件目录
server.use(expressStatic("./document"));

终端启动服务器,后面是目录结构

后端服务写好了,现在让我们来写前端代码,因为是要做demo,所以直接在app.vue里写,这里用的是axios(npm i axios -S)进行的请求。

mehtods里

    getHelp() {this.$axios.get("http://localhost:5566/getHelp").then((res) => {console.log(res.data);}).catch((err) => {console.log(err);});},

created里调用这个方法,我们就拿到了help点md里的内容。接下来考虑怎么把md格式的内容展示到页面上。

展示md文件到页面上

把md文件展示到页面上我们需要借助一个插件 marked.js 这个插件可以把md格式转换成html,我们就可以直接通过插值表达式或者v-html去渲染到页面上了,用法也很简单

npm i marked --save 然后再引入使用就可以了,我这里是在main里引入,然后绑定到全局了

import marked from "marked";
Vue.prototype.$marked = marked;

对getHelp方法进行改造

  getHelp() {this.$axios.get("http://localhost:5566/getHelp").then((res) => {// 原始md文件内容this.helpData = res.data;// 转换后的htmlthis.help = this.$marked(res.data);   }).catch((err) => {console.log(err);});},

然后我们之间在页面上去渲染help属性就好了。这里有个坑是图片的,因为用Writage转md后,所有图片的引入链接全是 media/943f402e73aecc2c08ffaeb88de41720.png ,需要手动全局替换目录地址,可以另外打开一个页面能成功看到图片,然后再做全局修改,这里我是这么做的。

接下来就是提取目录,然后目录跟页面内容一一对应起来。说实话,刚开始到这确实没有思路,直到看到了md转换成html后的内容,发现会自动给每个h标签添加一个id,其id值为标题内容,但是稍微有点坑,坑后面再说。这样的话,我们可以提取出来一个目录结构,然后通过锚点(创建一个a标签,href值为#标题名,点击这个a标签)跳转到对应的部分。这个目录我用的是element的树结构,所以先从原始md文件内容提取出来一个符合其树结构的数据格式的目录吧。

提取目录

我这里卡的比较久,可能是因为js基础不够扎实。因为md中,1个#到6个#是一一对应的,所以并且是按顺序的,所以我们一定可以通过代码来生成我们需要的目录。这里直接放代码(注释全写代码里了),你们如果有其他更好的方式可以分享出来,让我学习学习。

   // 获取目录树getTitle() {// 提取出所有#和后面标题,放到一个数组中let titleList = this.helpData.match(/#+.+/g);// 把每个标题出现次数统计出来,必须保证标题唯一,全部使用1.1.1.1这是一个好办法// 因为marked会把#后面的内容添加为id,如果标题重复,锚点跳转会只跳第一个let count = titleList.reduce((obj, name) => {if (name in obj) {obj[name]++;} else {obj[name] = 1;}return obj;}, {});// 删除只出现了一次的for (let item in count) {if (count[item] == 1) {delete count[item];}}// console.log(count);// 能保证标题唯一的话,可以不用加这行代码titleList = titleList.map((item, index) => (item = index + "$" + item));// console.log(titleList);// 最后一个各级别的标题let nowLabel = [];// 生成的标题let title = [];// 当前对象let cur;for (let item of titleList) {// 当前目录级别let level = item.match(/#+/)[0].length;// 其父级目录文字let label = nowLabel[level - 2];// 更新当前目录文字nowLabel[level - 1] = item;// console.log(item, nowLabel, "===============");if (level == 1) {cur = {level: level,label: item,value: item,children: [],};title.push(cur);} else {let obj = this.getObj(label, cur, level);// console.log(obj);// 把当前目录添加到其父级目录对象的children里obj.children.push({level: level,label: item,value: item,children: [],});}}// 这里是去除标题里的#和空格(如果上面没添加$和索引的话)this.titleData = this.removeTitleData(title);},// 该方法根据传入参数,返回其父级目录对象// 当前目录父级目录,当前一级目录对象,当前目录级别getObj(label, cur, level) {if (level - 1 > cur.level) {for (let item of cur.children) {let res = this.getObj(label, item, level);if (res) {return res;} else {continue;}}} else {if (cur.label == label) {return cur;} else {return false;}}},// 去除目录中无用的部分removeTitleData(title) {return title.map((item) => {// 这里因为标题里添加$和索引了,如果没有添加,可以不需要这行item.label = item.label.split("$")[1];item.label = item.label.replace(/#+\s+/, "");if (item.children.length > 0) {item.children = this.removeTitleData(item.children);}return item;});},

gethelp方法里调用方法获取目录

  getHelp() {this.$axios.get("http://localhost:5566/getHelp").then((res) => {// 原始md文件内容this.helpData = res.data;// 转换后的htmlthis.help = this.$marked(res.data);  // 获取目录this.getTitle(); }).catch((err) => {console.log(err);});},

data里是这样的

 data() {return {// md原始数据helpData: "",// md转成html标签后的数据help: "",// 目录数据titleData: [],// 目录树配置defaultProps: {children: "children",label: "label",},// 是否使用本地数据isLocalFile: true,};},

接下来引用element树,做渲染

引入element树

<!-- accordion是用手风琴效果了 -->
<el-treeclass="titleTree":data="titleData"accordion:props="defaultProps"@node-click="toTitle"></el-tree>

树节点点击toTitle方法

    // 目录跳转的方法toTitle(data) {// console.log(data);// 新建一个a标签用来做跳转let a = document.createElement("a");// 这里就是marked自动添加id的坑 1.2.1.2会变成 1212 ,把点会去掉// 第二个坑是会把空格转成 "-"console.log(data.label.replace(" ", "-").replace(/[.]{1}/g, ""));// 设置锚点,这里直接用label是因为我重新手动设置id为内容了a.href = "#" + decodeURI(data.label);// 触发a的点击事件进行跳转a.click();},

重新手动设置id的方法

    // 手动修改所有h标签的idresetId() {for (let i = 1; i < 7; i++) {let h = document.getElementsByTagName("h" + i);for (let j = 0; j < h.length; j++) {h[j].setAttribute("id", h[j].innerHTML);}// console.log(h);}},

getHelp方法里调用

  getHelp() {this.$axios.get("http://localhost:5566/getHelp").then((res) => {this.helpData = res.data;this.help = this.$marked(res.data);this.getTitle();// 必须在页面渲染完后再执行这个方法,不然会获取不到this.$nextTick(() => {this.resetId();});}).catch((err) => {console.log(err);});},

还有其他可以优化的地方,比如:树结构标题超长可以换行或者省略号展示,加载一次后把目录数据存在本地等,这里就不赘述了。接下来我们来讨论另一个,把md文档和对应图片放在本地,纯前端做展示。

纯前端展示

纯前端展示其实跟接口返回是一样的,这里我们得把md数据放在一个js文件中导出,然后在app中引入,这里图片地址同样得改,需要注意的是,media文件夹必须放在static里才能正常展示,其他地方没试。

<!-- 这里是 help.js 里的内容 -->
// 修改完图片地址后,直接把md文件的内容全粘到反引号里就可以了
let data = `  `export default data;

app.vue 完整代码如下,我是把使用本地资源还是接口数据写了一个变量做切换,电脑还是app显示也做了一个变量(就是把目录给隐藏了),app目录得用其他方式进行展示。目录文字超长这里我做了两种处理,折行显示或超出部分显示省略号。

<template><div id="app"><el-treev-if="!isApp"class="titleTree":data="titleData"accordion:props="defaultProps"@node-click="toTitle"></el-tree><div :class="[{ content: !isApp }]" v-html="help"></div></div>
</template><script>
import help from "@/document/help.js";
export default {name: "App",created() {if (this.isLocalFile) {this.helpData = help;this.help = this.$marked(help);this.getTitle();this.$nextTick(() => {this.resetId();});return;}this.getHelp();},data() {return {// md原始数据helpData: "",// md转成html标签后的数据help: "",// 目录数据titleData: [],// 目录树配置defaultProps: {children: "children",label: "label",},// 是否使用本地数据isLocalFile: false,// App显示还是电脑isApp: false,};},methods: {getHelp() {this.$axios.get("http://localhost:5566/getHelp").then((res) => {this.helpData = res.data;this.help = this.$marked(res.data);this.getTitle();// 必须在页面渲染完后再执行这个方法this.$nextTick(() => {this.resetId();});}).catch((err) => {console.log(err);});},// 获取目录树getTitle() {// 提取出所有#和后面标题,放到一个数组中let titleList = this.helpData.match(/#+.+/g);// 把每个标题出现次数统计出来,必须保证标题唯一,全部使用1.1.1.1这是一个好办法// 因为marked会把#后面的内容添加为id,如果标题重复,锚点跳转会只跳第一个let count = titleList.reduce((obj, name) => {if (name in obj) {obj[name]++;} else {obj[name] = 1;}return obj;}, {});// 删除只出现了一次的for (let item in count) {if (count[item] == 1) {delete count[item];}}// console.log(count);// 能保证标题唯一的话,可以不用加这行代码titleList = titleList.map((item, index) => (item = index + "$" + item));// console.log(titleList);// 最后一个各级别的标题let nowLabel = [];// 生成的标题let title = [];// 当前对象let cur;for (let item of titleList) {// 当前目录级别let level = item.match(/#+/)[0].length;// 其父级目录文字let label = nowLabel[level - 2];// 更新当前目录文字nowLabel[level - 1] = item;if (level == 1) {cur = {level: level,label: item,value: item,children: [],};title.push(cur);} else {let obj = this.getObj(label, cur, level);// 把当前目录添加到其父级目录对象的children里obj.children.push({level: level,label: item,value: item,children: [],});}}// 这里是去除标题里的#和空格(如果上面没添加$和索引的话)this.titleData = this.removeTitleData(title);},// 该方法根据传入参数,返回其父级目录对象// 当前目录父级目录,当前一级目录对象,当前目录级别getObj(label, cur, level) {if (level - 1 > cur.level) {for (let item of cur.children) {let res = this.getObj(label, item, level);if (res) {return res;} else {continue;}}} else {if (cur.label == label) {return cur;} else {return false;}}},// 去除目录中无用的部分removeTitleData(title) {return title.map((item) => {// 这里因为标题里添加$和索引了,如果没有添加,可以不需要这行item.label = item.label.split("$")[1];item.label = item.label.replace(/#+\s+/, "");if (item.children.length > 0) {item.children = this.removeTitleData(item.children);}return item;});},// 手动修改所有h标签的idresetId() {for (let i = 1; i < 7; i++) {let h = document.getElementsByTagName("h" + i);for (let j = 0; j < h.length; j++) {h[j].setAttribute("id", h[j].innerHTML);}// console.log(h);}},// 目录跳转的方法toTitle(data) {// console.log(data);// 新建一个a标签用来做跳转let a = document.createElement("a");// 这里就是marked自动添加id的坑 1.2.1.2会变成 1212 ,把点会去掉// 第二个坑是会把空格转成 "-"// console.log(data.label.replace(" ", "-").replace(/[.]{1}/g, ""));// 设置锚点,这里直接用label是因为我重新手动设置id为内容了a.href = "#" + decodeURI(data.label);// 触发a的点击事件进行跳转a.click();},},components: {VueMarkdown,},
};
</script><style>
#app {font-family: "Avenir", Helvetica, Arial, sans-serif;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;color: #2c3e50;margin-top: 60px;display: flex;
}
.titleTree {width: 300px;min-height: 100vh;background-color: #fafafa;position: fixed !important;left: 0;top: 0;border-right: 1px solid #e8e8e8;
}
.el-tree-node {white-space: normal;padding: 1px 0;padding-right: 10px;/* overflow: hidden;text-overflow: ellipsis;white-space: nowrap;padding-right: 10px; */
}
.el-tree-node__content {height: 100%;align-items: start;
}
.content {width: calc(100% - 310px);margin-left: 310px;background-color: #fff;
}
img {max-width: 100%;
}
</style>

最终效果如下:

完整目录结构

别问我为啥还有个2,因为本身使用的文档保密,所以废了好大劲重新找了一个。如果本文章帮助到你了,记得回来加个关注哦!博主前端菜鸟一个,目前只会vue,如果有实现不了的需求,可以在下面进行评论,我有时间了会尝试去实现。

vue+element实现word文档(转成markdown了)带目录预览相关推荐

  1. word文档转pdf并在任意浏览器预览打印一体化方案

    近日,遇到一个需求,要将 word 文档转化为 pdf 文档,并且能在 IE 浏览器.火狐浏览器.谷歌浏览器等主流浏览器上展示 pdf 内容. 分析:目前在线预览 word 文档用的是卓正的 page ...

  2. 如何把word文档转换成markdown格式

    比如我有一个word文档,我想把它的内容复制粘贴到社交媒体上去,但是全选之后,只能把纯文本复制粘贴到社交媒体网站上,所有word文件里的格式和图片都无法复制过来. 解决方案:使用软件Typora -& ...

  3. 附件预览(可用于图片、word文档、pdf、.xls表格等预览)

    一.fileView.vue文件里的完整代码(fileView.vue即为抽离出的附件预览组件) 1.html部分 <template><div class="docCon ...

  4. Java 将word文档转成html内容,输出到富文本

    使用Java 将word文档转成html内容,输出到富文本 上传word文档 解析到富文本 将word解析成html工具类 import com.common.utils.DateUtil; impo ...

  5. 如何让多个word文档合并成一个

    几千年没发过动态了,今天随便发发证明本人健在吧! 近两天有几个同学问我Word文档合并成一个,我属实有些懵. 首先建立一个空白文档,在[插入]里面找到[对象],然后选择[从文件中提取],之后按照顺序选 ...

  6. Word 文档转变成网页并显示

    开发工具与关键技术:VS/MVC 作者:何桂朋 撰写时间:2019年4月20日 我认为在ASP.NET MVC中,要想将Word 文档转变成网页并显示到页面上,需要以下五步: 1.确定选中的是不是Wo ...

  7. Word电子扫描仪 word文档转换为图片Pdf,Word文档扫描成Pdf,word文档加密 word转图片 word转pdf

    Word文档转换为图片Pdf,Word文档扫描成Pdf Word转换为图片Pdf 1.        Word转换为Pdf,程序很多,但转换后的Pdf,还可以复制,虽可以加密禁止复制,但市场上太多的P ...

  8. 把word文档转换成swf格式

    把word文档转换成swf格式 以前介绍过如何把PPT转换成swf格式,见 方法一 方法二:,也介绍过如何把word转换成pdf格式:点击查看,今天介绍如何把word文档转换成swf文件. 使用swf ...

  9. java将WORD文档转换成pdf文件

    总结对jacob和Itext学习总结.本文试验的是将WORD转换成PDF文件. 实现思路 一.先将WORD文档转换成HMTL文件格式(参阅我的前一文<JAVA操作WORD文档). 二.用流读取H ...

  10. WORD文档转换成PDF格式

    由于一个客户的项目中需要将WORD文档转换成PDF格式,实战教程如下: 需求分析:客户的项目以B/S结构为主,提供一个WORD文件在后台自动转换成PDF,经过实际 测试, 如果该篇WORD文档有100 ...

最新文章

  1. 软件测试在哪个城市好找工作,职业测试:你适合在哪个城市工作?
  2. C语言字符串操作函数
  3. 硬件信息统计_读取输出Excel_显示进度
  4. CasperJs 入门介绍
  5. WINCE基于CH7024实现TV OUT (VGA)功能
  6. HBase 2.X版本的元数据修复及一种数据迁移方式
  7. php商品秒杀时间代码,Thinkphp5+Redis实现商品秒杀代码实例讲解
  8. linux内核系列之二_资源
  9. mybatis-plus自定义sql分页
  10. TP-LINK无线网卡免驱版 安装使用问题整理
  11. 度中心性(degree)、接近中心性(closeness)和中介中心性(betweenness)的理解
  12. javaweb表格制作举例
  13. ReactNative第三方组件库
  14. Node.js 基础入门(四) 数据库与身份认证
  15. 求助!微信公众号页面部分手机加载后空白!
  16. 天耀18期 – 05.面向对象-封装【作业】.
  17. Gooxi国产化服务器专题介绍之海光服务器
  18. WIN11如何一键返回桌面
  19. 蓝桥云算法题之棋盘放麦子——Python满分解答
  20. 导购提成怎么算_导购提成应该怎么算?

热门文章

  1. 游戏手柄(JoyStick)编程学习笔记(2)
  2. 一语道破 到底什么是知识产权?
  3. fullpage插件的使用
  4. 2020-12-20
  5. 切线法(牛顿法)、割线法、抛物线法
  6. android swstnw cn,恶意软件分析 URL链接扫描 免费在线病毒分析平台 | 魔盾安全分析...
  7. 从零开始学会做一个简单的APP
  8. matlab画PV曲线程序,Matlab模拟包络线形成:Envolope程序
  9. 汽车HiL测试简单介绍及其优势
  10. 抖音只有几十个播放量的原因是什么?