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.pswp.cn/news/904979.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/904979.shtml
英文地址,請注明出處:http://en.pswp.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 支持…