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

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

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

      記錄---vue3項目實戰 打印、導出PDF

      ????? 寫在開頭

      點贊 + 收藏 === 學會??????

      vue3項目實戰 打印、導出PDF

      一 維護模板

      1 打印模板:

        <template>
          <div class="print-content">
            <div v-for="item in data.detailList" :key="item.id" class="label-item">
              <!-- 頂部價格區域 - 最醒目 -->
              <div class="price-header">
                <div class="main-price">
                  <span class="price-value">{{ formatPrice(item.detailPrice) }}</span>
                  <span class="currency">¥</span>
                </div>
                <div v-if="item.originalPrice && item.originalPrice !== item.detailPrice" class="origin-price">
                  原價 ¥{{ formatPrice(item.originalPrice) }}
                </div>
              </div>
        ?
              <!-- 商品信息區域 -->
              <div class="product-info">
                <div class="product-name">{{ truncateText(item.skuName, 20) }}</div>
                <div class="product-code">{{ item.skuCode || item.skuName.slice(-8) }}</div>
              </div>
        ?
              <!-- 條碼區域 -->
              <div class="barcode-section" v-if="item.showBarcode !== false">
                <img :src="item.skuCodeImg || '123456789'" alt="條碼" class="barcode" v-if="item.skuCode">
              </div>
        ?
              <!-- 底部信息區域 -->
              <div class="footer-info">
                <div class="info-row">
                  <span class="location">{{ item.location || "A1-02" }}</span>
                  <span class="stock">庫存{{ item.stock || 36 }}</span>
                </div>
              </div>
            </div>
          </div>
        </template>
        ?
        <script>
        export default {
          props: {
            data: {
              type: Object,
              required: true
            }
          },
          methods: {
            formatPrice(price) {
              return parseFloat(price || 0).toFixed(2);
            },
            truncateText(text, maxLength) {
              if (!text) return '';
              return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
            }
          }
        }
        </script>
        ?
        <style scoped lang="scss">
        /* 主容器 - 網格布局 */
        .print-content {
          display: grid;    /* 啟用 CSS Grid 布局 */
          grid-template-columns: repeat(auto-fill, 50mm); /* 每列寬 50mm,自動填充剩余空間 */
          grid-auto-rows: 30mm; /* 每行固定高度 30mm */
          background: #f5f5f5;  /* 網格背景色(淺灰色) */
        ?
          /* 單個標簽樣式 */
          .label-item {
            width: 50mm;
            height: 30mm;
            background: #ffffff;
            border-radius: 2mm;
            display: flex;
            flex-direction: column;
            position: relative;
            overflow: hidden;
            page-break-inside: avoid;
            font-family: 'OCR','ShareTechMono', 'Condensed','Liberation Mono','Microsoft YaHei', 'SimSun', 'Arial', monospace;
            box-shadow: none; /* 避免陰影被打印 */
        ?
            /* 價格頭部區域 - 最醒目 */
            .price-header {
              background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);
              color: white;
              padding: 1mm 2mm;
              text-align: center;
              position: relative;
        ?
              .main-price {
                display: flex;
                align-items: baseline;
                justify-content: center;
                line-height: 1;
        ?
                .currency {
                  color: #000 !important;
                  font-weight: bold;
                  margin-left: 2mm;
                }
        ?
                .price-value {
                  font-size: 16px;
                  font-weight: 900;
                  letter-spacing: -0.5px;
                  color: #000 !important;
                }
              }
        ?
              .origin-price {
                font-size: 6px;
                opacity: 0.8;
                text-decoration: line-through;
                margin-top: 0.5mm;
              }
        ?
              /* 特殊效果 - 價格角標 */
              &::after {
                content: '';
                position: absolute;
                bottom: -1mm;
                left: 50%;
                transform: translateX(-50%);
                width: 0;
                height: 0;
                border-left: 2mm solid transparent;
                border-right: 2mm solid transparent;
                border-top: 1mm solid #1976D2;
              }
            }
        ?
            /* 商品信息區域 */
            .product-info {
              padding: 1.5mm 2mm 1mm 2mm;
              flex: 1;
              display: flex;
              flex-direction: column;
              justify-content: center;
        ?
              .product-name {
                font-size: 10px;
                font-weight: 600;
                color: #000 !important;
                line-height: 1.2;
                text-align: center;
                margin-bottom: 0.5mm;
                overflow: hidden;
                display: -webkit-box;
                --webkit-line-clamp: 2;
                -webkit-box-orient: vertical;
              }
        ?
              .product-code {
                font-size: 8px;
                color: #000 !important;
                text-align: center;
                font-family: 'Courier New', monospace;
                letter-spacing: 0.3px;
              }
            }
        ?
            /* 條碼區域 */
            .barcode-section {
              padding: 0 1mm;
              text-align: center;
              height: 6mm;
              display: flex;
              align-items: center;
              justify-content: center;
        ?
              .barcode {
                height: 5mm;
                max-width: 46mm;
                object-fit: contain;
              }
            }
        ?
            /* 底部信息區域 */
            .footer-info {
              background: #f8f9fa;
              padding: 0.8mm 2mm;
              border-top: 0.5px solid #e0e0e0;
        ?
              .info-row {
                display: flex;
                justify-content: space-between;
                align-items: center;
        ?
                .location, .stock {
                  font-size: 5px;
                  color: #666;
                  font-weight: 500;
                }
        ?
                .location {
                  background: #e3f2fd;
                  color: #1976d2;
                  padding: 0.5mm 1mm;
                  border-radius: 1mm;
                  font-weight: 600;
                }
        ?
                .stock {
                  background: #f3e5f5;
                  color: #7b1fa2;
                  padding: 0.5mm 1mm;
                  border-radius: 1mm;
                  font-weight: 600;
                }
              }
            }
          }
        }
        ?
        /* 打印優化 */
        @media print {
          .price-header {
            /* 打印時使用模板顏色 */
            -webkit-print-color-adjust: exact;
            print-color-adjust: exact;
          }
        }
        ?
        </style>

      2 注意說明:

        1 注意:使用原生的標簽 + vue3響應式 ,不可以使用element-plus;
        2 @media print{} 用來維護打印樣式,最好在打印封裝中統一維護,否則交叉樣式會被覆蓋;

      二 封裝獲取模板

      1 模板設計

        // 1 模板類型:
          -- invoice-A4發票 ticket-80mm熱敏小票 label-貨架標簽
        // 2 模板寫死在前端,通過更新前端維護
          -- src/compoments/print/template/invoice/...
          -- src/compoments/print/template/ticket/...
          -- src/compoments/print/template/label/...
        // 3 通過 模板類型 templateType 、模板路徑 templatePath  -> 獲取唯一模板
          -- 前端實現模板獲取 

      2 封裝模板獲取

        
        // src/utils/print/templateLoader.js
        import { TEMPLATE_MAP } from '@/components/Print/templates';
        ?
        const templateCache = new Map();
        const MAX_CACHE_SIZE = 10; // 防止內存無限增長
        ?
        export async function loadTemplate(type, path, isFallback = false) {
          console.log('loadTemplate 進行模板加載:', type, path, isFallback);
          const cacheKey = `${type}/${path}`;
        ?
          // 檢查緩存
          if (templateCache.has(cacheKey)) {
            return templateCache.get(cacheKey);
          }
        ?
          try {
            // 檢查類型和路徑是否有效
            if (!TEMPLATE_MAP[type] || !TEMPLATE_MAP[type][path]) {
              throw new Error(`模板 ${type}/${path} 未注冊`);
            }
        ?
            // 動態加載模塊
            const module = await TEMPLATE_MAP[type][path]();
        ?
            // 清理最久未使用的緩存
            if (templateCache.size >= MAX_CACHE_SIZE) {
              // Map 的 keys() 是按插入順序的迭代器
              const oldestKey = templateCache.keys().next().value;
              templateCache.delete(oldestKey);
            }
        ?
            templateCache.set(cacheKey, module.default);
            return module.default;
          } catch (e) {
            console.error(`加載模板失敗: ${type}/${path}`, e);
        ?
            // 回退到默認模板
            if (isFallback || path === 'Default') {
              throw new Error(`無法加載模板 ${type}/${path} 且默認模板也不可用`);
            }
        ?
            return loadTemplate(type, 'Default', true);
          }
        }

      三 生成打印數據

      1 根據模板 + 打印數據 -> 生成 html(支持二維碼、條形碼)

        
        import JsBarcode from 'jsbarcode';
        import { createApp, h } from 'vue';
        import { isExternal } from "@/utils/validate";
        import QRCode from 'qrcode';
        // 1 生成條碼圖片
        function generateBarcodeBase64(code) {
          if (!code) return '';
          const canvas = document.createElement('canvas');
          try {
            JsBarcode(canvas, code, {
              format: 'CODE128',    // 條碼格式 CODE128、EAN13、EAN8、UPC、CODE39、ITF、MSI...
              displayValue: false,  // 是否顯示條碼值
              width: 2,             // 條碼寬度
              height: 40,           // 條碼高度  
              margin: 0,            // 條碼外邊距
            });
            return canvas.toDataURL('image/png');
          } catch (err) {
            console.warn('條碼生成失敗:', err);
            return '';
          }
        }
        ?
        // 2 拼接圖片路徑
        function getImageUrl(imgSrc) {
          if (!imgSrc) {
            return ''
          }
          try {
            const src = imgSrc.split(",")[0].trim();
            // 2.1 判斷圖片路徑是否為完整路徑
            return isExternal(src) ? src : `${import.meta.env.VITE_APP_BASE_API}${src}`;
          } catch (err) {
            console.warn('圖片路徑拼接失敗:', err);
            return '';
          }
        }
        ?
        // 更安全的QR碼生成
        async function generateQRCode(url) {
          if (!url) return '';
        ?
          try {
            return await QRCode.toDataURL(url.toString())
          } catch (err) {
            console.warn('QR碼生成失敗:', err);
            return '';
          }
        }
        ?
        /**
         * 3 打印模板渲染數據 
         * @param {*} Component  模板組件
         * @param {*} printData    打印數據  
         * @returns  html
         */
        export default async function renderTemplate(Component, printData) {
          // 1. 數據驗證和初始化
          if (!printData || typeof printData !== 'object') {
            throw new Error('Invalid data format');
          }
        ?
          // 2. 創建安全的數據副本
          const data = {
            ...printData,
            tenant: {
              ...printData.tenant,
              logo: printData?.tenant?.logo || '',
              logoImage: ''
            },
            invoice: {
              ...printData.invoice,
              invoiceQr: printData?.invoice?.invoiceQr || '',
              invoiceQrImage: ''
            },
            detailList: Array.isArray(printData.detailList) ? printData.detailList : [],
            invoiceDetailList: Array.isArray(printData.invoiceDetailList) ? printData.invoiceDetailList : [],
          };
        ?
          // 3. 異步處理二維碼和條碼和logo
          try {
            // 3.1 處理二維碼
            if (data.invoice.invoiceQr) {
              data.invoice.invoiceQrImage = await generateQRCode(data.invoice.invoiceQr);
            }
            // 3.2 處理條碼
            if (data.detailList.length > 0) {
              data.detailList = data.detailList.map(item => ({
                ...item,
                skuCodeImg: item.skuCode ? generateBarcodeBase64(item.skuCode) : ''
              }));
            }
            // 3.3 處理LOGO
            if (data.tenant.logo) {
              data.tenant.logoImage = getImageUrl(data.tenant?.logo);
            }
          } catch (err) {
            console.error('數據處理失敗:', err);
            // 即使部分數據處理失敗也繼續執行
          }
        ?
        ?
          // 4. 創建渲染容器
          const div = document.createElement('div');
          div.id = 'print-template-container';
        ?
          // 5. 使用Promise確保渲染完成
          return new Promise((resolve) => {
            const app = createApp({
              render: () => h(Component, { data })
            });
        ?
            // 6. 特殊處理:等待兩個tick確保渲染完成
            app.mount(div);
            nextTick().then(() => {
              return nextTick(); // 雙重確認
            }).then(() => {
              const html = div.innerHTML;
              app.unmount();
              div.remove();
              resolve(html);
            }).catch(err => {
              console.error('渲染失敗:', err);
              app.unmount();
              div.remove();
              resolve('<div>渲染失敗</div>');
            });
          });
        }

      四 封裝打印

        
        // src/utils/print/printHtml.js
        ?
        import { PrintTemplateType } from "@/views/print/printTemplate/printConstants";
        /**
         * 精準打印指定HTML(無瀏覽器默認頁眉頁腳)
         * @param {string} html - 要打印的HTML內容
         */
        export function printHtml(html, { templateType = PrintTemplateType.Invoice, templateWidth = 210, templateHeight = 297 }) {
        ?
          // 1 根據類型調整默認參數
          if (templateType === PrintTemplateType.Ticket) {
            templateWidth = 80; // 熱敏小票通常80mm寬
            templateHeight = 0; // 高度自動
          } else if (templateType === PrintTemplateType.Label) {
            templateWidth = templateWidth || 50; // 標簽打印機常見寬度50mm
            templateHeight = templateHeight || 30; // 標簽常見高度30mm
          }
        ?
          // 1. 創建打印專用容器
          const printContainer = document.createElement('div');
          printContainer.id = 'print-container';
          document.body.appendChild(printContainer);
        ?
          // 2. 注入打印控制樣式(隱藏頁眉頁腳)
          const style = document.createElement('style');
          style.innerHTML = `
            /* 打印頁面設置 */
            @page {
              margin: 0;  /* 去除頁邊距 */
              size: ${templateWidth}mm ${templateHeight === 0 ? 'auto' : `${templateHeight}mm`};  /* 自定義紙張尺寸 */
            }
            @media print {
              body, html {
                width: ${templateWidth}mm !important;
                margin: 0 !important;
                padding: 0 !important;
                background: #fff !important;  /* 強制白色背景 */
              }
              
              /* 隱藏頁面所有元素 */
              body * {
                visibility: hidden; 
              }
        ?
              /* 只顯示打印容器內容 */
              #print-container, #print-container * {
                visibility: visible;  
              }
        ?
              /* 打印容器定位 */
              #print-container {
                position: absolute;
                left: 0;
                top: 0;
                width: ${templateWidth}mm !important;
                ${templateHeight === 0 ? 'auto' : `height: ${templateHeight}mm !important;`}
                margin: 0 !important;
                padding: 0 !important;
                box-sizing: border-box;
                page-break-after: avoid;  /* 避免分頁 */
                page-break-inside: avoid;
              }
            }
        ?
            /* 屏幕預覽樣式 */
            #print-container {
              width: ${templateWidth}mm;
              ${templateHeight === 0 ? 'auto' : `height: ${templateHeight}mm;`}
              // margin: 10px auto;
              // padding: 5mm;
              box-shadow: 0 0 5px rgba(0,0,0,0.2);
              background: white;
            }
          `;
          document.head.appendChild(style);
        ?
          // 3. 放入要打印的內容
          printContainer.innerHTML = html;
        ?
          // 4. 觸發打印
          window.print();
        ?
          // 5. 清理(延遲確保打印完成)
          setTimeout(() => {
            document.body.removeChild(printContainer);
            document.head.removeChild(style);
          }, 1000);
        }

      五 封裝導出PDF

        
        // /src/utils/print/pdfExport.js
        ?
        import html2canvas from 'html2canvas';
        import { jsPDF } from 'jspdf';
        import { PrintTemplateType } from "@/views/print/printTemplate/printConstants";
        ?
        // 毫米轉像素的轉換系數 (96dpi下)
        const MM_TO_PX = 3.779527559;
        ?
        // 默認A4尺寸 (單位: mm)
        const DEFAULT_WIDTH = 210;
        const DEFAULT_HEIGHT = 297;
        ?
        export async function exportToPDF(html, {
          filename,
          templateType = PrintTemplateType.Invoice,
          templateWidth = DEFAULT_WIDTH,
          templateHeight = DEFAULT_HEIGHT,
          allowPaging = true
        }) {
          // 生成文件名
          const finalFilename = filename || `${templateType}_${Date.now()}.pdf`;
          // 處理寬度和高度,如果為0則使用默認值
          const widthMm = templateWidth === 0 ? DEFAULT_WIDTH : templateWidth;
          // 分頁模式使用A4高度,單頁模式自動高度
          const heightMm = templateHeight === 0 ? (allowPaging ? DEFAULT_HEIGHT : 'auto') : templateHeight;
        ?
          // 創建臨時容器
          const container = document.createElement('div');
          container.style.position = 'absolute';    // 使容器脫離正常文檔流
          container.style.left = '-9999px';         // 移出可視區域,避免在頁面上顯示
          container.style.width = `${widthMm}mm`;   // 容器寬度
          container.style.height = 'auto';          // 讓內容決定高度
          container.style.overflow = 'visible';     // 溢出部分不被裁剪
          container.innerHTML = html;               // 添加HTML內容
          document.body.appendChild(container);     // 將準備好的臨時容器添加到文檔中
        ?
          try {
            if (allowPaging) {
              console.log('導出PDF - 分頁處理模式');
              const pdf = new jsPDF({
                orientation: 'portrait',
                unit: 'mm',
                format: [widthMm, heightMm]
              });
        ?
              // 獲取所有頁面或使用容器作為單頁
              const pageElements = container.querySelectorAll('.page');
              const pages = pageElements.length > 0 ? pageElements : [container];
        ?
              for (let i = 0; i < pages.length; i++) {
                const page = pages[i];
                page.style.backgroundColor = 'white';
        ?
                // 計算頁面高度(像素)
                const pageHeightPx = page.scrollHeight;
                const pageHeightMm = pageHeightPx / MM_TO_PX;
        ?
                const canvas = await html2canvas(page, {
                  scale: 2,
                  useCORS: true,  // 啟用跨域訪問
                  backgroundColor: '#FFFFFF',
                  logging: true,
                  width: widthMm * MM_TO_PX,  // 畫布 寬度轉換成像素
                  height: pageHeightPx,       // 畫布 高度轉換成像素
                  windowWidth: widthMm * MM_TO_PX,    // 模擬視口 寬度轉換成像素
                  windowHeight: pageHeightPx          // 模擬視口 高度轉換成像素
                });
        ?
                const imgData = canvas.toDataURL('image/png');
                const imgWidth = widthMm;
                const imgHeight = (canvas.height * imgWidth) / canvas.width;
        ?
                if (i > 0) {
                  pdf.addPage([widthMm, heightMm], 'portrait');
                }
        ?
                pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight);
              }
        ?
              pdf.save(finalFilename);
            } else {
              console.log('導出PDF - 單頁處理模式');
              const canvas = await html2canvas(container, {
                scale: 2,
                useCORS: true,
                backgroundColor: '#FFFFFF',
                logging: true,
                width: widthMm * MM_TO_PX,
                height: container.scrollHeight,
                windowWidth: widthMm * MM_TO_PX,
                windowHeight: container.scrollHeight
              });
        ?
              const imgData = canvas.toDataURL('image/png');
              const imgWidth = widthMm;
              const imgHeight = (canvas.height * imgWidth) / canvas.width;
        ?
              const pdf = new jsPDF({
                orientation: imgWidth > imgHeight ? 'landscape' : 'portrait',
                unit: 'mm',
                format: [imgWidth, imgHeight]
              });
        ?
              pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight);
              pdf.save(finalFilename);
            }
          } catch (error) {
            console.error('PDF導出失敗:', error);
            throw error;
          } finally {
            document.body.removeChild(container);
          }
        }

      六 測試打印

      1 封裝打印預覽界面

        方便調試模板,此處就不提供預覽界面的代碼里,自己手搓吧!

      2 使用瀏覽器默認打印

        1 查看打印預覽,正常打印預覽與預期一樣;
        2 擦和看打印結果;

      3 注意事項

        1 涉及的模板尺寸 與 打印紙張的尺寸 要匹配;
          -- 否則預覽界面異常、打印結果異常;
        2 處理自動分頁,頁眉頁腳留夠空間,否則會覆蓋;
        3 有些打印機調試需要設置打印機的首選項,主要設置尺寸!

      七 問題解決

        // 1 打印預覽樣式與模板不一致
          -- 檢查 @media print{} 這里的樣式,
          -- 分別檢查模板 和 打印封裝;
          
        // 2 打印預覽異常、打印正常
          -- 問題原因:打印機紙張尺寸識別異常,即打印機當前設置的尺寸與模板尺寸不一致;
          -- 解決辦法:設置打印機 -> 首選項 -> 添加尺寸設置;
          
        // 3 打印機實測:
          -- 目前A4打印機、80熱敏打印機、標簽打印機 都有測試,沒有問題!
          -- 如果字體很丑,建議選擇等寬字體;
          -- 調節字體尺寸、顏色、盡可能美觀、節省紙張!
          
        // 4 進一步封裝
          -- 項目中可以進一步封裝打印,向所有流程封裝到一個service中,打印只需要傳遞 printData、templateType;
          -- 可以封裝批量打印;
          -- 模板可以根據用戶自定義配置,通過pinia維護狀態;
          
        // 5 后端來實現打印數據生成
          -- 我是前端能做的盡可能不放到后端處理,減少后端請求處理壓力!
          

      本文轉載于:https://juejin.cn/post/7521356618174021674

      如果對您有所幫助,歡迎您點個關注,我會定時更新技術文檔,大家一起討論學習,一起進步。

      posted @ 2025-08-20 17:25  林恒  閱讀(179)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 国产在线观看免费观看不卡| 亚洲欧美日韩在线不卡| 奇米影视7777久久精品| 国产欧美在线观看一区| 国产jizzjizz视频| 日本丰满熟妇videossex一| 午夜福利精品一区二区三区| 99久久精品费精品国产一区二| 日本不卡的一区二区三区| 日本久久99成人网站| 亚洲欧洲日产国码AV天堂偷窥| 人人澡人摸人人添| 亚洲综合小综合中文字幕| 成人av午夜在线观看| 国产精品毛片一区二区| 丰满人妻熟妇乱精品视频| 精品久久久久久无码中文野结衣| 久久亚洲国产五月综合网| 亚洲色大成网站WWW久久| 人妻日韩人妻中文字幕| 人妻无码中文专区久久app| 日韩精品一区二区三区中文无码| 蜜芽久久人人超碰爱香蕉 | 农村妇女野外一区二区视频| 久久精品国产亚洲av天海翼| 草裙社区精品视频播放| av激情亚洲男人的天堂| 国产免费无遮挡吸乳视频在线观看| 高潮迭起av乳颜射后入| 无码高潮爽到爆的喷水视频app | 亚洲国产成人AⅤ片在线观看| 性无码一区二区三区在线观看| 国内精品极品久久免费看| 中国老太婆video| 国产中文字幕在线一区| 国产乱码日产乱码精品精| 人妻久久久一区二区三区| 成人精品区| 博湖县| 国产一区在线播放av| 久爱无码精品免费视频在线观看 |