vue2 上传pdf,拖拽盖章,下载图片

 效果图片:

不多废话上代码:

<template><div class="pdf-stamp" onbeforecopy='return false' onselect='document.selection.empty()' ondragstart='return false' onselectstart ='return false' ><div class="scroll-box" @scroll="onScroll"><div class="scroll-warp"><div class="seal-list"><div class="title"> 印章 </div><div class="seal-img"><div class="seal-img-content"><div v-for="(item, index) of sealOfTheList" :key="index" class="seal-item"><div class="img-content"> <img class="img" :src="item.img"@mousedown.stop="moveDown" /> </div></div></div></div></div><div class="content-box"><div class="with-file"><input type="file" class="file" id="file" ref="fielinput" @change="uploadFile" style="display: none;"/><label class="select-file" for="file">选择文件</label> <span class="file-name"> {{ fileName }} </span><button class="save-down" @click.stop="saveDown">立即下载</button></div><div class="canvas-box-border"><div class="canvas-content" ref="canvasBox"><canvas ref="pdfCanvas" class="canvas-pdf"> </canvas></div></div><div class="foot-bar"><button @click="clickPre">上一页</button><span>第 {{ pageNo }} / {{ pdfPageNumber }} 页</span><button @click="clickNext">下一页</button></div></div>        </div></div></div>
</template><script>import pdfJS from "pdfjs-dist";import "pdfjs-dist/build/pdf.worker.entry";export default {name: 'PdfStamp',data() {return {pageNo: 0,pdfPageNumber: 0,renderingPage: false,pdfData: null, // PDF的base64sealDomList: [], //储存印章domscrollTop: 0, //scrollToponce: false, //执行一次获取总之/*** option 设置项*/downFileText: "pdf_down_file", //下载文件名fileStamp: true, //是否需要给文件盖章 才可下载zIndex: 100, //给一个z-index 防止被其他元素遮盖导致立马触发mousedown 或者 mouseleave 删除元素sealOfTheList: [{ name: "携程旅行", img: "https://webresource.c-ctrip.com/ares2/nfes/pc-home/1.0.65/default/image/logo.png" },{ name: "虎牙直播", img: "https://a.msstatic.com/huya/main3/static/img/logo.png" },{ name: "厦门VG", img: "https://livewebbs2.msstatic.com/avatar_1_d52819f40bc198fbd1098b30dc1edacf.png" },{ name: "画压印", img: "http://shopxmhs.oss-cn-beijing.aliyuncs.com/3e6ae202206221409453342.png" },{ name: "招聘", img: "http://shopxmhs.oss-cn-beijing.aliyuncs.com/1daa8202206221409468728.png" },{ name: "诚邀", img: "http://shopxmhs.oss-cn-beijing.aliyuncs.com/2c0ba202206221409472433.png" },{ name: "广州TTG", img: "https://livewebbs2.msstatic.com/avatar_1_bf8ba03e1f78144d84f3538672ca282b.png" },{ name: "成都AG超玩会", img: "https://esports-cdn.namitiyu.com/kog/team/FpDfD5z0hFN3N2gMpQHWx38qwmeF" },{ name: "重点文件", img: "https://himg.bdimg.com/sys/portrait/item/pp.1.a6b7177e.aIllZw8UGQQ6MVIljwBO-A.jpg?tt=1746676347515" },],scale: 3, // 缩放值maxseal: 3, //最大seal数量fileName: "尚未选择文件", //初始文件名};},methods: {/*** 环境函数回调********//*** outMax() * max: 设置的最大值 newVale 触发执行的值(max+1)  * 对应maxseal配置项*/outMax(max, /*newVal*/) {console.log(`超出最大数量:${max}`); },/*** notSelectFile 尚未选择文件回调 * 无参数*/notSelectFile() {console.log('请选择文件');},/*** notStamp 尚未选择文件回调 * 无参数* 对应fileStamp配置项*/notStamp() {console.log('请给文件盖章');},/*** 展示file*/uploadFile() {this.once = false;let fileInput = this.$refs.fielinput;let fileData = fileInput.files[0];this.fileName = fileData.name;let reader = new FileReader(); //文件读取reader.readAsDataURL(fileData); //得到读取的文件reader.onload = () => { //文件加载let data = atob(reader.result.substring(reader.result.indexOf(",") + 1) //取找到 ',' 符号后一个索引开始的所有数据 就是文件base64数据 /** reader = data:application/pdf;base64,(JVBERi0xLj... = data) data文件base64数据atob() 函数源码: globalScope.atob = function (input) { return Buffer.from(input, 'base64').toString('binary'); 'binary' 转换'utf8'编码格式: 返回字符串}*/);this.loadPdfData(data);};},loadPdfData(data) {// 引入pdf.js的字体let CMAP_URL = "https://unpkg.com/pdfjs-dist@2.0.943/cmaps/";//读取base64的pdf流文件 返回pdf实例对象this.pdfData = pdfJS.getDocument({data: data, // PDF base64编码cMapUrl: CMAP_URL,cMapPacked: true,});this.renderPage(1);},  // 根据页码渲染相应的PDFrenderPage(num, callback) { //num传入页 返回对应页的pdf数据this.renderingPage = true;this.pdfData.promise.then((pdf) => {if (!this.once) {this.once = true;this.pdfPageNumber = pdf.numPages;  //pdf.numPages 文件总页数}pdf.getPage(num).then((page) => {// 获取DOM中为预览PDF准备好的canvasDOM对象 绘制内容let canvas = this.$refs.pdfCanvas;let viewport = page.getViewport(this.scale); //获取窗口属性canvas.height = viewport.height;canvas.width = viewport.width;  let ctx = canvas.getContext("2d");let renderContext = {canvasContext: ctx, //将对应ctx赋给renderContext.canvasContext 调用page.render(renderContext) 后内部 对应ctx.fillText() 绘制内容viewport: viewport,};page.render(renderContext).then(() => { //渲染当前页内容if (typeof(callback) === 'function') {callback(ctx);}this.renderingPage = false;this.pageNo = num; //获取当页内容});});});},//上一页clickPre() {if (this.pdfPageNumber - 1 >= 1) {this.renderPage(this.pageNo - 1);}},//下一页clickNext() {if (this.pageNo + 1 <= this.pdfPageNumber) {this.renderPage(this.pageNo + 1);}},/*** 创建seal dom*///按下moveDown(event) {console.log(event);let _this = this;let targetImg = event.srcElement; //触发的imgconsole.log(targetImg);// 获取图片中心点let yDistance = (targetImg.offsetHeight/2);let xDistance = (targetImg.offsetWidth/2);console.log(yDistance);console.log(xDistance);let sealDomList = this.sealDomList;let addIndex = sealDomList.length;//创建imglet img =  targetImg.cloneNode(true);img.tabIndex = addIndex;img.style.position = 'absolute';img.style.zIndex = this.zIndex;img.style.width = targetImg.offsetWidth + 'px';img.style.height = targetImg.offsetHeight + 'px';img.style.backgroundPosition = 'center';img.style.backgroundRepeat = 'no-repeat';img.style.backgroundSize = '100%'; img.style.backgroundSize = '100%'; let xy = this.getCanvasBoxXY();let canLeft = xy[0];let canTop = xy[1];_this.moveNode(img, (event.x - xDistance - canLeft), (event.y - yDistance - canTop + _this.scrollTop));//移动document.onmousemove = function(event) {_this.moveNode(img, (event.x - xDistance - canLeft), (event.y - yDistance - canTop + _this.scrollTop));}//放下)document.onmouseup = function () {document.onmousemove = null;document.onmouseup = null;Promise.resolve(_this.clearDOM(img, _this.$refs.canvasBox)).then(res => {if(!res) {img.addEventListener( 'mousedown', _this.down, true);img.addEventListener( 'mouseup', _this.up, true);img.addEventListener( 'mouseleave', _this.leave, true);}})}//插入元素this.$refs.canvasBox.appendChild(img);_this.sealDomList.push(img); //储存seal dom},/*canvasBox面向内部成员 view 定位 x, y返回: 数组[x, y]*/getCanvasBoxXY() {let canvasBox = this.$refs.canvasBox; //iamge放置定位盒子let xy = this.getDomLeft(canvasBox);let canLeft = xy[0];let canTop = xy[1];return [canLeft, canTop];},//按下down(e) { //拖拽 和 是否创建印章let _this = this;let ev = e.srcElement;ev.style.zIndex = this.zIndex + 1; //我们希望拖拽印章的时候, 不会因为其他成员遮盖影响let yDistance = (ev.offsetHeight/2);let xDistance = (ev.offsetWidth/2);let xy = this.getCanvasBoxXY();let canLeft = xy[0];let canTop = xy[1];_this.moveNode(ev, (e.x - xDistance - canLeft), (e.y - yDistance - canTop + _this.scrollTop));ev.onmousemove = function (event) {_this.moveNode(ev, (event.x - xDistance - canLeft), (event.y - yDistance - canTop + _this.scrollTop));}},//放下up(event) { //停止拖拽且是否删除印章let target = event.srcElement;target.style.zIndex = this.zIndex; //我们希望结束拖拽操作后 印章的时候回到初始层级;target.onmousemove = null;this.clearDOM(target, this.$refs.canvasBox);},//离开leave(event) { //停止拖拽event.srcElement.onmousemove = null;},//定位moveNode(event, x, y) {event.style.left = x + 'px';event.style.top = y + 'px';},/*** 是否出界需清除* 返回: 布尔值 是否被删除*/clearDOM(node, box) {//node domlet target = node;let tarTop  =  target.offsetTop;let tarLeft = target.offsetLeft;let tarBottom = tarTop + target.offsetHeight;let tarRight= tarLeft + target.offsetWidth;//box domlet fileDom = box;let height = fileDom.offsetHeight;let width = fileDom.offsetWidth;if (tarBottom <  0 || tarTop > height) {this.removeSealChild(target);return true;}else if (tarRight < 0 || tarLeft > width) {this.removeSealChild(target);return true;}if (this.sealDomList.length > this.maxseal) { //最seal大数量this.outMax(this.maxseal, this.sealDomList.length );this.removeSealChild(node);return true;}return false;},//移除元素removeSealChild(node) {this.$refs.canvasBox.removeChild(node);this.sealDomList.splice(node.tabIndex, 1); for (let i = 0; i < this.sealDomList.length; i++) { //重新排序tabIndex标识this.sealDomList[i].tabIndex = i;}},/*** canvas下载*/saveDown() {if (!this.pageNo) {return this.notSelectFile();}else if (!this.sealDomList.length && this.fileStamp) {return this.notStamp();}else{this.drawImage(this.sealDomList);}},//绘制图片drawImage(imageList) {let canvas = this.$refs.pdfCanvas;let canvasBox = this.$refs.canvasBox;let _this = this;if (!this.fileStamp && !this.sealDomList.length) { //跳过印章绘制_this.canvasFile();return _this.backInitialState(_this.sealDomList);}function func(ctx) {let ratioX = canvas.width / canvasBox.offsetWidth;let ratioY = canvas.height / canvasBox.offsetHeight; let count = 0; //当前进度let totalCount = imageList.length; //总进度for (let image of imageList) {let imgLeft = image.offsetLeft;let imgTop = image.offsetTop;let x = imgLeft * ratioX;let y = imgTop * ratioY;let img = new Image(20, 10);img.crossOrigin = 'anonymous';img.onload = () => {count++;ctx.drawImage(img, x, y, image.offsetWidth*ratioX, image.offsetHeight*ratioY);if (count === totalCount) {_this.canvasFile();_this.backInitialState(_this.sealDomList);}};img.src = image.src;}}this.renderPage(this.pageNo, func);},//canvas 文件数据 下载canvasFile() {let canvas = this.$refs.pdfCanvas;let dataURL = canvas.toDataURL('image/png'); //canva文件数据this.downLoad(dataURL);},//下载文件downLoad(url) {let note = document.createElement('a');note.download = this.downFileText; // 设置下载的文件名,默认是'下载'note.href = url;document.body.appendChild(note);note.click();note.remove();},//下载成功 清空印章backInitialState(domList) {this.renderPage(this.pageNo);let len = domList.length;for (let i = 0; i < len; i++) {this.removeSealChild(domList[0]);}},/*** scrollTop*/onScroll(event) {let target = event.srcElement;this.scrollTop = target.scrollTop;},/*** 查找DOM 的 style属性*/getStyleVal(node, styleStr) { let style;// let parent = node.parentNode;if (node === document) { //window.getComputedStyle方法 不可调用 document 我们不对他查询style = null;}else {style = window.getComputedStyle(node)[styleStr];}return style;},      /*** 去除单位得到数值*/matchNum(str) {const regexp = /\d+(\.\d+)?/g; //匹配数字return Number((str+"").match(regexp)[0]) >>> 0;},/*** getDom 递归检测DOM 确定定位多次赋值 得到总真实offsetLeft 和 offsetTop* key: 可选 offsetLeft 和 offsetTop*/getDomLeft(node) {let _this = this;let valueXY = [0, 0]; //储存值let parent = node.parentNode;let uncertain = ["static", "initial", "revert" , "unset" ]; //定位被确定function dg(node, parent) {/*** //是否需要scrollXY (注销注释将不调用this.onScroll)* valueXY[0] -= parent.scrollLeft; //scrollLeft* valueXY[1] -= parent.scrollTop;  //scrollTop*/if (!~uncertain.indexOf(_this.getStyleVal(parent, "position"))) { valueXY[0] += node.offsetLeft + _this.matchNum(_this.getStyleVal(parent, "borderLeft"));valueXY[1] += node.offsetTop + _this.matchNum(_this.getStyleVal(parent, "borderTop"));return dg(parent, parent.parentNode); //多次上级访问找找到父节确定定位的元素 做 坐标 位置重新规划为定位后的元素 进行下次访问再取坐标}else {let grandParentNode = parent.parentNodeif (grandParentNode !== document) {return dg(node, parent.parentNode); //如果没找到一直上级查找 知道抵达父级为 document查询结束}else { //到达documen时候立即停止valueXY[0] += node.offsetLeft;valueXY[1] += node.offsetTop;return valueXY;}}}return dg(node, parent);}}}
</script><style scoped>
.pdf-stamp {width: 100vw;height: 100vh;background-color: white;overflow: hidden;position: relative;z-index: 1;
}.scroll-box {width: 100vw;height: 100vh;overflow-x: hidden;overflow-y: scroll;position: relative;}.scroll-box::-webkit-scrollbar {display: none;}.scroll-warp {display: flex;position: relative;}.seal-list {height: 400px;text-align: center;border: 2px solid #d3cece;background-color: #e7e7e7;display: flex;flex-direction: column;margin: 50px 100px;border-radius: 5px;}.title {font-size: 20px;margin: 0 10px;font-weight: 600;padding: 5px;color: #7a7a7a;border-bottom: 1px solid #d6d6d6;}.seal-img {flex: 1;overflow-x: hidden;overflow-y: scroll;}.seal-img::-webkit-scrollbar {display: none;}.seal-img-content {padding: 0 10px;}.seal-item {margin: 10px 0;}.img-name {color: #b6b6b6;font-weight: 600;padding: 10px;background-color: #fffdf9;border-radius: 5px;}.img-content {height: 100px;display: flex;justify-content: center;align-items: center;}.img-content .img {width: 100px;}.content-box {text-align: center;position: relative;margin: 50px;background-color: #f3efe6;padding: 10px 25px;border-radius: 5px;}.with-file {display: flex;align-items: center;justify-content: space-between;padding-bottom: 10px;}.select-file {display: block;padding: 10px 50px;background-color: #e9d2ff;color: #000;border-radius: 5px;}.save-down {padding: 10px 50px;border-style: none;display: block;background-color: #e9d2ff;border-radius: 5px;}.file-name {color: #919191;font-size: 18px;font-weight: 600;}.canvas-box-border {border: 4px double black;}.canvas-content {width: 500px;height: 700px;position: relative; }.canvas-pdf {width: 100%;height: 100%;}.foot-bar {position: relative;padding: 10px;display: flex;justify-content: space-around;align-items: center;}.foot-bar button {border-style: none;background-color: #efc9aa;display: block;padding: 10px 50px;border-radius: 5px;}.foot-bar span {color: #186666;}
</style>

依赖包:    "pdfjs-dist": "^2.0.943",

npm install pdfjs-dist@^2.0.943

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/904979.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

理性地倾听与表达:检索算法的语言学改进

论文标题 Rational Retrieval Acts: Leveraging Pragmatic Reasoning to Improve Sparse Retrieval 论文地址 https://arxiv.org/pdf/2505.03676 代码地址 https://github.com/arthur-75/Rational-Retrieval-Acts 作者背景 巴黎萨克雷大学&#xff0c;索邦大学&#xff…

MySQL及线程关于锁的面试题

目录 1.了解过 MySQL 死锁问题吗&#xff1f; 2.什么是线程死锁&#xff1f;死锁相关面试题 2.1 什么是死锁&#xff1a; 2.2 形成死锁的四个必要条件是什么&#xff1f; 2.3 如何避免线程死锁&#xff1f; 3. MySQL 怎么排查死锁问题&#xff1f; 4.Java线上死锁问题如…

【Reality Capture 】Reality Capture1.5中文版安装教程(附安装包下载)

文章目录 一、Reality Capture1.5中文版安装教程二、拷贝中文补丁三、Reality Capture1.5中文版下载地址一、Reality Capture1.5中文版安装教程 1. Reality Capture v1.4.0汉化版安装包下载并解压 2. 运行EpicInstaller-15.17.1-4a91a118786f4c2aa3c0093b23f83863.msi 3. 更改…

SVG数据可视化设计(AI)完全工作流解读|计育韬

AI 的 SVG 创作极限在哪里&#xff1f;绝不是那些初级的流程图生成和粗糙的商业模型设计。以下是由我们 JZ Creative Studio 通过 Claude 和 Deepseek 开展的专业级 SVG Data Visualization 创作&#xff0c;应广大读者强烈要求&#xff0c;专程直播讲授了一期 AI 工作流分享。…

not a genuine st device abort connection的问题

1.魔法棒里面电机Settings 2.然后在Other里面把Enabled的钩子去掉

uv简单使用

通过uv创建项目和虚拟环境 初始化项目 uv init --package my-project 初始化一个名为 my-project 的新项目&#xff0c;并生成必要的文件结构。 创建虚拟环境 uv venv .venv 激活虚拟环境 # For Windows .venv\Scripts\activate# For macOS/Linux source .venv/bin/acti…

测试左移系列-产品经理实战-实战认知1

课程&#xff1a;B站大学 记录产品经理实战项目系统性学习&#xff0c;从产品思维&#xff0c;用户画像&#xff0c;用户体验&#xff0c;增长数据驱动等不同方向理解产品&#xff0c;从0到1去理解产品从需求到落地的全过程&#xff0c;测试左移方向&#xff08;靠近需求、设计…

从需求到用例的AI路径:准确率与挑战

用工作流生成测试用例和自动化测试脚本&#xff01; 引言&#xff1a;用例的黄金起点 在软件工程中&#xff0c;“测试用例”是连接需求理解与质量保障之间的关键桥梁。一份高质量的测试用例&#xff0c;不仅是验证功能实现是否符合需求的工具&#xff0c;更是产品风险感知、用…

大语言模型中的“温度”参数到底是什么?如何正确设置?

近年来&#xff0c;市面上涌现了大量调用大模型的工具&#xff0c;如 Dify、Cherry Studio 等开源或自研平台&#xff0c;几乎都提供了 “温度”&#xff08;Temperature&#xff09; 选项。然而&#xff0c;很多人在使用时并不清楚该如何选择合适的温度值。 今天&#xff0c;…

如何删除网上下载的资源后面的文字

这是我在爱给网上下载的音效资源&#xff0c;但是发现资源后面跟了一大段无关紧要的文本&#xff0c;但是修改资源名称后还是有。解决办法是打开属性然后删掉资源的标签即可。

hot100-子串-JS

一、560.和为k的子串 560. 和为 K 的子数组 提示 给你一个整数数组 nums 和一个整数 k &#xff0c;请你统计并返回 该数组中和为 k 的子数组的个数 。 子数组是数组中元素的连续非空序列。 示例 1&#xff1a; 输入&#xff1a;nums [1,1,1], k 2 输出&#xff1a;2示例 2…

01背包类问题

文章目录 [模版]01背包1. 第一问: 背包不一定能装满(1) 状态表示(2) 状态转移方程(3) 初始化(4) 填表顺序(5) 返回值 2. 第二问: 背包恰好装满3. 空间优化 416.分割等和子集1. 状态表示2. 状态转移方程3. 初始化4. 填表顺序5. 返回值 [494. 目标和](https://leetcode.cn/proble…

解锁 DevOps 新境界 :使用 Flux 进行 GitOps 现场演示 – 自动化您的 Kubernetes 部署

前言 GitOps 是实现持续部署的云原生方式。它的名字来源于标准且占主导地位的版本控制系统 Git。GitOps 的 Git 在某种程度上类似于 Kubernetes 的 etcd&#xff0c;但更进一步&#xff0c;因为 etcd 本身不保存版本历史记录。毋庸置疑&#xff0c;任何源代码管理服务&#xf…

将Docker镜像变为可执行文件?体验docker2exe带来的便捷!

在现代软件开发中,容器化技术极大地改变了应用程序部署和管理的方式。Docker,作为领先的容器化平台,已经成为开发者不可或缺的工具。然而,对于不熟悉Docker的用户来说,接触和运行Docker镜像可能会是一个复杂的过程。为了解决这一问题,docker2exe项目应运而生。它提供了一…

IBM BAW(原BPM升级版)使用教程第八讲

续前篇&#xff01; 一、流程开发功能模块使用逻辑和顺序 前面我们已经对 流程、用户界面、公开的自动化服务、服务、事件、团队、数据、性能、文件各个模块进行了详细讲解&#xff0c;现在统一进行全面统一讲解。 在 IBM Business Automation Workflow (BAW) 中&#xff0c;…

针对共享内存和上述windows消息机制 在C++ 和qt之间的案例 进行详细举例说明

针对共享内存和上述windows消息机制 在C++ 和qt之间的案例 进行详细举例说明 以下是关于在 C++ 和 Qt 中使用共享内存(QSharedMemory)和 Windows 消息机制(SendMessage / PostMessage)进行跨线程或跨进程通信的详细示例。 🧩 使用 QSharedMemory 进行进程间通信(Qt 示例…

jetson orin nano super AI模型部署之路(十)使用frp配置内网穿透,随时随地ssh到机器

为什么要内网穿透&#xff1f; 我们使用jetson设备时&#xff0c;一般都是在局域网内的电脑去ssh局域网内的jetson设备&#xff0c;但是这种ssh或者VNC仅限于局域网之间的设备。 如果你出差了&#xff0c;或者不在jetson设备的局域网内&#xff0c;想再去ssh或者VNC我们的jet…

VScode密钥(公钥,私钥)实现免密登录【很细,很全,附带一些没免密登录成功的一些解决方法】

一、 生成SSH密钥对 ssh-keygen 或者 ssh-keygen -t rsa -b 4096区别&#xff1a;-t rsa可以明确表示生成的是 RSA 类型的密钥-b参数将密钥长度设置为 4096 位默认&#xff1a;2048 位密钥不指定-t参数&#xff0c;ssh -keygen默认也可能生成 RSA 密钥【确保本机安装ssh&#…

解释器和基于规则的系统比较

解释器&#xff08;Interpreter&#xff09;和基于规则的系统&#xff08;Rule-Based System&#xff09;是两种不同的软件架构风格&#xff0c;分别适用于不同的应用场景。它们在设计理念、执行机制和适用领域上有显著差异。以下是它们的核心对比&#xff1a; 1. 解释器&#…

DB4S:一个开源跨平台的SQLite数据库管理工具

DB Browser for SQLite&#xff08;DB4S&#xff09;是一款开源、跨平台的 SQLite 数据库管理工具&#xff0c;用于创建、浏览和编辑 SQLite 以及 SQLCipher 数据库文件。 功能特性 DB4S 提供了一个电子表格风格的数据库管理界面&#xff0c;以及一个 SQL 查询工具。DB4S 支持…