hiprint 打印预览正常,导出 Excel 报错

0 阅读3分钟

hiprint 打印预览正常,导出 Excel 报错

1. 设计模板

print-template.png

2. 预览效果

print-preview.png

3. 导出excel(电话和地址合并到一起)

export-excel.png

4. 配置表

config-json.png

5. 查看控制台发现报错了,合并重复了

console-error.png

地址的结束列6和电话的开始列4 没有错开,是不是改下这个列计算就可以。

 getTextColByLeft(leftVal, firstTableLeft, firstTableCols) {

        const relativeLeft = leftVal - firstTableLeft;
        if (relativeLeft < 0) return 1;
        if (firstTableCols.length === 0) return Math.max(1, Math.round(relativeLeft / 50));
        let accumulated = 0;
        for (let i = 0; i < firstTableCols.length; i++) {
          accumulated += firstTableCols[i];
          // console.log('accumulated:' + accumulated)
          // console.log('relativeLeft:' + relativeLeft)

          if (relativeLeft < accumulated) {
            return i+1;
          }
        }
        //左边超过表格 按照单元格宽度50换算
        return firstTableCols.length + Math.round((relativeLeft - accumulated) / 50)


      },

查了下这段列计算的逻辑:按照表格各列宽度累加,来判断文本的左边位置和右边位置分别落在表格的第几列。所以只要让文本元素的左右位置不再重叠,就不会再触发重复合并。理论上也可以在代码里加偏移计算来修正位置,但相比之下直接改设计模板更省事——把电话元素往右移,让它和地址错开。

template-fixed.png

然后导出再试下发现可以了。

export-result.png

预览导出代码

<template>
  <el-dialog class="preview_dialog" ref="previewDialog" title="快速预览" :show-close="false" @opened="open" :modal="false"
    :width="width + 'mm'" :visible.sync="previewDialogStatus">
    <div id="preview_content" ref="myComponent"></div>

    <span slot="footer" class="dialog-footer">
      <el-button @click.native="previewDialogStatus = false" size="mini">{{$t('form.cancel')}}</el-button>
      <el-button @click.native="printTemplate" size="mini" type="primary">{{$t('form.printBtn')}}</el-button>
      <el-button @click="captureImage" size="mini">生成图片</el-button>

      <el-button @click="exportToExcel1" size="mini">导出Excel(模板)</el-button>
      <el-button @click="exportToPdf" size="mini">转Pdf</el-button>
    </span>
  </el-dialog>

</template>

