vue3 docxtemplater庫的 組件實現word導出,支持word圖片浮動插入
<script>
import { defineComponent, ref, watch, onMounted } from 'vue';
import Docxtemplater from 'docxtemplater';
import PizZip from 'pizzip';
import { saveAs } from 'file-saver';
import { renderAsync } from 'docx-preview';
import { Buffer } from 'buffer'
import ImageModule from 'docxtemplater-image-module-free';
//import { Document, Packer, Paragraph, ImageRun } from 'docx';
// 接口
import { getDetailStaff } from '@/api/info/staff'
import config from '@/config'
import JSZip from 'jszip';
const { fileDownload: fileDownloadUrl } = config.filestoreUrl
// 處理圖片數據
const loading = ref(false);
const imageCache = new Map();
const getImageBase64 = async (imageUrl) => {
if (imageCache.has(imageUrl)) {
return imageCache.get(imageUrl);
}
const response = await fetch(imageUrl);
const blob = await response.blob();
const base64 = await new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
imageCache.set(imageUrl, base64);
return base64;
};
// 優化的圖片獲取方法
const getImageBase64Sp = async (imageUrl) => {
// 1. 檢查緩存
if (imageCache.has(imageUrl)) {
return imageCache.get(imageUrl);
}
try {
let finalImageUrl = imageUrl;
// 調用異步API獲取簽名圖片路徑
const response = await getDetailStaff({ id: imageUrl });
// 確保數據存在
if (!response?.data?.data?.signature) {
throw new Error(`No signature found for id: ${imageUrl}`);
}
// 構建最終圖片URL
finalImageUrl = fileDownloadUrl + response.data.data.signature;
console.log('Resolved image URL:', finalImageUrl);
// 3. 獲取圖片數據
const base64 = await fetchAndConvertToBase64(finalImageUrl);
// 4. 緩存結果
imageCache.set(imageUrl, base64);
return base64;
} catch (error) {
console.error('Error in getImageBase64Sp:', error);
// 返回占位圖片或空字符串
return getPlaceholderImage();
}
};
// 輔助函數:獲取并轉換圖片為base64
const fetchAndConvertToBase64 = async (url) => {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
}
const blob = await response.blob();
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
};
// 輔助函數:獲取占位圖片
const getPlaceholderImage = () => {
// 返回一個透明1x1像素的占位圖
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
};
// 輔助函數:檢查是否是Base64字符串
const isBase64 = (str) => {
return /^data:image\/(png|jpeg|jpg|gif);base64,/.test(str);
};
// 默認字段映射配置
const defaultFieldMapping = {};
export default defineComponent({
name: 'DocxView',
props: {
modelData: {
type: Object,
required: true
},
templateUrl: {
type: String,
default: ''
},
fieldMapping: {
type: Object,
default: () => ({...defaultFieldMapping})
},
downloadFileName: {
type: String,
default: 'generated-doc.docx'
}
},
setup(props) {
const previewContainer = ref(null);
const fullscreenPreview = ref(null);
const templateData = ref(null);
const open = ref(false);
// 更新模板數據
const updateTemplateData =async () => {
try {
// 合并字段映射配置
const mapping = { ...defaultFieldMapping, ...props.fieldMapping };
// 處理所有包含Uuid的字段
const processUuidFields = async (obj) => {
for (const key in obj) {
if (key.endsWith('Uuid') && obj[key]) {
obj[key] = isBase64(obj[key])
? obj[key]
: await getImageBase64(fileDownloadUrl + obj[key]);
}
}
};
// 處理所有包含Seal的字段
const processSealFields = async (obj) => {
for (const key in obj) {
if (key.endsWith('Seal') && obj[key]) {
obj[key] = isBase64(obj[key])
? obj[key]
: await getImageBase64(fileDownloadUrl + obj[key]);
}
}
};
// 處理所有包含SignaturePic的字段
const processSignaturePicFields = async (obj) => {
for (const key in obj) {
if (key.endsWith('SignaturePic') && obj[key]) {
obj[key] = isBase64(obj[key])
? obj[key]
: await getImageBase64Sp(obj[key]);
}
}
};
// 處理所有ListJSON字段
const processListFields = async () => {
console.log('processListFields')
for (const key in mapping) {
if (key.endsWith('ListJSON') && Array.isArray(mapping[key])) {
mapping[key] = await Promise.all(
mapping[key].map(async (item) => {
const newItem = { ...item };
await processUuidFields(newItem);
return newItem;
})
);
}
}
};
// 處理所有ListJSONSign字段
const processListFieldsSign = async () => {
console.log('processListFields')
for (const key in mapping) {
if (key.endsWith('ListJSONSign') && Array.isArray(mapping[key])) {
mapping[key] = await Promise.all(
mapping[key].map(async (item) => {
const newItem = { ...item };
await processSignaturePicFields(newItem);
return newItem;
})
);
}
}
};
// 先處理普通字段中的Seal
await processSealFields(mapping);
// 先處理普通字段中的SignaturePic
await processSignaturePicFields(mapping);
// 先處理普通字段中的Uuid
await processUuidFields(mapping);
// 再處理ListJSON中的Uuid
await processListFields();
await processListFieldsSign();
templateData.value = mapping;
} catch (error) {
console.error('更新模板數據失敗:', error);
// 可以根據需要添加錯誤處理邏輯
}
};
// 加載模板文件
const loadTemplate = async () => {
const response = await fetch(props.templateUrl);
return await response.arrayBuffer();
};
// 生成文檔
const generateDoc = async (data) => {
try {
const template = await loadTemplate();
const zip = new PizZip(template);
const doc = new Docxtemplater(zip, {
modules: [
new ImageModule({
getImage: (tagValue,tagName) => {
console.log('getImage:', tagName)
// 處理Base64圖片
if (typeof tagValue === 'string' && tagValue.startsWith('data:')) {
const base64Data = tagValue.split(',')[1]
return Buffer.from(base64Data, 'base64')
}
return null;
},
getSize: (img, tagValue, tagName) => {
if (tagName.endsWith('SignaturePic')) {
return [80, 60];
}
else if(tagName.endsWith('Seal')){
// 默認尺寸(可選)
return [100, 100];
}
else {
// 默認尺寸(可選)
return [100, 100];
}
}
})
],
paragraphLoop: true,
linebreaks: true,
});
doc.render(data);
return doc.getZip().generate({
type: 'blob',
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
});
} catch (error) {
console.error('文檔生成失敗:', error);
throw error;
}
};
// 導出文檔
const xmlContent = ref("");
const images = ref([]);
const documentInfo = ref(null);
const processedDoc = ref(null);
const exportWord = async () => {
if (!templateData.value) return;
try {
// 生成文檔
const blob = await generateDoc(templateData.value);
// 2. 初始化docxtemplater
const templateArrayBuffer = await blob.arrayBuffer();
//
// 讀取原始docx的Blob,用JSZip解壓
if(blob){
const unzipData = await JSZip.loadAsync(templateArrayBuffer)
console.log('unzipData,',unzipData)
// 獲取document.xml
const documentXml = await unzipData.file('word/document.xml').async('text');
xmlContent.value = documentXml;
console.log('documentXml,',documentXml)
// 解析XML獲取圖片和替換文字
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(documentXml, 'text/xml');
// 查找所有包含替換文字的圖片元素
const drawings = xmlDoc.getElementsByTagName('w:drawing');
const foundImages = [];
for (let i = 0; i < drawings.length; i++) {
const drawing = drawings[i];
const docPr = drawing.getElementsByTagName('wp:docPr')[0];
if (docPr) {
const altText = docPr.getAttribute('descr') || docPr.getAttribute('title') || '';
const blip = drawing.getElementsByTagName('a:blip')[0];
if (blip) {
const embedId = blip.getAttribute('r:embed');
if (embedId) {
// 獲取圖片名稱
const rels = await unzipData.file('word/_rels/document.xml.rels').async('text');
const relsDoc = parser.parseFromString(rels, 'text/xml');
const relationships = relsDoc.getElementsByTagName('Relationship');
for (let j = 0; j < relationships.length; j++) {
const rel = relationships[j];
if (rel.getAttribute('Id') === embedId) {
const target = rel.getAttribute('Target');
console.log('target:',target)
const imgPath = `word/${target}`;
const imgFile = unzipData.file(imgPath);
if (imgFile) {
const blob = await imgFile.async('blob');
const preview = URL.createObjectURL(blob);
foundImages.push({
name: imgPath.split('/').pop(),
path: imgPath,
altText: altText,
preview: preview,
size: blob.size,
type: blob.type,
embedId: embedId,
replaced: false
});
}
break;
}
}
}
}
}
}
images.value = foundImages;
console.log('foundImages:',foundImages)
documentInfo.value = {
// name: file.name,
// size: file.size,
imageCount: images.value.length,
altTextCount: images.value.filter(img => img.altText).length
};
for (const img of images.value) {
console.log('replaceimg:',img)
// 重點,簽章替換,識別word帶替換文本的圖片(可用透明圖片來占位)
if (img.altText === 'zjzSeal') {
const arrayBuffer =base64ToArrayBuffer('data:image/png;base64,iVBORw0KG ')
}
}
// 生成新的DOCX文件
const content = await unzipData.generateAsync({ type: 'blob' });
processedDoc.value = content;
saveAs(processedDoc.value, props.downloadFileName);
}
} catch (error) {
console.error('導出失敗:', error);
}
};
function base64ToArrayBuffer(base64) {
// 移除 data URL 頭部(如果存在)
const base64Data = base64.split(',')[1] || base64;
// 解碼 Base64 字符串
const binaryString = atob(base64Data);
// 創建 ArrayBuffer 和視圖
const buffer = new ArrayBuffer(binaryString.length);
const uintArray = new Uint8Array(buffer);
// 填充二進制數據
for (let i = 0; i < binaryString.length; i++) {
uintArray[i] = binaryString.charCodeAt(i);
}
return buffer;
}
// 預覽文檔
const previewWord = async () => {
try {
if (!previewContainer.value) return;
loading.value = true;
const blob = await generateDoc(templateData.value);
await renderAsync(blob, previewContainer.value);
if(fullscreenPreview.value){
await renderAsync(blob, fullscreenPreview.value);
}
} catch (error) {
console.error('預覽生成失敗:', error);
} finally {
loading.value = false;
}
};
// 全屏預覽文檔
const previewWordFull = async () => {
try {
const blob = await generateDoc(templateData.value);
await renderAsync(blob, fullscreenPreview.value);
} catch (error) {
console.error('預覽生成失敗:', error);
} finally {
loading.value = false;
}
};
const onOpen = () =>{
previewWordFull()
open.value = true
}
// 初始化數據
updateTemplateData(props.modelData);
// 監聽modelData變化
watch(
() => props.modelData,
(newVal) => {
updateTemplateData(newVal);
previewWord();
previewWordFull()
},
{deep: true}
);
// 全屏高度相關
const modalRef = ref()
const dynamicHeight = ref('0px')
// 掛載后立即預覽
onMounted(async () => {
await updateTemplateData();
await previewWord();
loading.value = false;
});
return {
modalRef,
open,
onOpen,
previewContainer,
fullscreenPreview,
exportWord,
previewWord,
previewWordFull,
dynamicHeight
};
}
})
</script>
<template>
<div ref="detailContainerRef" class="detail-container">
<div class="top-container">
<Icon
class="top-container-icon"
type="ios-expand"
size="32"
title="全屏"
@click="onOpen">
</Icon>
</div>
<Teleport to="body">
<div v-if="open" ref="modalRef" class="modal">
<div class="modal-top">
<Button type="primary" @click="open = false">關閉</Button>
</div><div ref="fullscreenPreview" class="docx-preview-container full-screen"></div>
</div>
</Teleport>
<div ref="previewContainer" class="docx-preview-container"></div>
</div>
</template>
<style lang="less" scoped>
.modal {
position: fixed;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: white;
z-index: 9999;
padding: 20px;
&>.modal-top{
display: flex;
flex-direction: row-reverse;
}
}
.detail-container{
margin-top: 10px;
width: 100%;
min-height: 650px;
border: 1px solid #ddd;
&>.top-container{
display: flex;
flex-direction: row-reverse;
padding: 15px 15px 0 15px;
&>.top-container-icon{
cursor: pointer;
}
}
}
.docx-preview-container {
width: 100%;
min-height: 600px;
padding: 0 15px 15px 0;
margin-top: 15px;
box-sizing: border-box;
}
/* 適配docx預覽樣式 */
.docx-wrapper {
background: #fff !important;
padding: 20px !important;
}
.full-screen{
overflow: hidden;
overflow-y: scroll;
height: calc(100vh - 75px);
}
</style>

浙公網安備 33010602011771號