<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      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>
      posted @ 2025-06-17 11:06  燒肉粽  閱讀(333)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 天天综合天天添夜夜添狠狠添| 超碰伊人久久大香线蕉综合| 九九热精品免费视频| 国产成人无码AV片在线观看不卡| 国产国语一级毛片| 18禁动漫一区二区三区| 日韩有码中文字幕av| 和静县| 自拍偷在线精品自拍偷99| 桃花岛亚洲成在人线AV| 色欲精品国产一区二区三区av| 蜜桃无码一区二区三区| 亚洲综合精品第一页| 久久99精品国产自在现线小黄鸭| 亚洲欧美中文字幕日韩一区二区| 91精品久久久久久无码人妻| 四虎永久免费很黄的视频| 我和亲妺妺乱的性视频| 99在线小视频| 思热99re视热频这里只精品| 色偷偷中文在线天堂中文| 中文字幕少妇人妻精品| 国产一区日韩二区欧美三区| 波多野结衣网站| 成人小说亚洲一区二区三区| √天堂中文在线最新版| 日本视频一两二两三区| 日韩在线观看 一区二区| 欧美日韩精品一区二区三区在线 | 国产色悠悠视频在线观看| 欧美日韩另类国产| 午夜大片免费男女爽爽影院| 成人午夜福利视频一区二区| 永久免费观看美女裸体的网站| 国产一区二区三区九九视频| 老色鬼永久精品网站| 亚洲老妇女一区二区三区| 日韩人妻无码一区二区三区| 蜜臀av性久久久久蜜臀aⅴ麻豆| 一本一道av无码中文字幕麻豆| 云梦县|