Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
計量領域中,計量檢定是一種重要形式,主要用于評定計量器具的計量性能,確定其量值是否準確一致,實現手段包括計量檢驗、出具檢定證書和加封蓋印等。
在檢定證書這一環節,存在一個難點,就是無法在線預覽以及智能生成。
1、證書管理不能滿足用戶精準打印、特殊字符或多頁打印的需求。因為在計量行業中,精密儀器較多,往往會存在一些特殊字符的應用或者會使用某些較為復雜的測量單位。
2、系統不支持批量證書更新以及批量打印等功能,在常見的場景中,出具證書是需要進行批量導出的過程。
3、無法滿足實時打印預覽或者PDF預覽,這樣直至打印前都無法確定打印的格式、范圍等是否符合需求。
在這篇分享中,我們將幫助大家著重解決兩個問題:
1、在瀏覽器中生成PDF文件;
2、解決中文以及特殊字符導出PDF亂碼的問題。
在瀏覽器中生成PDF文件。前端生成PDF文件純依賴于客戶端的瀏覽器資源,對于不同的終端,導出PDF的難度會比服務端有所增加。市面上主流的瀏覽器有三四家,例如Chrome、Safari、FireFox等,每個瀏覽器對于文字內容、CSS屬性處理都不一致,有可能某些配置在某個瀏覽器上可行,換了一個瀏覽器之后就有可能天差地別。另外,對于原生的PDF文件來說,僅包含英文字體,不包含任何中文字體,因此當導出的內容中含有中文字體編碼時,就會顯示亂碼,所以通常情況下,我們都需要為PDF進行字體注冊操作。
目前常用的前端生成PDF文件的方法大致有以下幾種。
1、HTML2Canvas的方法將HTML 轉換成圖片后,在將圖轉PDF文件。這種方法比較適合單一頁面。
2、jsPDF 直接H5轉成PDF。
除了上述的方案之后,使用SpreadJS直接在線設計布局,并且可以直接生成PDF文件。 帶來的好處是什么呢?可視化的操作、代碼量少并且可以適配不同的瀏覽器環境。當然也會有一定的缺點,對于字體較多的文件,需要注冊不同的字體,字體文件越大,占用的帶寬就越大。另外,當文件比較大的時候,有可能會存在性能問題,不過這個也幾乎是前端導出PDF文件的一個瓶頸。那么較為理想的方案便是可以在前端(SpreadJS)設計、展示,最后交由后端來單獨導出或者批量導出。
介紹了那么多,我們還是回到本篇文章的主題,如何通過前端來生成PDF文件。需要用到SpreadJS以及導出PDF相關的功能,首先需要在頁面上引入相關的資源。
然后創建一個用于承載表格實例的DOM。
初始化表格控件并加載已設計好的表單,或者也可以通過setValue的接口實現簡單的賦值操作。
想要表單按照指定的要求導出,可以通過代碼設置打印相關的配置,也可以用設計器來進行設計。下面是配置打印信息相關的代碼。
最后,通過調用savePDF方法,將工作簿對象轉為blob,我們可以通過window.open來進行pdf的預覽或者通過一些保存文件的插件直接將這個blob保存為PDF文件。
這是open之后的效果,我們可以直接通過瀏覽器導出PDF文件或者是調用瀏覽器的打印接口實現打印。
正如前面所說的,在國內,使用中文的報告是一件再常見不過的事,在計量檢測等相關場景,特殊字符的使用也較多。在沒注冊對應的字體之前,導出的中文字體和特殊字體都顯示的是亂碼。因此,還需要處理導出中文以及特殊字符PDF亂碼的問題。
前面提到了注冊字體,那我們的字體應該怎么來?要什么格式的字體呢?首先,先確認我們的表單需要用到哪些字體,然后去找對應字體的ttf文件(電腦上或者是一些字體網站上都有,需注意版權問題)。找到之后將其轉為base64格式的文件。具體如何轉,可以找一些在線的文件轉換器,不過在線的有可能會因為字體文件太大而崩潰,或者有能力的大佬可以自己寫一個轉換的工具。然后通過下面的方式去把我們的字體文件存儲為一個js文件放到我們的項目中。
初始化表單這些就和上面的操作基本一致了,下面就是關鍵的注冊字體步驟了。我們定義了一個font對象,里面只定義了常規(normal)的字體,里面的simkai.ttf就是我們上面的創建的字體文件。
還有一點需要注意的是,雖然注冊了字體,但是要設置對應的中文字體。或者換過來說,你需要在表單上設置什么字體,就去注冊對應的字體。
那我們再來看看特殊字符,注冊字體與中文字體的步驟是一致的,特殊在于為了想要在頁面上顯示特殊字符,我們需要通過css的font-face來指定一個font-family。例如創建了一個叫sunway-font的特殊字體,想要在頁面上顯示。
最后就是通過savePDF方法導出PDF文件,可以看到PDF的中文和特殊字符都可以正常顯示。
怎么樣?學“廢”了嗎?不妨試試SpreadJS,“卷”起來。
者 | 大澈
大家好,我是大澈!
遇到難題,可以進問答群,問題直接群里扔,完事總有人會陪你一起搞。
建立這個平臺的初衷:
1、打造一個問答平臺,一個僅包含前端問題的平臺,讓大家可以高效處理同樣問題。
2、通過不斷積累問題,去練習大家的個人邏輯思維,并順便學習相關的知識點。
3、遇到難題,遇到有共鳴的問題,一起討論,一起沉淀,一起成長。
ONE
需求分析,問題描述
一、需求
點擊導出word按鈕,將頁面任意指定區域的內容,導出為word文檔。
二、問題
1、如何獲取指定內容?
2、如何將HTML內容轉換為Word文檔?
3、如何導出下載Word文檔?
TWO
解決問題,答案速覽
實現代碼如下,復制粘貼即可直接使用。
如果你有時間,具體問題梳理、代碼分析、知識總結,可見第三部分。
一、使用說明
參考網上使用最多的方式,使用 FileSaver.js 和 html-docx-js 庫(或其它將HTML內容轉換為Word文檔的庫)來在Vue組件中導出內容為Word文檔。
但是這種方式只能在服務端實現,在瀏覽器端使用會報錯,這是因為瀏覽器的安全策略限制了對文件系統的直接訪問,以防止惡意腳本濫用用戶的文件系統。
為了避免報錯,您可以考慮以下解決方案:
1、服務器端導出【推薦】:將生成Word文檔的邏輯放在服務器端,通過Vue組件向服務器發送請求,服務器生成并返回Word文檔的下載鏈接或文件。
2、使用其他導出方式:考慮使用其他導出方式,例如將內容轉換為PDF格式或生成HTML格式的文件,以避免瀏覽器限制。
3、考慮使用專門的Word文檔生成庫【導出復雜Word】:如果您需要在瀏覽器中生成復雜的Word文檔,可以考慮使用專門的JavaScript庫,例如docxtemplater或mammoth.js,它們提供了更完整的Word文檔生成功能。
4、考慮使用原生的方式實現【導出簡單Word】。即我們下面要說的,利用a元素的原生文件下載功能來實現Word導出。
二、代碼實例
1、在assets文件夾下新建js文件夾,然后在js文件夾下新建文件exportToWord.js,把下面代碼放進去。
// 導出Word
export const exportToWord = (id, name) => {
// 獲取選中區域Html
const dom = document.getElementById(id)
const content = dom.innerHTML;
const convertedContent = convertToWordDocument(content);
// Html類型數據 轉換為 文件類型數據
const blob = new Blob([convertedContent], { type: 'application/msword' });
// 下載Word文檔
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = name+'.doc' || 'exported.doc';
link.click();
}
//完善Html格式
const convertToWordDocument = (content) => {
const header = `<!DOCTYPE html><html><head><meta charset='utf-8'><title>Exported Document</title></head><body>`;
const footer = `</body></html>`;
return `${header}${content}${footer}`;
}
2、在需要的組件中引入exportToWord函數,直接調用即可。函數接收兩個參數,第一個是指定區域元素的id,第二個是導出Word文檔的名稱。
<template>
<div>
<div id="word_demo">
<h1>標題</h1>
<p style="color: green">綠色字</p>
<p style="color: red">紅色字</p>
<p style="color: dodgerblue">藍色字</p>
</div>
<button @click="downLoad('word_demo', '哈哈哈')">點擊導出word</button>
</div>
</template>
<script>
import { exportToWord } from '@/assets/js/exportToWord'
export default {
methods: {
downLoad(id, name) {
exportToWord(id, name)
},
}
}
</script>
THREE
問題解析,知識總結
一、如何獲取指定內容?
這個比較簡單,相信大家都會,這里簡單提一下。
對于組件可以用ref,對于元素可以用id。
二、如何將HTML內容轉換為Word文檔?
通過new Blob對象,將Html類型數據轉換為生成Word文檔的二進制數據。
關于Blob對象:
1、簡介:
Blob(Binary Large Object)是JavaScript中的一個接口,用于表示不可變的、原始數據的類似文件的對象。
它通常用于處理二進制數據,例如圖像、音頻、視頻等。
Blob對象可以包含任意類型的數據,包括文本、數組緩沖區和其他Blob對象。
Blob對象在處理文件上傳、數據傳輸和媒體處理等場景中非常有用。您可以將Blob對象發送到服務器、保存到本地文件系統或使用其他API進行進一步處理。
2、Blob對象的構造函數接受以下參數:
Blob(blobParts, options):構造函數接受兩個參數。
第一個參數blobParts是一個數組,其中包含將被包含在Blob對象中的數據。數組的元素可以是字符串、ArrayBuffer、ArrayBufferView、Blob對象或其他類似對象。
第二個參數options是一個可選的對象,用于指定Blob對象的屬性。
在options參數中,可以使用以下屬性:
type:指定Blob對象的MIME類型。默認值為空字符串。
endings:指定以何種方式標準化換行符。可能的值是transparent、native和\r\n。默認值是transparent。
3、以下是一些常用的Blob屬性和方法:
屬性:
Blob.size:返回Blob對象的字節大小。
Blob.type:返回Blob對象的MIME類型。
方法:
Blob.slice(start, end, contentType):
創建并返回一個新的Blob對象,該對象包含原始Blob對象的指定字節范圍。可選參數contentType用于指定新Blob對象的MIME類型。
Blob.arrayBuffer():
返回一個Promise,該Promise解析為一個ArrayBuffer對象,其中包含Blob對象的整個內容。
Blob.text():
返回一個Promise,該Promise解析為一個字符串,其中包含Blob對象的文本內容。
Blob.stream():
返回一個ReadableStream對象,可以用于流式讀取Blob對象的內容。
Blob.text():
返回一個Promise,該Promise解析為一個字符串,其中包含Blob對象的文本內容。
Blob.stream():
返回一個ReadableStream對象,可以用于流式讀取Blob對象的內容。
三、如何導出下載Word文檔?
通過a元素的 download 屬性,來實現文件的導出下載。
在 HTML 中,a元素的 download 屬性用于指定一個下載鏈接,告訴瀏覽器該鏈接是要被下載而不是在瀏覽器中打開。這樣,當用戶點擊鏈接時,瀏覽器會彈出一個下載對話框,提示用戶保存文件到本地設備。
download 屬性的值可以是一個文件名,用于指定用戶保存文件時的默認文件名。當用戶點擊下載鏈接時,瀏覽器會使用該值作為默認文件名,但用戶仍然可以選擇其他文件名保存。
請注意,download 屬性并不是所有瀏覽器都支持的新特性。特別是在移動設備上,某些瀏覽器可能會忽略該屬性并在瀏覽器中打開鏈接。因此,在使用 download 屬性時,最好提供一個備用方案,例如在鏈接的文本或旁邊添加一段說明,告訴用戶右鍵點擊鏈接并選擇 "保存鏈接" 或類似選項來下載文件。
- END -
何保持頁面樣式基本不變的前提下將HTML頁面導出為PDF,下面提供一些示例代碼,純屬個人原創,如對你有幫助請記得加關注、加收藏、點贊、轉發、分享~謝謝~~
<div>
<!-- 要打印的內容區 -->
<div ref="contentRef">
<div class="print-item print-out-flow">這是脫離文檔流的內容區域</div>
<div class="print-item">這是一行內容,也是最小葉子元素內容</div>
</div>
<!-- 打印內容容器 -->
<div ref="printContainerRef" class="print-container"></div>
</div>
/**
* 1.使用一個隱藏div裝載有滾動條的div.innerHTML
* 2.隱藏div使用position: absolute, z-index: -999, left: -9999px, width: 900px 控制讓用戶無感知
* 3.根據需要覆寫隱藏div內html樣式(例如textarea多行顯示有問題, 可以新增一個隱藏的div
* 包裹textarea的綁定值, 然后在打印樣式中覆寫樣式, 隱藏textarea并顯示對應div)
*/
handleExport() {
// 下面是VUE組件內獲取DOM元素代碼,將內容放置到打印區(定義的隱藏DIV)中
const contentRef = this.$refs.contentRef as HTMLElement;
const printContainerRef = this.$refs.printContainerRef as HTMLElement;
// 打印區的需額外處理絕對定位值, 調整使得第一個元素的.top值為0, 以便于頁面計算
printContainerRef.innerHTML = contentRef.innerHTML;
// 所有葉子div元素加上 print-item 樣式名, 脫離文檔流的額外添加 print-out-flow
handlePrintItem(printContainerRef); // 解決多頁內容可能被切割問題
html2canvas(printContainerRef, {allowTaint: false, useCORS: true}).then((canvas: any) => {
const contentHeight = canvas.height;
const contentWidth = canvas.width;
// pdf每頁顯示的內容高度
const pageHeight = contentWidth / 595.28 * 841.89;
// 未生成pdf的頁面高度
let offsetHeight = contentHeight;
// 頁面偏移值
let position = 0;
// a4紙的尺寸[595.28, 841.89], canvas圖片按a4紙大小縮放后的寬高
const imgWidth = 595.28;
const imgHeight = 595.28 / contentWidth * contentHeight;
const dataURL = canvas.toDataURL('image/jpeg', 1.0);
const doc = new jsPDF('p', 'pt', 'a4');
if (offsetHeight < pageHeight) {
doc.addImage(dataURL, 'JPEG', 0, 0, imgWidth, imgHeight);
} else {
while (offsetHeight > 0) {
doc.addImage(dataURL, 'JPEG', 0, position, imgWidth, imgHeight);
offsetHeight -= pageHeight;
position -= 841.89;
if (offsetHeight > 0) {
doc.addPage();
}
}
}
doc.save(this.generateReportFileName());
printContainerRef.innerHTML = '';
});
}
上干貨代碼:上面分頁導出PDF可能網上能看到類型代碼,但絕對找不到下面的代碼,純手搓解決分頁元素被切開問題(思路:獲取自身定位,如自己剛好在被分頁處,則加上一定的margin-top值將內容向下移)
/**
* 處理打印元素項, 修復分頁后被切割的元素
* @param printContainerRef 打印內容div容器
* @param itemClassName 打印最小元素標識類名
* @param outFlowClassName 脫離文檔流的元素標識類名
*/
export function handlePrintItem(
printContainerRef: HTMLElement,
itemClassName: string = 'print-item',
outFlowClassName: string = 'print-out-flow'
): void {
const rootClientRect = printContainerRef.getBoundingClientRect();
// 初始化頁面相關數據
const totalHeight = rootClientRect.height; // 內容總高度
const a4PageHeight = (printContainerRef.clientWidth / 595.28) * 841.89; // a4紙高度
let pageNum = Math.ceil(totalHeight / a4PageHeight); // 總頁數
let addPageHeight = 0; // 修正被分割元素而增加的頁面高度總和
let currentPage = 1; // 當前正在處理切割的頁面
const splitItemObj: { [key: number]: HTMLElement[] } = {}; // 內容中各頁被切割元素存儲對象
const printItemNodes: NodeListOf<HTMLElement> = printContainerRef.querySelectorAll(`.${itemClassName}`);
for (let item of printItemNodes) {
// 如果當前頁已經是最后一頁, 則中斷判斷
if (currentPage >= pageNum) {
break;
}
// 獲取元素絕對定位數據
const clientRect = item.getBoundingClientRect();
let top = clientRect.top;
const selfHeight = clientRect.height;
// 如果當前元素距離頂部高度大于當前頁面頁腳高度, 則開始判斷下一頁頁腳被切割元素
if (top > currentPage * a4PageHeight) {
// 換頁前修正上一頁被切割元素
addPageHeight += fixSplitItems(currentPage, a4PageHeight, splitItemObj[currentPage], outFlowClassName);
pageNum = Math.ceil((totalHeight + addPageHeight) / a4PageHeight);
top = item.getBoundingClientRect().top;
currentPage++;
}
// 如果元素剛好處于兩頁之間, 則記錄該元素
if (top > (currentPage - 1) * a4PageHeight && top < currentPage * a4PageHeight && top + selfHeight > currentPage * a4PageHeight) {
if (!splitItemObj[currentPage]) {
splitItemObj[currentPage] = [];
}
splitItemObj[currentPage].unshift(item);
// 如果當前元素是最后一個元素, 則直接處理切割元素, 否則交由處理下一頁元素時再處理切割
if (item === printItemNodes[printItemNodes.length - 1]) {
fixSplitItems(currentPage, a4PageHeight, splitItemObj[currentPage], outFlowClassName);
}
}
}
}
/**
* 修復當前頁所有被切割元素
* @param currentPage 當前頁
* @param pageHeight 每頁高度
* @param splitElementItems 當前被切割元素數組
* @param outFlowClassName 脫離文檔流的樣式類名
*/
function fixSplitItems(
currentPage: number,
pageHeight: number,
splitElementItems: HTMLElement[],
outFlowClassName: string
): number {
if (!splitElementItems || !splitElementItems.length) {
return 0;
}
const yMargin = 5; // y方向距離頁眉的距離
const splitItemsMinTop = getSplitItemsMinTop(splitElementItems);
if (!splitItemsMinTop) {
return 0;
}
let fixHeight = currentPage * pageHeight - splitItemsMinTop + yMargin;
const outFlowElement = splitElementItems.find((item) => item.classList.contains(outFlowClassName));
if (outFlowElement && outFlowElement.parentElement) {
const parentPreviousElement = outFlowElement.parentElement.previousElementSibling as HTMLElement;
fixHeight += getMarinTopNum(parentPreviousElement, outFlowElement.parentElement);
outFlowElement.parentElement.style.marginTop = `${fixHeight}px`;
return fixHeight;
}
splitElementItems.forEach((splitElement) => {
splitElement.style.marginTop = `${fixHeight}px`;
});
return fixHeight;
}
/**
* 獲取被切割元素數組中最小高度值(如一行有多個元素被切割,則選出距離頂部最小的高度值)
* @param splitElementItems 當前被切割元素數組
*/
function getSplitItemsMinTop(
splitElementItems: HTMLElement[]
): number | undefined {
// 獲取元素中最小top值作為基準進行修正
let minTop: number | undefined;
let minElement: HTMLElement | undefined;
splitElementItems.forEach((splitElement) => {
let top = splitElement.getBoundingClientRect().top;
if (minTop) {
minTop = top < minTop ? top : minTop;
minElement = top < minTop ? splitElement : minElement;
} else {
minTop = top;
minElement = splitElement;
}
});
// 修正當前節點及其前面同層級節點的margin值
if (minTop && minElement) {
const previousElement = splitElementItems[splitElementItems.length - 1].previousElementSibling as HTMLElement;
minTop -= getMarinTopNum(previousElement, minElement);
}
return minTop;
}
/**
* 通過前一個兄弟元素和元素自身的位置確認一個距離頂部高度修正值
* @param previousElement 前一個兄弟元素
* @param curElement 當前元素
*/
function getMarinTopNum(previousElement: HTMLElement, curElement: HTMLElement): number {
let preMarginNum = 0;
let curMarginNum = 0;
if (previousElement) {
// 獲取外聯樣式需要getComputedStyle(), 直接.style時對象的值都為空
const previousMarginBottom = window.getComputedStyle(previousElement).marginBottom;
preMarginNum = previousMarginBottom ? Number(previousMarginBottom.replace('px', '')) : 0;
}
const marginTop = window.getComputedStyle(curElement).marginTop;
curMarginNum = marginTop ? Number(marginTop.replace('px', '')) : 0;
return preMarginNum > curMarginNum ? preMarginNum : curMarginNum;
}
以上純原創!歡迎加關注、加收藏、點贊、轉發、分享(代碼閑聊站)~
*請認真填寫需求信息,我們會在24小時內與您取得聯系。