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号