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 = () => {   // 返回一个透明1×1像素的占位图   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>

来源链接:https://www.cnblogs.com/skytiger/p/18932562

© 版权声明
THE END
支持一下吧
点赞12 分享
评论 抢沙发
头像
请文明发言!
提交
头像

昵称

取消
昵称表情代码快捷回复

    暂无评论内容