<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
如有侵犯您的版权,请及时联系3500663466#qq.com(#换@),我们将第一时间删除本站数据。
暂无评论内容