一.背景
当前工作中,有不少项目有关于文档校验审核的功能,这个功能的需求是通过视觉扫描文档,检测出文档中有问题的部分,然后在右侧展示出现问题,点击右侧的问题区域,
会定位到文档中对应问题的地方,然后在出现问题的地方做个标记,这样用户就能很快发现文档中的问题,并迅速修改。
二.分析
文档展示区域位于页面的中间部分,由于文档的篇幅可能比较大整体高度肯定是超过当前的浏览器可是界面的高度,这时我们就需要将盛放文档的元素容器固定高度,他的高度为整个页面减去上面区域的高度,通过对容器设置overflow:auto,让这个文档内容多余的部分隐藏,通过下来滚动条来展示隐藏的部分。
右侧是展示文档中出现问题的列表,当我们用鼠标点击右侧的某个问题时,中间部分的paf会将出现问题的滚动到文档区域可视区中间位置,这个交互定位动作需要获取出现问题坐标返回给前端。前端拿到这个坐标后通过scrollTop这个方法,将出现问题的区域滚动到容器的可视区范围。
滚动到可视区后,需要根据后端返回的坐标在对应的位置上做标记,通过坐标开发出一个矩形框将出现的内容框起来,这样便于用户能够立马看到有问题的文档,在一个文档上面画一个矩形框常用的方式就是通过svg去开发一个矩形框,根据坐标生成矩形框,可以使用svg的polygon形状容器去画一个标记。
三.开发方案
1.计算文档可视区域的高度和宽度
const docView = this.$refs.docView;// window.innerHeight - docView.getBoundingClientRect().top = 文档肉眼可见高度, 如果标记框有y值超过则需要滚动let canSeeHeight = window.innerHeight - docView.getBoundingClientRect().top;let canSeeWidth = docView.offsetWidth;
2.计算滚动的高度和宽度
// 需要滚动的高度let height = 0; // 离底部的高度即出现问题的文档出现在可视区上下中间的位置let htb = canSeeHeight / 2;// 需要滚动的宽度let width = 0;// 离左侧的宽度,现问题的文档出现在可视区中间的位置 let wtl = canSeeWidth / 4; for (let p of coord) { arr = [...arr, ...Object.values(p)]; // 高度取最下方的坐标位置 if (p.y - canSeeHeight + htb > height) { height = p.y - canSeeHeight + htb; } // 宽度取最右侧的坐标位置 if (p.x - canSeeWidth + wtl > width) { width = p.x - canSeeWidth + wtl; } }// 设置滚动条滚动 document.querySelector(".docView").scrollTo({ left: width, top: height, behavior: "smooth", });
3.画矩形框
this.points = arr;<svg class="tagRect"> <polygon :points="points" class="red-class"></polygon></svg>
四.效果
五.代码
<template> <PageContent :breadcrumbs="breadcrumbs"> <div class="docView" ref="docView"> <!-- 图片展示 --> <img v-if="docType === 'img'" :src="docUrl" alt="" /> <!-- pdf展示 --> <vue-pdf-embed ref="vuepdf" v-if="docType === 'pdf'" :source="docUrl" :page="currentPage" @loaded="loadedPdf" @loading-failed="loadedPdfFailed" @rendered="renderedPdf" @rendering-failed="renderingPdfFailed" :width="pdfWidth" @progress="progressPdf" /> <!-- 标记框 --> <svg class="tagRect"> <polygon :points="points" class="red-class"></polygon> </svg> </div> </div> <!-- 要素/规则 --> <div class="action"> <div class="action-title"> {{ type === "key" ? "标签" : "规则" }}列表 </div> <!-- 要素列表 --> <el-collapse @change="keyChange" v-model="activeName" accordion> <el-collapse-item v-for="(item, index) in keyList" :key="index" :name="index + 1" > <template slot="title"> {{ item.eleName }} </template> <template v-if="item.elementList !== null"> <!-- 展开切换文档 --> <!-- <div v-for="(file, ind) in item.fileHitList" :key="`${index}--${ind}`" > --> <div v-for="(ele, i) in item.elementList" :key="`${index}--${i}`"> <!-- 要素值 --> <div @click="switchDoc(ele, ele.url)" style="cursor: pointer"> 要素值: <span class="color-main">{{ ele.contentOri }}</span> </div> </div> <!-- </div> --> </template> <div style="margin-top: 10px"> 提取方式:{{ item.extractionMode }} </div> </el-collapse-item> </el-collapse> <!-- 规则列表 --> <el-collapse @change="keyChange" v-model="activeName" accordion v-if="['rule', 'rules'].includes(type) && ruleList.length > 0" > <el-collapse-item v-for="(item, index) in ruleList" :key="index" :name="index + 1" > <template slot="title"> {{ item.auditName }} </template> <template v-if="item.elementList.length > 0"> <!-- 渲染要素标签 --> <!-- <div v-for="ele in item.elementList" :key="ele.id"> --> <div style="margin-top: 10px" v-for="(ele, i) in item.elementList" :key="`${index}--${i}`" > <!-- 标签名称 --> <div>{{ ele.cnName }}</div> <!-- 要素值 --> <div @click=" switchDoc(ele, ele.url); fileName = ele.fileName; " style="cursor: pointer" > 要素值: <span class="color-main">{{ ele.contentOri }}</span> </div> <!-- </div> --> </div> </template> <div style="margin-top: 10px">提取方式:{{ item.modeName }}</div> <div style="margin-top: 10px">稽核结果:{{ item.auditValue }}</div> </el-collapse-item> </el-collapse> </div> </div> </PageContent></template> <script> // https://github.com/hrynko/vue-pdf-embed import VuePdfEmbed from "vue-pdf-embed/dist/vue2-pdf-embed"; export default { name: "ViewDoc", components: { VuePdfEmbed, }, data() { return { // 查看文档的类型 'key':要素 rule:规则单文档 rules:规则多文档 type: "key", // 默认画个五角星 points: "35,37.5 37.9,46.1 46.9,46.1 39.7,51.5 42.3,60.1 35,55 27.7,60.1 30.3,51.5 23.1,46.1 32.1,46.1", // 文档类型 txt pdf img word docUrl: "", docType: "", currentPage: 1, // 页码 pageCount: 0, // 总页数 keyList: [], ruleList: [], // 激活的面板默认第一个 activeName: 1, folderList: [], fileList: [], selectFolder: "", selectFile: "", fileName: "", // pdfLoading pdfLoading: false, pdfLoadingTxt: "", pdfPages: 0, showPdf: true, pdfWidth: "826", }, }; }, mounted() { }, methods: { // 手动切换文档 switchDoc(ele, url) { this.showDoc(url, ele); this.drawTagPoly(ele); }, // pdf,img 绘制标记框 drawTagPoly(ele) { const { coord } = ele; if (!coord) { return; } const docView = this.$refs.docView; // window.innerHeight - docView.offsetTop = 文档肉眼可见高度, 如果标记框有y值超过则需要滚动 let canSeeHeight = window.innerHeight - docView.offsetTop; let canSeeWidth = docView.offsetWidth; let arr = []; // 需要滚动的高度 let height = 0; // 离底部的高度 let htb = canSeeHeight / 2; // 需要滚动的宽度 let width = 0; // 离左侧的宽度 let wtl = canSeeWidth / 4; for (let p of coord) { arr = [...arr, ...Object.values(p)]; // 高度 if (p.y - canSeeHeight + htb > height) { height = p.y - canSeeHeight + htb; } // 宽度 if (p.x - canSeeWidth + wtl > width) { width = p.x - canSeeWidth + wtl; } } // console.log("scroll", height, width); // 设置滚动条滚动 document.querySelector(".docView").scrollTo({ left: width, top: height, behavior: "smooth", }); this.points = arr; } }, };</script> <style lang="scss" scoped> @import "@/styles/variables.scss"; .pageContent { .docView { position: relative; max-height: calc(100vh - 242px); max-width: 100%; overflow: scroll; .tagRect { overflow: visible !important; position: absolute; left: 0; top: 0; } .red-class { fill: transparent; stroke: red; stroke-width: 2; } } }</style>