最近开发了一个在软件内部预览word文档的功能,功能是根据后端返回服务器资源的下载地址直接进行前端预览,由于浏览器不支持.docx格式的文件直接打开,所以需要借助第三方的方法和插件进行开发,话不多说开始整活。
🎇 mammonthjs官网:将word转换成html 方案一实现
✨ @vue-office/docx官网:将word转换成html 方案二实现
效果演示:
前言
今天分享的两种方法其实原理都是将word文档的内容转换成html网页的形式进行预览展示,使用插件的好处是快速实现大致效果,缺点是只能保留最基本的标签样式,复杂的样式会丢失导致和wps或者office这种专业软件打开的效果不一致、不美观。解决方法,可以使用样式穿透对其内部标签样式进行优化,基本实现美观展示。
一、mammonthjs是什么?
Mammoth.js 是一个开源的 JavaScript 库,旨在将 Microsoft Word 创建的 .docx
文件转换为 HTML 格式。这个库的设计目标是生成简单和干净的 HTML,主要通过使用文档中的语义信息来实现,而不是尝试精确复制文档的样式(如字体、颜色、大小等)。Mammoth.js 支持多种文档样式的转换,并允许用户自定义样式映射,以便将 Word 文档中的样式更准确地转换为 HTML 元素。
二、使用步骤
1.引入库
代码如下(示例):
npm install --save mammoth
2.使用示例(部分代码)
代码如下(示例):
<template>
<div class="statistical-report">
<el-form :inline="true" :model="formInline" border class="demo-form-inline">
<el-form-item label="时间:">
<el-radio-group
v-model="formInline.rangeTime"
size="small"
@change="handleSelfTime"
>
<el-radio-button :label="1">近一月</el-radio-button>
<el-radio-button :label="2">近三月</el-radio-button>
<el-radio-button :label="3">近半年</el-radio-button>
<el-radio-button :label="4">自定义</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-date-picker
:disabled="selfFlag"
v-model="formInline.time"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
@change="handleTime"
/>
</el-form-item>
<el-form-item label="告警类型:">
<el-radio-group v-model="formInline.type">
<el-radio label="week">周报</el-radio>
<el-radio label="month">月报</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleSearch">
查询
</el-button>
<el-button @click="handleRest" icon="el-icon-refresh">重置</el-button>
</el-form-item>
</el-form>
<div class="body-all">
<div class="table-body" v-show="folded">
<el-table
ref="multipleTable"
class="table-self"
:data="tableData"
highlight-current-row
@selection-change="handleSelectionChange"
@row-click="handleRowClick"
>
<el-table-column align="center" width="60" :resizable="false">
<template slot-scope="{ row }">
<el-radio v-model="multipleSelection.id" :label="row.id">
{{ "" }}
</el-radio>
</template>
</el-table-column>
<el-table-column label="序号" width="50px" type="index" />
<el-table-column
prop="type"
label="监管工作报告类型"
align="center"
width="150"
>
<template slot-scope="scope">
<span v-if="scope.row.type === 'week'">周报</span>
<span v-else>月报</span>
</template>
</el-table-column>
<el-table-column
prop="reportname"
label="监管工作报告名称"
align="center"
width="150"
>
</el-table-column>
<el-table-column prop="status" label="处置状态" align="center">
已生成
</el-table-column>
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<el-button
type="text"
size="small"
@click="handleDownload(scope.row)"
>下载</el-button
>
</template>
</el-table-column>
</el-table>
<div class="fanYe">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="formInline.page"
:page-sizes="[10, 15, 20, 50, 100]"
:page-size="formInline.limit"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
>
</el-pagination>
</div>
</div>
<div style="cursor: pointer" class="fr" v-show="!folded" @click="fold">
<i class="el-icon-s-fold"></i>
</div>
<div style="cursor: pointer" class="fr" v-show="folded" @click="unfold">
<i class="el-icon-s-unfold"></i>
</div>
<div class="rightContent">
<div id="wordView" v-html="wordText" />
</div>
</div>
</div>
</template>
<script>
// docx文档预览(只能转换.docx文档,转换过程中复杂样式被忽,居中、首行缩进等)
import mammoth from "mammoth";
import kingDownload from "@/utils/KingDownload.js"; // 通用下载方法
import { selectByList } from "@/api/video/statisticalReportApi.js"; // 列表接口
export default {
name: "StatisticalReport",
data() {
return {
// 接口的数据
queryStartTime: "",
queryEndTime: "",
// 搜索表单
formInline: {
rangeTime: null, //四选一
time: null, // 时间段(数组)
type: null,
pageNum: 1,
pageSize: 10,
},
total: 20,
// table列表
tableData: [],
selfFlag: true,
// ---------------
folded: true, // table是否显示
wordUrl: "", //预览pdf的地址
wordText: "",
multipleSelection: [],
wordShow: "", //是否展示pdf
};
},
computed: {
// 当天时间yyyy-mm-dd
endTime: {
get() {
return (
new Date().getFullYear() +
"-" +
("0" + (new Date().getMonth() + 1)).slice(-2) +
"-" +
("0" + new Date().getDate()).slice(-2)
);
},
},
firstMonth: {
get() {
let now = new Date(); // 假设现在是 此时此刻的当天
let targetDate = new Date(now);
targetDate.setDate(now.getDate() - 30); // 往回推30天
// 格式化日期
let formattedStartDate = `${targetDate.getFullYear()}-${(
"0" +
(targetDate.getMonth() + 1)
).slice(-2)}-${("0" + targetDate.getDate()).slice(-2)}`;
return formattedStartDate;
},
},
secondMonth: {
get() {
let now = new Date(); // 假设现在是 此时此刻的当天
let targetDate = new Date(now);
targetDate.setDate(now.getDate() - 90); // 往回推90天
// 格式化日期
let formattedStartDate = `${targetDate.getFullYear()}-${(
"0" +
(targetDate.getMonth() + 1)
).slice(-2)}-${("0" + targetDate.getDate()).slice(-2)}`;
return formattedStartDate;
},
},
thirdMonth: {
get() {
let now = new Date(); // 假设现在是 此时此刻的当天
let targetDate = new Date(now);
targetDate.setDate(now.getDate() - 180); // 往回推180天
// 格式化日期
let formattedStartDate = `${targetDate.getFullYear()}-${(
"0" +
(targetDate.getMonth() + 1)
).slice(-2)}-${("0" + targetDate.getDate()).slice(-2)}`;
return formattedStartDate;
},
},
},
mounted() {
this.selectByList();
},
methods: {
// 将word转换成html并展示
getWordText() {
const xhr = new XMLHttpRequest();
xhr.open("get", this.wordUrl, true);
xhr.responseType = "arraybuffer";
xhr.onload = () => {
if (xhr.status == 200) {
mammoth
.convertToHtml({ arrayBuffer: new Uint8Array(xhr.response) })
.then((resultObject) => {
this.$nextTick(() => {
this.wordText = resultObject.value;
});
});
}
};
xhr.send();
},
fold() {
this.folded = true;
},
unfold() {
this.folded = false;
},
handleSelectionChange(val) {
this.multipleSelection = val;
},
// 行点击事件
handleRowClick(row, column, event) {
this.multipleSelection = row;
this.wordShow = true;
this.wordUrl= row.downloadpath;
this.getWordText();
},
// 下载
handleDownload(e) {
console.log(e, "下载");
const path = e.downloadpath;
kingDownload(path);
},
// 列表数据接口
async selectByList(e) {
const { code, rows, message, total } = await selectByList(e);
if (code === 200) {
this.tableData = rows;
this.total = total;
// 模拟第一行点击事件,默认选中第一行数据
this.$nextTick(() => {
this.handleRowClick(this.tableData[3]);
this.$refs.multipleTable.setCurrentRow(this.tableData[3]);
});
} else {
this.$message({
message: message,
type: "error",
});
}
},
// 时间切换
handleSelfTime(e) {
if (e === 4) {
this.selfFlag = false;
this.queryStartTime = "";
this.queryEndTime = "";
} else if (e === 1) {
this.queryStartTime = this.firstMonth;
this.queryEndTime = this.endTime;
this.formInline.time = null;
this.selfFlag = true;
} else if (e === 2) {
this.queryStartTime = this.secondMonth;
this.queryEndTime = this.endTime;
this.formInline.time = null;
this.selfFlag = true;
} else if (e === 3) {
this.queryStartTime = this.thirdMonth;
this.queryEndTime = this.endTime;
this.formInline.time = null;
this.selfFlag = true;
} else {
this.queryStartTime = "";
this.queryEndTime = "";
this.formInline.time = null;
this.selfFlag = true;
}
},
// 时间选择
handleTime(e) {
this.queryStartTime = e?.[0];
this.queryEndTime = e?.[1];
},
// 重置
handleRest() {
this.formInline = {
rangeTime: null, //四选一
time: null, // 时间段(数组)
type: null,
pageNum: 1,
pageSize: 10,
};
this.queryStartTime = "";
this.queryEndTime = "";
},
// 按条件检索
handleSearch() {
let query = {
beginTime: this.queryStartTime,
endTime: this.queryEndTime,
type: this.formInline.type,
pageSize: this.formInline.pageSize,
pageNum: this.formInline.pageNum,
};
this.selectByList(query);
},
handleSizeChange(val) {
let query = {
beginTime: this.queryStartTime,
endTime: this.queryEndTime,
type: this.formInline.type,
pageSize: val,
};
this.formInline.pageSize = val;
this.selectByList(query);
},
handleCurrentChange(val) {
let query = {
beginTime: this.queryStartTime,
endTime: this.queryEndTime,
type: this.formInline.type,
pageNum: val,
};
this.formInline.pageNum = val;
this.selectByList(query);
},
},
};
</script>
<style scoped lang="scss">
.statistical-report {
padding: 12px;
margin: 12px;
background-color: #ffffff;
.demo-form-inline {
::v-deep .el-form-item {
margin-right: 18px;
margin-bottom: 0;
}
::v-deep .el-form-item__label {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
color: #606163;
}
::v-deep .el-radio-button {
margin: 0 8px;
}
::v-deep .el-radio-button--small .el-radio-button__inner {
padding: 6px 10px;
font-size: 14px;
border-radius: 0;
border: 1px solid #dcdfe6;
box-sizing: border-box;
border-radius: 3px;
}
::v-deep .el-radio-button--small.is-active .el-radio-button__inner {
border-left: none;
}
::v-deep .el-radio {
margin-right: 20px;
}
}
.body-all {
margin-top: 12px;
border-top: 1px solid #e5e5e5; /* 修改水平线颜色为灰色 */
display: flex;
.table-body {
flex: 1;
margin-right: 20px;
padding-top: 12px;
.table-self {
::v-deep .el-table__header-wrapper th {
background-color: #eff3fc;
}
margin-top: 10px;
}
.fanYe {
padding: 10px;
display: flex;
justify-content: flex-end;
}
}
.fr {
margin-top: 12px;
}
.rightContent {
flex: 2;
padding: 15px 10px;
margin-top: 12px;
height: calc(100vh - 160px);
overflow-y: scroll;
// 在此对插件转换的html样式进行美化
#wordView {
::v-deep p {
text-align: left;
font-size: 16px !important;
img {
// width: 100%;
display: block;
margin: 0 auto;
}
}
::v-deep table {
width: 100%;
border-collapse: collapse; /* 边框合并为一个单一的边框 */
th,
td {
border: 1px solid #ddd; /* 单元格边框 */
padding: 8px; /* 单元格内边距 */
p {
text-align: center; /* 文本左对齐 */
}
}
th {
background-color: #f2f2f2; /* 表头背景色 */
}
/* 如果你想为表格的每一行添加鼠标悬停效果,可以添加以下样式 */
tr:hover {
background-color: #f5f5f5; /* 鼠标悬停时的背景色 */
}
}
}
.noData {
text-align: center;
line-height: 40;
}
}
}
}
</style>
三、mammonthjs是什么?
@vue-office/docx是一个Vue组件库,专门用于在Vue项目中预览和编辑Microsoft Word (.docx)文件。这个库提供了与Word模板交互的JavaScript API,允许开发者生成动态的Word文档。它结合了Vue的组件化思想,使得在前端开发中对Word文档的操作变得更加便捷和高效。
四、使用步骤
1.引入库
代码如下(示例):这个插件非常难装,装了好几次才成功可能是网络问题也可能有各种限制(学会科学上网可能会好点)
#docx文档预览组件 npm install @vue-office/docx vue-demi@0.14.6
如果是vue2.6版本或以下还需要额外安装 @vue/composition-api
npm install @vue/composition-api
2.使用示例(部分代码)
代码如下(示例):
<template>
<div class="statistical-report">
<el-form :inline="true" :model="formInline" border class="demo-form-inline">
<el-form-item label="时间:">
<el-radio-group
v-model="formInline.rangeTime"
size="small"
@change="handleSelfTime"
>
<el-radio-button :label="1">近一月</el-radio-button>
<el-radio-button :label="2">近三月</el-radio-button>
<el-radio-button :label="3">近半年</el-radio-button>
<el-radio-button :label="4">自定义</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-date-picker
:disabled="selfFlag"
v-model="formInline.time"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
@change="handleTime"
/>
</el-form-item>
<el-form-item label="告警类型:">
<el-radio-group v-model="formInline.type">
<el-radio label="week">周报</el-radio>
<el-radio label="month">月报</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleSearch">
查询
</el-button>
<el-button @click="handleRest" icon="el-icon-refresh">重置</el-button>
</el-form-item>
</el-form>
<div class="body-all">
<div class="table-body" v-show="folded">
<el-table
ref="multipleTable"
class="table-self"
:data="tableData"
highlight-current-row
@selection-change="handleSelectionChange"
@row-click="handleRowClick"
>
<el-table-column align="center" width="60" :resizable="false">
<template slot-scope="{ row }">
<el-radio v-model="multipleSelection.id" :label="row.id">
{{ "" }}
</el-radio>
</template>
</el-table-column>
<el-table-column label="序号" width="50px" type="index" />
<el-table-column
prop="type"
label="监管工作报告类型"
align="center"
width="150"
>
<template slot-scope="scope">
<span v-if="scope.row.type === 'week'">周报</span>
<span v-else>月报</span>
</template>
</el-table-column>
<el-table-column
prop="reportname"
label="监管工作报告名称"
align="center"
width="150"
>
</el-table-column>
<el-table-column prop="status" label="处置状态" align="center">
已生成
</el-table-column>
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<el-button
type="text"
size="small"
@click="handleDownload(scope.row)"
>下载</el-button
>
</template>
</el-table-column>
</el-table>
<div class="fanYe">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="formInline.page"
:page-sizes="[10, 15, 20, 50, 100]"
:page-size="formInline.limit"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
>
</el-pagination>
</div>
</div>
<div style="cursor: pointer" class="fr" v-show="!folded" @click="fold">
<i class="el-icon-s-fold"></i>
</div>
<div style="cursor: pointer" class="fr" v-show="folded" @click="unfold">
<i class="el-icon-s-unfold"></i>
</div>
<div class="rightContent">
<vue-office-docx
v-show="isFlag"
:src="wordUrl"
@rendered="rendered($event)"
@error="error($event)"
/>
<el-empty
v-show="!isFlag || total === 0"
description="暂无报告"
></el-empty>
</div>
</div>
</div>
</template>
<script>
//引入VueOfficeDocx组件
import VueOfficeDocx from "@vue-office/docx";
//引入相关样式(此处没啥作用)
// import "@vue-office/docx/lib/index.css";
import kingDownload from "@/utils/KingDownload.js";
import { selectByList } from "@/api/video/statisticalReportApi.js";
export default {
name: "StatisticalReport",
components: {
VueOfficeDocx,
},
data() {
return {
// 接口的数据
queryStartTime: "",
queryEndTime: "",
// 搜索表单
formInline: {
rangeTime: null, //四选一
time: null, // 时间段(数组)
type: null,
pageNum: 1,
pageSize: 10,
},
total: 20,
// table列表
tableData: [],
selfFlag: true,
// ---------------
folded: true, // table是否显示
isFlag: true, // 为了做文档空状态的处理
pdfUrl: "", //预览pdf的地址
wordText: "",
multipleSelection: [],
};
},
computed: {
// 当天时间yyyy-mm-dd
endTime: {
get() {
return (
new Date().getFullYear() +
"-" +
("0" + (new Date().getMonth() + 1)).slice(-2) +
"-" +
("0" + new Date().getDate()).slice(-2)
);
},
},
firstMonth: {
get() {
let now = new Date(); // 假设现在是 此时此刻的当天
let targetDate = new Date(now);
targetDate.setDate(now.getDate() - 30); // 往回推30天
// 格式化日期
let formattedStartDate = `${targetDate.getFullYear()}-${(
"0" +
(targetDate.getMonth() + 1)
).slice(-2)}-${("0" + targetDate.getDate()).slice(-2)}`;
return formattedStartDate;
},
},
secondMonth: {
get() {
let now = new Date(); // 假设现在是 此时此刻的当天
let targetDate = new Date(now);
targetDate.setDate(now.getDate() - 90); // 往回推90天
// 格式化日期
let formattedStartDate = `${targetDate.getFullYear()}-${(
"0" +
(targetDate.getMonth() + 1)
).slice(-2)}-${("0" + targetDate.getDate()).slice(-2)}`;
return formattedStartDate;
},
},
thirdMonth: {
get() {
let now = new Date(); // 假设现在是 此时此刻的当天
let targetDate = new Date(now);
targetDate.setDate(now.getDate() - 180); // 往回推180天
// 格式化日期
let formattedStartDate = `${targetDate.getFullYear()}-${(
"0" +
(targetDate.getMonth() + 1)
).slice(-2)}-${("0" + targetDate.getDate()).slice(-2)}`;
return formattedStartDate;
},
},
},
mounted() {
this.selectByList();
},
methods: {
渲染未完成触发
error(e) {
this.isFlag = false;
},
// 渲染完成触发
rendered(e) {
this.isFlag = true;
},
fold() {
this.folded = true;
},
unfold() {
this.folded = false;
},
handleSelectionChange(val) {
this.multipleSelection = val;
},
// 行点击事件
handleRowClick(row, column, event) {
this.multipleSelection = row;
this.wordUrl = row.downloadpath;
},
// 下载
handleDownload(e) {
const path = e.downloadpath;
kingDownload(path);
},
// 列表数据接口
async selectByList(e) {
const { code, rows, message, total } = await selectByList(e);
if (code === 200) {
this.tableData = rows;
this.total = total;
// 模拟第一行点击事件,默认选中第一行数据
this.$nextTick(() => {
this.handleRowClick(this.tableData[0]);
this.$refs.multipleTable.setCurrentRow(this.tableData[0]);
});
} else {
this.$message({
message: message,
type: "error",
});
}
},
// 时间切换
handleSelfTime(e) {
if (e === 4) {
this.selfFlag = false;
this.queryStartTime = "";
this.queryEndTime = "";
} else if (e === 1) {
this.queryStartTime = this.firstMonth;
this.queryEndTime = this.endTime;
this.formInline.time = null;
this.selfFlag = true;
} else if (e === 2) {
this.queryStartTime = this.secondMonth;
this.queryEndTime = this.endTime;
this.formInline.time = null;
this.selfFlag = true;
} else if (e === 3) {
this.queryStartTime = this.thirdMonth;
this.queryEndTime = this.endTime;
this.formInline.time = null;
this.selfFlag = true;
} else {
this.queryStartTime = "";
this.queryEndTime = "";
this.formInline.time = null;
this.selfFlag = true;
}
},
// 时间选择
handleTime(e) {
this.queryStartTime = e?.[0];
this.queryEndTime = e?.[1];
},
// 重置
handleRest() {
this.formInline = {
rangeTime: null, //四选一
time: null, // 时间段(数组)
type: null,
pageNum: 1,
pageSize: 10,
};
this.queryStartTime = "";
this.queryEndTime = "";
},
// 按条件检索
handleSearch() {
let query = {
beginTime: this.queryStartTime,
endTime: this.queryEndTime,
type: this.formInline.type,
pageSize: this.formInline.pageSize,
pageNum: this.formInline.pageNum,
};
this.selectByList(query);
},
handleSizeChange(val) {
let query = {
beginTime: this.queryStartTime,
endTime: this.queryEndTime,
type: this.formInline.type,
pageSize: val,
};
this.formInline.pageSize = val;
this.selectByList(query);
},
handleCurrentChange(val) {
let query = {
beginTime: this.queryStartTime,
endTime: this.queryEndTime,
type: this.formInline.type,
pageNum: val,
};
this.formInline.pageNum = val;
this.selectByList(query);
},
},
};
</script>
<style scoped lang="scss">
.statistical-report {
padding: 12px;
margin: 12px;
background-color: #ffffff;
.demo-form-inline {
::v-deep .el-form-item {
margin-right: 18px;
margin-bottom: 0;
}
::v-deep .el-form-item__label {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
color: #606163;
}
::v-deep .el-radio-button {
margin: 0 8px;
}
::v-deep .el-radio-button--small .el-radio-button__inner {
padding: 6px 10px;
font-size: 14px;
border-radius: 0;
border: 1px solid #dcdfe6;
box-sizing: border-box;
border-radius: 3px;
}
::v-deep .el-radio-button--small.is-active .el-radio-button__inner {
border-left: none;
}
::v-deep .el-radio {
margin-right: 20px;
}
}
.body-all {
margin-top: 12px;
border-top: 1px solid #e5e5e5; /* 修改水平线颜色为灰色 */
display: flex;
.table-body {
flex: 1;
margin-right: 20px;
padding-top: 12px;
.table-self {
height: calc(100vh - 230px);
overflow-y: scroll;
::v-deep .el-table__header-wrapper th {
background-color: #eff3fc;
}
::v-deep .el-table__empty-text {
line-height: 500px;
font-size: 16px;
}
margin-top: 10px;
}
.fanYe {
padding: 10px;
display: flex;
justify-content: flex-end;
}
}
.fr {
margin-top: 12px;
}
.rightContent {
flex: 2;
padding: 15px 10px;
margin-top: 12px;
height: calc(100vh - 160px);
overflow-y: scroll;
.el-empty {
height: calc(100vh - 220px);
}
// 此处进行样式优化
::v-deep article {
padding: 26px;
div {
width: 100% !important;
img {
display: block;
margin: 0 auto;
}
}
table {
width: 100% !important;
border-collapse: collapse; /* 边框合并为一个单一的边框 */
th,
td {
border: 1px solid #ddd; /* 单元格边框 */
padding: 8px; /* 单元格内边距 */
}
th {
background-color: #f2f2f2; /* 表头背景色 */
}
/* 如果你想为表格的每一行添加鼠标悬停效果,可以添加以下样式 */
tr:hover {
background-color: #f5f5f5; /* 鼠标悬停时的背景色 */
}
}
}
.noData {
text-align: center;
line-height: 40;
}
}
}
}
</style>
五、拓展
vue-office还可以进行excel,pdf(这个比较常用)的预览开发
总结
由于个人能力有限,此次开发时间紧迫只找到了以上两种适合word文档预览的插件,功能不是很完善但是基本的功能已经实现,样式的美化需要更多的投入和开发,好了这次就分享这么多,希望有其他见解的朋友来交流学习。