<script>
  import {
    buildHiprintTemplate
  } from './hiprint';
  import {
    getPrintModel,
    addPrintCount
  } from '@/api/hiprint/hiprint';

  import html2canvas from "html2canvas";
  import QRCode from 'qrcode';
  import JsBarcode from 'jsbarcode';
  import ExcelJS from 'exceljs';

  export default {
    name: "printPreview",
    props: {},
    data() {
      return {
        width: '250', //210
        previewDialogStatus: false,
        // 模板
        hiprintTemplate: {},
        // 数据
        printData: {},
        template: {}


      }
    },
    methods: {
      captureImage() {
        const element = this.$refs.myComponent;
        html2canvas(element).then((canvas) => {
          const image = canvas.toDataURL('image/png');
          const link = document.createElement('a');
          link.download = document.title + '.png'; // 图片名称
          link.href = image;
          link.click();
        });
      },
      exportToPdf() {
        this.hiprintTemplate.toPdf(this.printData, document.title + '.pdf');

      },

      async initImageToExcel(opts, textCol, row, worksheet, workbook, colSpan) {

        const imageId = workbook.addImage({
          base64: opts.src,
          extension: 'png'
        });

        const codeSize = {
          w: Math.max(opts.width, opts.height),
          h: Math.max(opts.width, opts.height)
        };

        // 计算目标列宽:如果跨多列,均分宽度;否则按图片宽度
        const targetColWidth = colSpan > 1 ? (codeSize.w / colSpan) + 10 : codeSize.w + 10;
        for (let i = 0; i < colSpan; i++) {
          worksheet.getColumn(textCol + i).width = targetColWidth;
        }

        worksheet.getRow(row).height = codeSize.h + 10;

        // 跨列时先合并单元格,再插入图片
        if (colSpan > 1) {
          worksheet.mergeCells(row, textCol, row, textCol + colSpan - 1);
        }

        // 计算单元格可用宽度(Excel 列宽单位 ≈ 像素/7,行高单位 = 像素)
        const cellWidthPx = targetColWidth * 7 * colSpan;
        const cellHeightPx = codeSize.h + 10;

        // 按比例缩放,以短边为限制
        let drawW = codeSize.w;
        let drawH = codeSize.h;
        const scale = Math.min(cellWidthPx / drawW, cellHeightPx / drawH, 1);
        if (scale < 1) {
          drawW = drawW * scale;
          drawH = drawH * scale;
        }

        // 水平居中偏移(像素转 Excel 列偏移:1 列宽 ≈ 7px)
        const offsetXPx = (cellWidthPx - drawW) / 2;
        const offsetCol = offsetXPx / 7 / targetColWidth;

        worksheet.addImage(imageId, {
          tl: {
            col: textCol - 1 + offsetCol,
            row: row - 1 + 0.05
          },
          ext: {
            width: drawW,
            height: drawH
          }
        });

      },
      async initQrCodeToExcel(opts, textCol, row, worksheet, workbook, colSpan) {
        let imageBase64 = await QRCode.toDataURL(opts.testData, {
          width: Math.round(opts.width * 2),
          margin: 1,
          errorCorrectionLevel: 'M'
        });
        const imageId = workbook.addImage({
          base64: imageBase64,
          extension: 'png'
        });
        const CODE_SCALE = 2;
        const codeSize = {
          w: Math.max(opts.width, opts.height) * CODE_SCALE,
          h: Math.max(opts.width, opts.height) * CODE_SCALE
        };
        const cellWidth = codeSize.w;
        const titleHeight = opts.title ? 20 : 0;
        const bottomPadding = 6;
        const rowHeight = codeSize.h + titleHeight + bottomPadding;
        worksheet.getRow(row).height = rowHeight;
        worksheet.getColumn(textCol).width = Math.max(worksheet.getColumn(textCol).width || 20, cellWidth /
          7.5);

        worksheet.addImage(imageId, {
          tl: {
            col: textCol - 1 + 0.9,
            row: row - 1 + 0.05
          },
          ext: {
            width: codeSize.w,
            height: codeSize.h
          }
        });

        if (opts.title) {
          const titleCell = worksheet.getRow(row).getCell(textCol);
          titleCell.value = opts.title;
          titleCell.font = {
            name: '宋体',
            size: opts.fontSize,
            bold: false
          };
          titleCell.alignment = {
            horizontal: 'center',
            vertical: 'bottom',
            wrapText: true
          };
        }
        if (colSpan > 1) {
          try {
            worksheet.mergeCells(row, textCol, row, textCol + colSpan - 1);
          } catch (e) {
            /* 跳过 */
          }
        }


      },
      async initBarCodeToExcel(opts, textCol, row, worksheet, workbook, colSpan) {
        const canvas = document.createElement('canvas');
        JsBarcode(canvas, opts.testData, {
          format: 'CODE128',
          width: 1,
          height: Math.round(opts.height),
          displayValue: false
        });
        let imageBase64 = canvas.toDataURL('image/png');
        const imageId = workbook.addImage({
          base64: imageBase64,
          extension: 'png'
        });
        const CODE_SCALE = 2;

        const codeSize = {
          w: opts.width * CODE_SCALE,
          h: opts.height * CODE_SCALE
        };

        const cellWidth = codeSize.w;
        const titleHeight = opts.title ? 20 : 0;
        const bottomPadding = 6;
        const rowHeight = codeSize.h + titleHeight + bottomPadding;
        worksheet.getRow(row).height = rowHeight;
        worksheet.getColumn(textCol).width = Math.max(worksheet.getColumn(textCol).width || 20, cellWidth /
          7.5);
        worksheet.addImage(imageId, {
          tl: {
            col: textCol - 1 + 0.25,
            row: row - 1 + 0.1
          },
          ext: {
            width: codeSize.w,
            height: codeSize.h
          }
        });
        // 文字放在同一单元格底部
        if (opts.title) {
          const titleCell = worksheet.getRow(row).getCell(textCol);
          titleCell.value = opts.title;
          titleCell.font = {
            name: '宋体',
            size: opts.fontSize,
            bold: false
          };
          titleCell.alignment = {
            horizontal: 'center',
            vertical: 'bottom',
            wrapText: true
          };
        }
        if (colSpan > 1) {
          try {
            worksheet.mergeCells(row, textCol, row, textCol + colSpan - 1);
          } catch (e) {
            /* 跳过 */
          }
        }

      },

      async initTextToExcel(textBeforeTable, worksheet, row, textDataSource, tableElements, workbook) {
        // 按 top 排序
        textBeforeTable.sort((a, b) => {
          const aTop = (a.options && a.options.top) || 0;
          const bTop = (b.options && b.options.top) || 0;
          return aTop - bTop;
        });




        // 获取第一个表格的列宽信息和起始 left,用于文本 left 换算列位置
        const firstTableCols = [];
        let firstTableLeft = 0;
        if (tableElements.length > 0) {
          const firstOpts = tableElements[0].options || {};
          firstTableLeft = firstOpts.left || 0;
          const firstColumns = firstOpts.columns[0] || [];
          console.log('firstColumns:', firstColumns)
          for (const col of firstColumns) {
            if (col.checked === false) continue;
            firstTableCols.push(col.width);
          }
        }
        console.log('firstTableCols:', firstTableCols)


        let rows = [];



        for (let i = 0; i < textBeforeTable.length; i++) {
          const top1 = textBeforeTable[i].options.top
          let cols = [textBeforeTable[i]];


          for (let j = i + 1; j < textBeforeTable.length; j++) {
            const top2 = textBeforeTable[j].options.top
            if (top2 - top1 <= 5) {
              cols.push(textBeforeTable[j])
              i = j;


            } else {
              break;
            }



          }
          rows.push(cols);



        }



        //console.log('rows:' + JSON.stringify(rows))

        // 处理表格前文本
        for (let i = 0; i < rows.length; i++) {
          let cols = rows[i];

          for (const textEl of cols) {
            const opts = textEl.options || {};
            const peType = (textEl.printElementType && textEl.printElementType.type) || '';
            const textField = (typeof opts.field === 'string' && opts.field) || '';
            const title = (opts.hideTitle && opts.hideTitle === true) ? '' : (opts.title || '');

            const fieldValue = (textField && textDataSource[textField] != null) ? textDataSource[textField] : '';
            let text = title ? (textField ? title + ':' + String(fieldValue) : title) : String(fieldValue);
            // 将 &nbsp; 替换为普通空格
            text = text.replace(/&nbsp;/gi, ' ');

            const textLeft = opts.left || 0;
            const textWidth = opts.width || 200;
            const textHeight = opts.height || 20;
            const fontSize = opts.fontSize || 10;
            const formatter = opts.formatter;
            const fontWeight = opts.fontWeight;
            const borderLeft = opts.borderLeft;
            const borderRight = opts.borderRight;
            const borderBottom = opts.borderBottom;
            const borderTop = opts.borderTop;
            const vCenter=opts.vCenter;
            const hCenter=opts.hCenter;


            const startCol = this.getTextColByLeft(textLeft, firstTableLeft, firstTableCols);
            const endCol = this.getTextColByLeft(textLeft + textWidth, firstTableLeft, firstTableCols);
            const colSpan = Math.max(1, endCol - startCol + 1);
            console.log('startCol:', startCol, 'endCol:', endCol, 'colSpan:', colSpan, 'text:', text, opts)

            // 二维码/条形码:图片在上文字在下,同一单元格
            if (peType === 'qrcode') {
              await this.initQrCodeToExcel(opts, startCol, row, worksheet, workbook, colSpan)

            } else if (peType === 'barcode') {
              await this.initBarCodeToExcel(opts, startCol, row, worksheet, workbook, colSpan)
            } else if (peType == 'image') {
              await this.initImageToExcel(opts, startCol, row, worksheet, workbook, colSpan)

            } else {

              if (colSpan > 1) {
                try {
                  let value = '';
                  for (let i = startCol; i <= endCol; i++) {
                    value = value + (worksheet.getRow(row).getCell(i).value ? worksheet.getRow(row).getCell(i).value : '')
                  }
                  worksheet.mergeCells(row, startCol, row, endCol);
                  worksheet.getRow(row).getCell(startCol).value = value;
                } catch (e) {

                }
              }

              const cell = worksheet.getRow(row).getCell(startCol);
              if (text.indexOf(':') > -1) {
                cell.value = text + (cell.value ? cell.value : '');
              } else {
                cell.value = (cell.value ? cell.value : '') + text;
              }
              if (formatter) {
                console.log('formatter:' + formatter)

                cell.value = undefined;
                const formattedValue = this.formatTextValue(formatter);
                console.log('formattedValue:' + formattedValue)

                cell.value = formattedValue != null ? formattedValue : '';
              }


              cell.font = {
                name: '宋体',
                size: fontSize,
                bold: fontWeight === 'bold' || fontWeight === '700' || opts.bold === true
              };
              cell.alignment = {
                horizontal: opts.textAlign || 'left',
                vertical: 'middle',
                wrapText: true
              };

              // 根据 opts 设置边框
              const borderStyle = {
                style: 'thin',
                color: {
                  argb: 'FF000000'
                }
              };
              cell.border = {
                left: borderLeft ? borderStyle : undefined,
                right: borderRight ? borderStyle : undefined,
                top: borderTop ? borderStyle : undefined,
                bottom: borderBottom ? borderStyle : undefined
              };
            }

          }
          row++



        }
        return row



      },
      getTextColByLeft(leftVal, firstTableLeft, firstTableCols) {

        const relativeLeft = leftVal - firstTableLeft;
        if (relativeLeft < 0) return 1;
        if (firstTableCols.length === 0) return Math.max(1, Math.round(relativeLeft / 50));
        let accumulated = 0;
        for (let i = 0; i < firstTableCols.length; i++) {
          accumulated += firstTableCols[i];
          // console.log('accumulated:' + accumulated)
          // console.log('relativeLeft:' + relativeLeft)

          if (relativeLeft < accumulated) {
            return i+1;
          }
        }
        //左边超过表格 按照单元格宽度50换算
        return firstTableCols.length + Math.round((relativeLeft - accumulated) / 50)


      },

      // 根据 template 配置导出表格到 Excel
      async exportToExcel1() {
        const template = this.template;

        console.log('=== exportToExcel1 ===:' + JSON.stringify(template));
        if (!template || !template.panels || template.panels.length === 0) {
          this.$message.warning('没有可导出的模板配置');
          return;
        }

        const workbook = new ExcelJS.Workbook();
        const sheetName = String(template.panels[0].name || 'Sheet1');
        const worksheet = workbook.addWorksheet(sheetName);
        const COL_WIDTH_RATIO = 5; // 列宽比例:col.width / COL_WIDTH_RATIO

        // 收集所有表格元素(保留原始索引用于排序)printElementType.type=='table'
        //导出有问题解决方案:表格用detailTable这个组件,禁用detailTable2,mainTable,剩下所有字段排版到表格前面或者后面
        const tableFields = ['detailTable', 'detailTable2', 'mainTable'];
        const tableElements = [];
        for (const panel of template.panels) {
          const elements = panel.printElements || [];
          for (const pe of elements) {
            const opts = pe.options || {};
            if (tableFields.includes(opts.field)) {
              tableElements.push(pe);
            }
          }
        }

        if (tableElements.length === 0) {
          this.$message.warning('未找到表格配置');
          return;
        }

        // 收集文本、二维码、条形码元素(type === 'text'/'qrcode'/'barcode'),根据 top 分为表格前和表格后
        const firstTableTop = tableElements[0].options ? (tableElements[0].options.top || 0) : 0;
        const textBeforeTable = [];
        const textAfterTable = [];
        for (const panel of template.panels) {
          const elements = panel.printElements || [];
          for (const pe of elements) {
            const peOpts = pe.options || {};
            const peType = (pe.printElementType && pe.printElementType.type) || '';
            if (peType === 'text' || peType === 'qrcode' || peType === 'barcode' || peType === 'image') {
              const textTop = peOpts.top || pe.top || 0;
              if (textTop < firstTableTop) {
                textBeforeTable.push(pe);
              } else {
                textAfterTable.push(pe);
              }
            }
          }
        }





        // 数据源(用于文本字段值替换)
        let item = this.printData && this.printData.mainTable && this.printData.mainTable[0] ? this.printData
          .mainTable[0] : {}

        const textDataSource = {
          ...item,
          ...this.printData,

        }


        let row = 1;
        row = await this.initTextToExcel(textBeforeTable, worksheet, row, textDataSource, tableElements, workbook);


        // 处理表格元素

        for (const tableEl of tableElements) {

          const opts = tableEl.options || {};
          const tableField = opts.field;
          let tableData = (this.printData && this.printData[tableField]) || [];
          const columns = (opts.columns && opts.columns[0]) || [];
          const footerFormatter = opts.footerFormatter || '';

          // 收集可见列
          const visibleCols = [];
          for (const col of columns) {
            if (col.checked === false) continue;
            visibleCols.push(col);
          }
          console.log('visibleCols:' + visibleCols.length)

          if (visibleCols.length === 0) continue;
          if (!Array.isArray(tableData) || tableData.length === 0) continue;



          // 判断是否显示表头(detailTable 显示表头,mainTable 不显示)
          const showHeader = (tableField === 'detailTable' || tableField === 'detailTable2') ? true : false;

          if (showHeader) {

            row++;
            const headerRow = worksheet.getRow(row);
            headerRow.height = 20;
            let colIdx = 1;
            for (const col of visibleCols) {
              const cell = headerRow.getCell(colIdx);
              cell.value = col.title || '';
              cell.font = {
                name: '宋体',
                size: 10,
                bold: true
              };
              cell.alignment = {
                horizontal: 'center',
                vertical: 'middle',
                wrapText: true
              };
              cell.border = this.getThinBorder();
              worksheet.getColumn(colIdx).width = col.width / COL_WIDTH_RATIO;
              colIdx++;
            }
            row++;
          }

          // 数据行
          tableData = tableData.filter(item => Object.keys(item).length > 0);
          for (let ri = 0; ri < tableData.length; ri++) {
            const rowData = tableData[ri];
            const dataRow = worksheet.getRow(row);
            dataRow.height = 18;
            let dc = 1;
            for (const col of visibleCols) {
              const cell = dataRow.getCell(dc);

              if (col.tableTextType === 'sequence') {
                cell.value = ri + 1;
              } else if (col.formatter2) {
                cell.value = this.formatCellValue2(col.formatter2, col.field, rowData);
              } else if (col.formatter) {
                cell.value = this.formatCellValue(col.formatter, col.field, rowData);
              } else {
                cell.value = rowData[col.field];
              }
              if (cell.value == null || cell.value === 'null') {
                cell.value = '';
              } else if (typeof cell.value === 'string') {
                cell.value = cell.value.replace(/null|undefined/g, '').trim();
              }

              cell.font = {
                name: '宋体',
                size: 9
              };
              if (tableField === 'mainTable') {
                cell.alignment = {
                  horizontal: col.halign || col.align || 'left',
                  vertical: 'middle',
                  wrapText: true
                };
              } else {
                cell.alignment = {
                  horizontal: col.halign || col.align || 'center',
                  vertical: 'middle',
                  wrapText: true
                };
              }
              cell.border = this.getThinBorder();
              dc++;
            }
            row++;
          }

          // 处理 rowsColumnsMerge
          const rowsColumnsMergeFn = opts.rowsColumnsMerge;
          if (rowsColumnsMergeFn && typeof rowsColumnsMergeFn === 'string') {
            try {
              const fnBody = rowsColumnsMergeFn.replace(/^function\s*\([^)]*\)\s*\{|\}$/g, '').trim();
              const mergeFn = new Function('data', 'col', 'colIndex', 'rowIndex', 'tableData', 'printData',
                '"use strict"; ' + fnBody);
              const dataStartRow = row - tableData.length;
              for (let ri = 0; ri < tableData.length; ri++) {
                const excelRow = dataStartRow + ri;
                let ci = 0;
                while (ci < visibleCols.length) {
                  const col = visibleCols[ci];
                  const result = mergeFn(tableData[ri], col, ci, ri, tableData, this.printData);
                  const rowspan = result ? (result[0] || 0) : 0;
                  const colspan = result ? (result[1] || 0) : 0;
                  if (rowspan > 1 || colspan > 1) {
                    try {
                      worksheet.mergeCells(excelRow, ci + 1, excelRow + rowspan - 1, ci + colspan);
                    } catch (e) {
                      /* 跳过 */
                    }
                  }
                  ci += Math.max(colspan, 1);
                }
              }
            } catch (e) {
              console.error('rowsColumnsMerge 处理失败:', e);
            }
          }

          // 合计行
          if (footerFormatter && typeof footerFormatter === 'string') {
            const footerHtml = this.extractFooterHtml(footerFormatter, tableData, visibleCols);
            if (footerHtml) {
              const footerRow = worksheet.getRow(row);
              footerRow.height = 20;
              const tdRegex = /<td[^>]*>([\s\S]*?)<\/td>/gi;
              let match;
              let fc = 1;
              const totalCols = visibleCols.length;
              while ((match = tdRegex.exec(footerHtml)) !== null && fc <= totalCols) {
                let tdContent = match[1].replace(/<[^>]+>/g, '').trim();
                const colspanMatch = match[0].match(/colspan\s*=\s*["']?(\d+)/i);
                const colspan = colspanMatch ? parseInt(colspanMatch[1]) : 1;
                if (colspan > 1 && fc + colspan - 1 <= totalCols) {
                  try {
                    worksheet.mergeCells(row, fc, row, fc + colspan - 1);
                  } catch (e) {
                    /* 跳过 */
                  }
                }
                const cell = footerRow.getCell(fc);
                cell.value = tdContent;
                cell.font = {
                  name: '宋体',
                  size: 10,
                  bold: true
                };
                cell.alignment = {
                  horizontal: 'center',
                  vertical: 'middle',
                  wrapText: true
                };
                cell.border = this.getThinBorder();
                fc += colspan;
              }
              while (fc <= totalCols) {
                footerRow.getCell(fc).border = this.getThinBorder();
                fc++;
              }
              row++;
            }
          }
        }

        row = await this.initTextToExcel(textAfterTable, worksheet, row, textDataSource, tableElements, workbook)



        // 下载
        try {
          const buffer = await workbook.xlsx.writeBuffer();
          const blob = new Blob([buffer], {
            type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
          });
          const link = document.createElement('a');
          link.href = URL.createObjectURL(blob);
          link.download = (template.modelName || document.title || '导出报表') + '.xlsx';
          link.click();
          URL.revokeObjectURL(link.href);
          this.$message.success('导出成功');
        } catch (error) {
          console.error('导出失败:', error);
          this.$message.error('导出失败: ' + error.message);
        }
      },
      // 从 footerFormatter 函数字符串中提取 HTML 内容
      extractFooterHtml(footerFormatter, tableData, columns) {
        try {
          // 构建执行上下文
          const rows = tableData || [];
          const getPrintTotal = (_options, _rows, title) => {
            const col = columns.find(c => c.title === title);
            if (!col) return 0;
            return rows.reduce((sum, r) => sum + (Number(r[col.field]) || 0), 0);
          };
          const getPrintTotalNotNull = (_options, _rows, title) => {
            const col = columns.find(c => c.title === title);
            if (!col) return '';
            const total = rows.reduce((sum, r) => sum + (Number(r[col.field]) || 0), 0);
            return total === 0 ? '' : String(total);
          };
          const getCnMoney = (num) => this.numberToChinese(num);

          // 执行函数体,提取 return 语句中的 HTML
          const trimmed = footerFormatter.trim();
          // 跳过前面可能存在的注释,找到 function 关键字的位置
          const fnStart = trimmed.search(/function\s*(?:\w+\s*)?\(/);
          if (fnStart === -1) return null;
          const fnBody = trimmed.slice(fnStart)
            .replace(/^function\s*(?:\w+\s*)?\([^)]*\)\s*\{/, '')
            .replace(/\}\s*$/, '')
            .trim();
          const fn = new Function('options', 'rows', 'data', 'pageData',
            'getPrintTotal', 'getPrintTotalNotNull', 'getCnMoney',
            '"use strict";' + fnBody);

          // 调用获取 HTML:pageData 默认用 rows(未分页时保持一致)
          const options = columns;
          const html = fn(options, rows, {}, rows, getPrintTotal, getPrintTotalNotNull, getCnMoney);

          // 提取 return 后面的内容
          if (typeof html === 'string') return html;

          // 如果函数最后是 return 语句,重新提取
          const returnMatch = fnBody.match(/return\s+(`[^`]*`|'[^']*'|"[^"]*")/s);
          if (returnMatch) {
            let ret = returnMatch[1];
            if (ret.startsWith('`')) ret = ret.slice(1, -1);
            else ret = ret.slice(1, -1);
            return ret;
          }
        } catch (e) {
          console.error('解析 footerFormatter 失败:', e);
        }
        return null;
      },
      // 数字转中文大写
      numberToChinese(num) {
        if (isNaN(num) || num === 0) return '零元整';
        const units = ['', '拾', '佰', '仟', '万', '拾', '佰', '仟', '亿'];
        const digits = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖'];
        const radices = ['', '角', '分'];

        let numStr = Math.abs(num).toFixed(2);
        let integerPart = parseInt(numStr.split('.')[0]);
        let decimalPart = numStr.split('.')[1];

        let result = '';
        let unitIndex = 0;
        let zeroFlag = false;

        while (integerPart > 0) {
          const digit = integerPart % 10;
          if (digit === 0) {
            if (!zeroFlag && result !== '') {
              result = '零' + result;
              zeroFlag = true;
            }
          } else {
            result = digits[digit] + units[unitIndex] + result;
            zeroFlag = false;
          }
          integerPart = Math.floor(integerPart / 10);
          unitIndex++;
        }

        result += '元';
        if (decimalPart === '00') {
          result += '整';
        } else {
          for (let i = 0; i < decimalPart.length; i++) {
            const d = parseInt(decimalPart[i]);
            if (d > 0) result += digits[d] + radices[i + 1];
          }
        }

        return result;
      },




      // 处理 formatter2(函数字符串格式)
      formatCellValue2(formatter2, field, rowData, index = 0, options = {}) {
        console.log('field', field)
        console.log('rowData', rowData)

        if (!formatter2) return rowData[field] != null ? rowData[field] : '';
        try {
          const value = rowData[field] != null ? rowData[field] : '';
          const row = rowData;
          const parseFloat = (v) => {
            const n = Number(v);
            return isNaN(n) ? 0 : n;
          };
          // 使用 Function 构造器创建函数并执行
          const fn = new Function('value', 'row', 'index', 'options', 'parseFloat',
            '"use strict"; return (' + formatter2 + ')');
          const func = fn(value, row, index, options, parseFloat);
          if (typeof func === 'function') {
            return func(value, row, index, options);
          }
          return func;
        } catch (e) {
          return rowData[field] != null ? String(rowData[field]) : '';
        }
      },



      // 格式化单元格值(处理 formatter)
      formatCellValue(formatter, field, rowData) {
        if (!formatter) return rowData[field] != null ? rowData[field] : '';
        try {
          // 构建安全的求值上下文
          const value = rowData[field] != null ? rowData[field] : '';
          const row = rowData;
          const parseFloat = (v) => {
            const n = Number(v);
            return isNaN(n) ? 0 : n;
          };
          // 使用 Function 构造器执行 formatter
          const fn = new Function('value', 'row', 'parseFloat', '"use strict"; return (' + formatter + ')');
          return fn(value, row, parseFloat);
        } catch (e) {
          // formatter 执行失败,回退到原始值
          return rowData[field] != null ? String(rowData[field]) : '';
        }
      },
      formatTextValue(formatter) {
        if (!formatter) return '';

        try {
          const fn = new Function('"use strict"; return (' + formatter + ')');
          const func = fn();
          if (typeof func === 'function') {
            return func();
          }
          return func;
        } catch (e) {
          return '';
        }
      },

      // 获取细线边框
      getThinBorder() {
        const borderStyle = {
          style: 'thin',
          color: {
            argb: 'FF000000'
          }
        };
        return {
          top: borderStyle,
          right: borderStyle,
          bottom: borderStyle,
          left: borderStyle
        };
      },






      show(hiprintTemplate, printData, width = this.width, template) {
        this.previewDialogStatus = true;
        this.width = width
        this.hiprintTemplate = hiprintTemplate
        this.printData = printData
        this.template = template

      },
      async preview(panelId, printData, width = this.width) {

        if (printData.billNo === '' && printData.mainTable && Array.isArray(printData.mainTable) && printData
          .mainTable.length > 0) {
          if (printData.mainTable[0].billNo) {
            printData.billNo = printData.mainTable[0].billNo;
          }

        }
        if (!panelId) {
          this.$message.warning("请先选择预览的模板")
          return
        }
        const option = {}
        let res = await getPrintModel(panelId);
        if (res.data.success && res.data.data) {
          option.template = JSON.parse(res.data.data.modelJson)
          console.log('template:' + JSON.stringify(option.template))
          this.template = option.template;
        }
        this.hiprintTemplate = buildHiprintTemplate(option);

        this.hiprintTemplate.design("#preview_content", false);
        this.show(this.hiprintTemplate, printData, width, this.template)
        //console.log('template:', this.hiprintTemplate)
      },
      open() {
        $("#preview_content").html(
          this.hiprintTemplate.getHtml(this.printData)
        );
      },
      printTemplate() {
        this.hiprintTemplate.print(this.printData);
        this.previewDialogStatus = false;
        //记录打印次数
        if (this.printData.id) {
          addPrintCount({
            sourceBillId: this.printData.id,
            tranType: this.printData.tranType,
          }).then(res => {
            if (res.data.success) {

            }
          })
        }

      },
      async print(panelId, printData) {
        if (!panelId) {
          this.$message.warning("请先选择预览的模板")
          return
        }
        const option = {}
        await getPrintModel(panelId)
          .then(res => {
            if (res.data.success && res.data.data) {
              option.template = JSON.parse(res.data.data.modelJson)
            }
          })
        this.hiprintTemplate = buildHiprintTemplate(option);
        this.hiprintTemplate.print2(printData)
      }
    }
  }
</script>
<style scoped>


</style>