Эх сурвалжийг харах

feat: 添加出库单打印功能,整合打印组件并获取企业信息作为单据标题

xieyong 1 өдөр өмнө
parent
commit
edd7b1b9ee

+ 932 - 0
src/views/warehouseManagement/outgoingManagement/components/printDelivery.vue

@@ -0,0 +1,932 @@
+<template>
+  <ele-modal
+    title="出库单打印"
+    :visible.sync="QRvisible"
+    v-if="QRvisible"
+    width="90%"
+    @close="close"
+  >
+    <div :id="printSectionId" class="delivery-print" :style="qrStyleVars">
+      <!-- 单据主体:公司抬头 + 标题 + 二维码(二维码位置可配置) -->
+      <div class="dp-header">
+        <div class="dp-qrcode" :class="qrPosition" title="扫码查看单据内容">
+          <ele-qr-code-svg
+            class="dp-qr-svg"
+            :value="qrCodeText"
+            :size="qrSize"
+            :level="qrLevel"
+            :margin="2"
+          />
+          <div class="dp-qrcode-tip">扫码查看单据</div>
+        </div>
+        <div class="dp-title-box">
+          <div class="dp-company">{{ companyName }}{{ titleText }}</div>
+        </div>
+      </div>
+
+      <!-- 单据信息 -->
+      <table class="dp-table dp-info-table" cellspacing="0" cellpacing="0">
+        <tbody>
+          <tr>
+            <td class="dp-info-label">客户名称:</td>
+            <td class="dp-info-value" style="width: 34%">
+              {{ formData.clientName || '' }}
+            </td>
+            <td class="dp-info-label" style="width: 15%">发货单号:</td>
+            <td class="dp-info-value">{{ formData.bizNo || '' }}</td>
+          </tr>
+          <tr>
+            <td class="dp-info-label">发货日期:</td>
+            <td class="dp-info-value">
+              {{ deliveryDate || '' }}
+            </td>
+            <td class="dp-info-label">联系人:</td>
+            <td class="dp-info-value">
+              {{ formData.clientUser || '' }}
+              <span class="dp-inline">联系电话:{{ formData.clientPhone || '' }}</span>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+
+      <!-- 明细 -->
+      <table class="dp-table dp-detail-table" cellspacing="0" cellpacing="0">
+        <thead>
+          <tr>
+            <th style="width: 5%">序号</th>
+            <th style="width: 16%">型号</th>
+            <th style="width: 21%">零件名</th>
+            <th style="width: 7%">单位</th>
+            <th style="width: 9%">送货数量</th>
+            <th style="width: 9%">验收数量</th>
+            <th style="width: 10%">备注</th>
+            <th style="width: 15%">订单号</th>
+            <th style="width: 8%">加工工艺</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="(item, index) in codeList" :key="index">
+            <td class="dp-td-center">{{ index + 1 }}</td>
+            <td class="dp-td-center">{{ item.categoryModel || '' }}</td>
+            <td class="dp-td-center">{{ item.categoryName || '' }}</td>
+            <td class="dp-td-center">{{ item.measureUnit || '' }}</td>
+            <td class="dp-td-center">{{ item.measureQuantity ?? '' }}</td>
+            <td class="dp-td-center">{{ item.acceptQuantity ?? '' }}</td>
+            <td class="dp-td-center">{{ item.remark || '' }}</td>
+            <td class="dp-td-center">{{ item.sourceBizNo || formData.sourceBizNo || '' }}</td>
+            <td class="dp-td-center">{{ item.process || '' }}</td>
+          </tr>
+          <!-- 合计 -->
+          <tr class="dp-sum-row">
+            <td></td>
+            <td colspan="2" class="dp-td-center">合计</td>
+            <td class="dp-td-center">{{ totalUnit }}</td>
+            <td class="dp-td-center">{{ totalQuantity }}</td>
+            <td class="dp-td-center">{{ totalAcceptQuantity }}</td>
+            <td colspan="3"></td>
+          </tr>
+        </tbody>
+      </table>
+
+      <!-- 签收 -->
+      <table class="dp-table dp-sign-table" cellspacing="0" cellpacing="0">
+        <tbody>
+          <tr>
+            <td style="width: 34%">发货人:{{ senderName }}</td>
+            <td style="width: 33%">送货人:{{ deliveryName }}</td>
+            <td>收货人:{{ receiverName }}</td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+
+    <div slot="footer">
+      <el-button icon="el-icon-download" type="primary" @click="exportExcel"
+        >导出Excel</el-button
+      >
+      <el-button @click="print">打印预览</el-button>
+      <el-button @click="close">关闭</el-button>
+    </div>
+  </ele-modal>
+</template>
+
+<script>
+  import { mapGetters } from 'vuex';
+  import EleQrCodeSvg from 'ele-admin/es/ele-qr-code-svg';
+  import { saveAs } from 'file-saver';
+  import QRCode from 'qrcode';
+  import storageApi from '@/api/warehouseManagement';
+
+  // 单据标题,由外部传入
+  const DOC_TITLE = '出库单';
+
+  export default {
+    name: 'printDelivery',
+    components: {
+      EleQrCodeSvg
+    },
+    props: {
+      // 公司/集团名称(标题),由外部传入
+      groupName: {
+        type: String,
+        default: ''
+      },
+      // 二维码位置:left / right
+      qrPosition: {
+        type: String,
+        default: 'left'
+      },
+      // 二维码内容模式:
+      // detail(默认) = 单据明细文本,扫码直接看到内容
+      // link = 单据详情页链接
+      // id = 仅单据标识参数
+      qrContentMode: {
+        type: String,
+        default: 'detail'
+      },
+      // 扫码文本排版:plain = 空格对齐(默认,省容量),box = 框线表格
+      qrTextStyle: {
+        type: String,
+        default: 'plain'
+      },
+      // 二维码尺寸:弹框预览与打印预览保持一致
+      qrSize: {
+        type: Number,
+        default: 120
+      },
+      // 纠错等级:L 容错最低但容量最大,模块更少、打印后更易识别
+      qrLevel: {
+        type: String,
+        default: 'L'
+      },
+      // 二维码内容字节上限,超出自动截断明细,避免码点过密扫不出
+      qrMaxBytes: {
+        type: Number,
+        default: 1100
+      }
+    },
+    computed: {
+      ...mapGetters(['user']),
+      companyName() {
+        return this.groupName;
+      },
+      titleText() {
+        return DOC_TITLE;
+      },
+      printSectionId() {
+        return 'deliveryPrintSection';
+      },
+      // 二维码尺寸变量:写在根节点上,弹框与打印预览共用同一个值
+      qrStyleVars() {
+        return {
+          '--qr-size': `${this.qrSize}px`,
+          '--qr-box': `${this.qrSize + 8}px`,
+          '--qr-header': `${this.qrSize + 12}px`
+        };
+      },
+      // 二维码内容:扫码后可直接看到本单据内容
+      qrCodeText() {
+        if (this.qrContentMode === 'link') {
+          const path = '/warehouseManagement/outgoingManagement/details';
+          return `${window.location.origin}${path}?id=${
+            this.formData.id ?? ''
+          }&bizNo=${this.formData.bizNo ?? ''}`;
+        }
+        if (this.qrContentMode === 'id') {
+          const params = new URLSearchParams({
+            inoutType: 'out',
+            outId: this.formData.id ?? '',
+            bizNo: this.formData.bizNo ?? ''
+          });
+          return params.toString();
+        }
+        if (this.qrTextStyle === 'box') {
+          return this.buildQrBoxText();
+        }
+        if (this.qrTextStyle === 'tab') {
+          return this.buildQrDetailText();
+        }
+        return this.buildQrPlainText();
+      },
+      deliveryDate() {
+        const time = this.formData.storageTime || this.formData.createTime;
+        return this.formatDate(time);
+      },
+      // 明细合计
+      totalQuantity() {
+        return this.sumField('measureQuantity');
+      },
+      totalAcceptQuantity() {
+        return this.sumField('acceptQuantity');
+      },
+      totalUnit() {
+        const units = [
+          ...new Set(this.codeList.map((it) => it.measureUnit).filter(Boolean))
+        ];
+        return units.join('/');
+      },
+      senderName() {
+        return this.formData.fromUser || this.user?.info?.name || '';
+      },
+      deliveryName() {
+        return this.formData.extInfo?.createUserName || this.user?.info?.name || '';
+      },
+      receiverName() {
+        return this.formData.clientUser || '';
+      }
+    },
+    data() {
+      return {
+        QRvisible: false,
+        codeList: [],
+        formData: {}
+      };
+    },
+
+    methods: {
+      // 组装扫码内容:空格对齐的表格(等宽显示下即一张单据)
+      buildQrPlainText() {
+        const f = this.formData;
+
+        // 抬头信息,标签列对齐
+        const info = [
+          ['客户名称', f.clientName],
+          ['发货单号', f.bizNo],
+          ['发货日期', this.deliveryDate],
+          ['联系人', f.clientUser],
+          ['联系电话', f.clientPhone],
+          ['发货人', this.senderName],
+          ['送货人', this.deliveryName],
+          ['收货人', this.receiverName]
+        ].filter(([, v]) => v !== '' && v != null);
+        const labelW = Math.max(4, ...info.map(([k]) => this.dispWidth(k)));
+        const head = [this.companyName, this.titleText].filter(Boolean);
+        info.forEach(([k, v]) => {
+          head.push(`${this.pad(k, labelW)}  ${v}`);
+        });
+        if (this.totalQuantity !== '') {
+          head.push(
+            `${this.pad('合计', labelW)}  ${this.totalQuantity}${this.totalUnit}`
+          );
+        }
+
+        // 明细列,max 为最大显示宽度
+        const allCols = [
+          { label: '序号', get: (it, i) => String(i + 1), max: 4 },
+          { label: '型号', get: (it) => it.categoryModel, max: 14 },
+          { label: '零件名', get: (it) => it.categoryName, max: 18 },
+          { label: '单位', get: (it) => it.measureUnit, max: 4 },
+          { label: '送货数量', get: (it) => it.measureQuantity, max: 8 },
+          { label: '验收数量', get: (it) => it.acceptQuantity, max: 8 },
+          { label: '备注', get: (it) => it.remark, max: 10 },
+          {
+            label: '订单号',
+            get: (it) => it.sourceBizNo || f.sourceBizNo,
+            max: 16
+          },
+          { label: '加工工艺', get: (it) => it.process, max: 8 }
+        ];
+        // 整列为空的列不输出,省下二维码容量(序号列恒保留)
+        const cols = allCols.filter(
+          (c, idx) =>
+            idx === 0 ||
+            this.codeList.some((it) => String(c.get(it, 0) ?? '') !== '')
+        );
+        const rows = this.codeList.map((it, i) =>
+          cols.map((c) => String(c.get(it, i) ?? ''))
+        );
+        const widths = cols.map((c, idx) =>
+          Math.min(
+            c.max,
+            Math.max(
+              this.dispWidth(c.label),
+              ...rows.map((r) => this.dispWidth(r[idx]))
+            )
+          )
+        );
+        const GAP = '  ';
+        const renderRow = (cells) =>
+          cells.map((c, i) => this.pad(c, widths[i])).join(GAP).trimEnd();
+        const sepLine = widths.map((w) => '-'.repeat(w)).join(GAP);
+
+        const headerLine = renderRow(cols.map((c) => c.label));
+        const rowLines = rows.map(renderRow);
+
+        // 按字节预算装入明细行,表头与合计始终保留
+        const noteTemplate = (kept) =>
+          `(明细共${rows.length}条,此处仅显示前${kept}条,完整内容请查系统)`;
+        const fixed = this.utf8Bytes(
+          [...head, headerLine, sepLine, noteTemplate(0), ''].join('\n')
+        );
+        let budget = Math.max(this.qrMaxBytes - fixed, 0);
+        const keptRows = [];
+        rowLines.forEach((line) => {
+          const cost = this.utf8Bytes(line) + 1;
+          if (cost <= budget) {
+            keptRows.push(line);
+            budget -= cost;
+          }
+        });
+
+        const lines = [...head, headerLine, sepLine, ...keptRows];
+        if (keptRows.length < rowLines.length) {
+          lines.push(noteTemplate(keptRows.length));
+        }
+        return lines.join('\n');
+      },
+      // 组装扫码内容:框线表格,等宽显示下看起来就是一张单据
+      buildQrBoxText() {
+        const f = this.formData;
+
+        // 抬头信息,标签列对齐
+        const info = [
+          ['客户名称', f.clientName],
+          ['发货单号', f.bizNo],
+          ['发货日期', this.deliveryDate],
+          ['联系人', f.clientUser],
+          ['联系电话', f.clientPhone],
+          ['发货人', this.senderName],
+          ['送货人', this.deliveryName],
+          ['收货人', this.receiverName]
+        ].filter(([, v]) => v !== '' && v != null && v !== undefined);
+        const labelW = Math.max(4, ...info.map(([k]) => this.dispWidth(k)));
+        const head = [this.companyName, this.titleText].filter(Boolean);
+        info.forEach(([k, v]) => {
+          head.push(`${this.pad(k, labelW)}  ${v}`);
+        });
+        if (this.totalQuantity !== '') {
+          head.push(`${this.pad('合计', labelW)}  ${this.totalQuantity}${this.totalUnit}`);
+        }
+
+        // 明细列:[表头, 取值, 最大显示宽度]
+        const columns = [
+          ['序号', (it, i) => String(i + 1), 4],
+          ['型号', (it) => it.categoryModel, 15],
+          ['零件名', (it) => it.categoryName, 20],
+          ['单位', (it) => it.measureUnit, 4],
+          ['送货数量', (it) => it.measureQuantity, 8],
+          ['验收数量', (it) => it.acceptQuantity, 8],
+          ['备注', (it) => it.remark, 9],
+          ['订单号', (it) => it.sourceBizNo || f.sourceBizNo, 15],
+          ['加工工艺', (it) => it.process, 8]
+        ];
+        const rows = this.codeList.map((it, i) =>
+          columns.map(([, get]) => get(it, i) ?? '')
+        );
+        // 每列宽度:表头与内容取最大值,受最大宽度限制
+        const widths = columns.map(([label, , max], idx) =>
+          Math.min(
+            max,
+            Math.max(
+              this.dispWidth(label),
+              ...rows.map((r) => this.dispWidth(String(r[idx] ?? '')))
+            )
+          )
+        );
+
+        const border = (l, m, r) =>
+          l + widths.map((w) => '─'.repeat(w + 2)).join(m) + r;
+        const renderRow = (cells) =>
+          '│' +
+          cells
+            .map((c, i) => ` ${this.pad(String(c ?? ''), widths[i])} `)
+            .join('│') +
+          '│';
+
+        const top = border('┌', '┬', '┐');
+        const mid = border('├', '┼', '┤');
+        const bottom = border('└', '┴', '┘');
+        const headRow = renderRow(columns.map(([label]) => label));
+
+        // 按字节预算装入明细行,表头/合计始终保留
+        const noteTemplate = (kept) =>
+          `(明细共${rows.length}条,此处仅显示前${kept}条,完整内容请查系统)`;
+        const fixed = this.utf8Bytes(
+          [...head, top, headRow, mid, bottom, noteTemplate(0), ''].join('\n')
+        );
+        let budget = Math.max(this.qrMaxBytes - fixed, 0);
+        const keptRows = [];
+        rows.forEach((r) => {
+          const line = renderRow(r);
+          const cost = this.utf8Bytes(line) + this.utf8Bytes('\n├─┼─┤\n');
+          if (cost <= budget) {
+            keptRows.push(line);
+            budget -= cost;
+          }
+        });
+
+        const lines = [...head, top, headRow, mid, ...keptRows];
+        if (keptRows.length < rows.length) {
+          lines.push(noteTemplate(keptRows.length));
+        }
+        lines.push(bottom);
+        return lines.join('\n');
+      },
+      // 字符显示宽度:中文/全角按 2 列计算
+      dispWidth(str) {
+        let width = 0;
+        for (const ch of String(str ?? '')) {
+          const code = ch.codePointAt(0);
+          const wide =
+            (code >= 0x1100 && code <= 0x115f) ||
+            code === 0x2329 ||
+            code === 0x232a ||
+            (code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) ||
+            (code >= 0xac00 && code <= 0xd7a3) ||
+            (code >= 0xf900 && code <= 0xfaff) ||
+            (code >= 0xfe30 && code <= 0xfe6f) ||
+            (code >= 0xff00 && code <= 0xff60) ||
+            (code >= 0xffe0 && code <= 0xffe6) ||
+            (code >= 0x20000 && code <= 0x3fffd);
+          width += wide ? 2 : 1;
+        }
+        return width;
+      },
+      // 按显示宽度补齐,超宽截断加省略号
+      pad(str, width) {
+        const s = String(str ?? '');
+        if (this.dispWidth(s) <= width) {
+          return s + ' '.repeat(width - this.dispWidth(s));
+        }
+        let out = '';
+        let used = 0;
+        for (const ch of s) {
+          const w = this.dispWidth(ch);
+          if (used + w > width - 1) break;
+          out += ch;
+          used += w;
+        }
+        return out + '…' + ' '.repeat(Math.max(width - used - 1, 0));
+      },
+      // 组装扫码可见的单据内容(与弹框单据内容一致,去掉二维码本身)
+      buildQrDetailText() {
+        const f = this.formData;
+        // 单元格之间用 Tab 分隔,扫码后仍是一张可读的表
+        const TAB = '\t';
+        const cells = (list) => list.filter((v) => v !== '' && v != null).join(TAB);
+
+        const head = [
+          this.companyName,
+          this.titleText,
+          cells([
+            `客户名称:${f.clientName ?? ''}`,
+            `发货单号:${f.bizNo ?? ''}`
+          ]),
+          cells([
+            `发货日期:${this.deliveryDate}`,
+            `联系人:${f.clientUser ?? ''}`,
+            `联系电话:${f.clientPhone ?? ''}`
+          ]),
+          cells([
+            '序号',
+            '型号',
+            '零件名',
+            '单位',
+            '送货数量',
+            '验收数量',
+            '备注',
+            '订单号',
+            '加工工艺'
+          ])
+        ];
+
+        // 明细行:列顺序与弹框表格一致
+        const itemLines = this.codeList.map((item, index) =>
+          cells([
+            String(index + 1),
+            item.categoryModel ?? '',
+            item.categoryName ?? '',
+            item.measureUnit ?? '',
+            item.measureQuantity ?? '',
+            item.acceptQuantity ?? '',
+            item.remark ?? '',
+            item.sourceBizNo || f.sourceBizNo || '',
+            item.process ?? ''
+          ])
+        );
+
+        const foot = [];
+        if (this.totalQuantity !== '') {
+          foot.push(cells(['合计', this.totalUnit, this.totalQuantity]));
+        }
+        foot.push(
+          cells([
+            `发货人:${this.senderName}`,
+            `送货人:${this.deliveryName}`,
+            `收货人:${this.receiverName}`
+          ])
+        );
+        foot.push(`单据ID:${f.id ?? ''}`);
+
+        // 按字节预算装入明细,装不下则截断并提示(表头/合计/签收始终保留)
+        const noteTemplate = (kept) =>
+          `(明细共${itemLines.length}条,此处仅显示前${kept}条,完整内容请查系统)`;
+        const fixedBytes = this.utf8Bytes(
+          [...head, ...foot, noteTemplate(0)].join('\n')
+        );
+        let budget = Math.max(this.qrMaxBytes - fixedBytes, 0);
+        const kept = [];
+        itemLines.forEach((line) => {
+          const cost = this.utf8Bytes(line) + 1;
+          if (cost <= budget) {
+            kept.push(line);
+            budget -= cost;
+          }
+        });
+        const lines = [...head, ...kept];
+        if (kept.length < itemLines.length) {
+          lines.push(noteTemplate(kept.length));
+        }
+        lines.push(...foot);
+        return lines.join('\n');
+      },
+      // 计算 UTF-8 字节数(二维码按字节编码,中文占 3 字节)
+      utf8Bytes(str) {
+        let bytes = 0;
+        for (let i = 0; i < str.length; i++) {
+          const code = str.charCodeAt(i);
+          if (code < 0x80) {
+            bytes += 1;
+          } else if (code < 0x800) {
+            bytes += 2;
+          } else if (code >= 0xd800 && code <= 0xdbff) {
+            bytes += 4;
+            i++;
+          } else {
+            bytes += 3;
+          }
+        }
+        return bytes;
+      },
+      // 导出 Excel:用带内联样式的表格,完整保留边框/字体/加粗/合并
+      async exportExcel() {
+        if (!this.codeList.length) {
+          return this.$message.warning('暂无可导出的明细数据!');
+        }
+        const f = this.formData;
+        // 二维码转成 PNG,嵌入导出文件
+        let qrImg = '';
+        try {
+          const dataUrl = await QRCode.toDataURL(this.qrCodeText, {
+            width: 240,
+            margin: 2,
+            errorCorrectionLevel: this.qrLevel
+          });
+          qrImg = `<img src="${dataUrl}" width="84" height="84" alt="二维码" />`;
+        } catch (e) {
+          qrImg = '<span>二维码</span>';
+        }
+        const border =
+          'border-top:1px solid #000;border-right:1px solid #000;border-bottom:1px solid #000;border-left:1px solid #000;';
+        const cellBase = `${border}padding:4px;mso-number-format:"\\@";vertical-align:middle;text-align:center;mso-horizontal-align:center;`;
+        const th = `${cellBase}font-weight:bold;font-family:'宋体';font-size:12pt;`;
+        const td = `${cellBase}font-family:'宋体';font-size:11pt;`;
+        const tdBold = `${td}font-weight:bold;`;
+        const labelLeft = `${border}padding:4px;mso-number-format:"\\@";vertical-align:middle;text-align:left;mso-horizontal-align:left;font-family:'宋体';font-size:11pt;`;
+
+        // 列宽(单位 pt):序号 型号 零件名 单位 送货数量 验收数量 备注 订单号 加工工艺
+        const colWidths = [45, 130, 190, 45, 70, 70, 75, 115, 65];
+        const totalCols = colWidths.length;
+        const colGroup = colWidths
+          .map((w) => `<col width="${w}" style="width:${w}pt;" />`)
+          .join('');
+
+        
+
+        const title = `${this.groupName || ''}${this.titleText.replace(
+          /\s+/g,
+          ''
+        )}`;
+
+        const rows = this.codeList
+          .map((item, index) => {
+            return `
+              <tr height="24" style="height:24pt;">
+                <td align="center" valign="middle" style="${td}">${index +
+                  1}</td>
+                <td align="center" valign="middle" style="${td}">${this.esc(
+              item.categoryModel
+            )}</td>
+                <td align="center" valign="middle" style="${td}">${this.esc(
+              item.categoryName
+            )}</td>
+                <td align="center" valign="middle" style="${td}">${this.esc(
+              item.measureUnit
+            )}</td>
+                <td align="center" valign="middle" style="${td}">${this.esc(
+              item.measureQuantity
+            )}</td>
+                <td align="center" valign="middle" style="${td}">${this.esc(
+              item.acceptQuantity
+            )}</td>
+                <td align="center" valign="middle" style="${td}">${this.esc(
+              item.remark
+            )}</td>
+                <td align="center" valign="middle" style="${td}">${this.esc(
+              item.sourceBizNo || f.sourceBizNo
+            )}</td>
+                <td align="center" valign="middle" style="${td}">${this.esc(
+              item.process
+            )}</td>
+              </tr>`;
+          })
+          .join('');
+
+        const table = `
+          <table border="1" cellspacing="0" cellpadding="0" align="center"
+            style="border-collapse:collapse;border:1px solid #000;mso-border-alt:solid #000 0.5pt;font-family:'宋体';font-size:11pt;text-align:center;">
+            <colgroup>${colGroup}</colgroup>
+            <tr height="40" style="height:40pt;">
+              <td colspan="${totalCols}" align="center" valign="middle" style="${border}padding:6px;text-align:center;font-family:'宋体';font-size:22pt;font-weight:bold;">${this.esc(
+          title
+        )}</td>
+            </tr>
+            <tr height="58" style="height:58pt;">
+              <td colspan="2" rowspan="2" align="center" valign="middle" style="${border}padding:2px;">${qrImg}</td>
+              <td colspan="4" align="left" valign="middle" style="${labelLeft}">客户名称:${this.esc(
+          f.clientName
+        )}</td>
+              <td colspan="3" align="left" valign="middle" style="${labelLeft}">发货单号:${this.esc(
+          f.bizNo
+        )}</td>
+            </tr>
+            <tr height="26" style="height:26pt;">
+              <td colspan="4" align="left" valign="middle" style="${labelLeft}">发货日期:${this.esc(
+          this.deliveryDate
+        )}</td>
+              <td colspan="3" align="left" valign="middle" style="${labelLeft}">联系人:${this.esc(
+          f.clientUser
+        )}  联系电话:${this.esc(f.clientPhone)}</td>
+            </tr>
+            <tr height="28" style="height:28pt;">
+              <td align="center" valign="middle" style="${th}">序号</td>
+              <td align="center" valign="middle" style="${th}">型号</td>
+              <td align="center" valign="middle" style="${th}">零件名</td>
+              <td align="center" valign="middle" style="${th}">单位</td>
+              <td align="center" valign="middle" style="${th}">送货数量</td>
+              <td align="center" valign="middle" style="${th}">验收数量</td>
+              <td align="center" valign="middle" style="${th}">备注</td>
+              <td align="center" valign="middle" style="${th}">订单号</td>
+              <td align="center" valign="middle" style="${th}">加工工艺</td>
+            </tr>
+            ${rows}
+            <tr height="26" style="height:26pt;">
+              <td align="center" valign="middle" style="${tdBold}">合计</td>
+              <td colspan="2" align="center" valign="middle" style="${tdBold}">${this.esc(
+          this.totalUnit
+        )}</td>
+              <td align="center" valign="middle" style="${tdBold}">${this.esc(
+          this.totalQuantity
+        )}</td>
+              <td align="center" valign="middle" style="${td}"></td>
+              <td align="center" valign="middle" style="${td}"></td>
+              <td align="center" valign="middle" style="${td}"></td>
+              <td align="center" valign="middle" style="${td}"></td>
+              <td align="center" valign="middle" style="${td}"></td>
+            </tr>
+            <tr height="28" style="height:28pt;">
+              <td colspan="3" align="left" valign="middle" style="${labelLeft}">发货人:${this.esc(
+          this.senderName
+        )}</td>
+              <td colspan="3" align="left" valign="middle" style="${labelLeft}">送货人:${this.esc(
+          this.deliveryName
+        )}</td>
+              <td colspan="3" align="left" valign="middle" style="${labelLeft}">收货人:${this.esc(
+          this.receiverName
+        )}</td>
+            </tr>
+            <tr height="28" style="height:28pt;">
+              <td colspan="${totalCols}" align="left" valign="middle" style="${labelLeft}">备注:${this.esc(
+          f.remark
+        )}</td>
+            </tr>
+          </table>`;
+
+        const html = `<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">
+        <head>
+        <meta charset="UTF-8" />
+        <style>
+          br { mso-data-placement: same-cell; }
+          table { mso-displayed-decimal-separator:"."; mso-displayed-thousand-separator:","; }
+          td { mso-ignore:padding; }
+          @page { size: A4 landscape; margin: 0.4in; }
+        </style>
+        <!--[if gte mso 9]><xml>
+        <x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet>
+        <x:Name>出库单</x:Name>
+        <x:WorksheetOptions>
+          <x:DefaultRowHeight>260</x:DefaultRowHeight>
+          <x:Print><x:ValidPrinterInfo/><x:HorizontalResolution>600</x:HorizontalResolution><x:VerticalResolution>600</x:VerticalResolution></x:Print>
+          <x:PageSetup><x:Orientation>Landscape</x:Orientation></x:PageSetup>
+          <x:Selected/>
+          <x:DoNotDisplayGridlines/>
+        </x:WorksheetOptions>
+        </x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook>
+        </xml><![endif]-->
+        </head>
+        <body>${table}</body></html>`;
+
+        const blob = new Blob(['' + html], {
+          type: 'application/vnd.ms-excel;charset=utf-8'
+        });
+        saveAs(blob, `${title}_${f.bizNo || this.yyyymmdd()}.xls`);
+      },
+      // 转义 HTML,避免内容里的 & < > 破坏表格结构
+      esc(val) {
+        if (val === null || val === undefined) return '';
+        return String(val)
+          .replace(/&/g, '&amp;')
+          .replace(/</g, '&lt;')
+          .replace(/>/g, '&gt;')
+          .replace(/"/g, '&quot;');
+      },
+      // 文件名用日期
+      yyyymmdd() {
+        const d = new Date();
+        const p = (n) => (n < 10 ? `0${n}` : `${n}`);
+        return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}`;
+      },
+      open(id) {
+        storageApi.getInboundDetailsById(id).then((res) => {
+          this.formData = res || {};
+          this.codeList = res?.outInDetailList || [];
+          this.QRvisible = true;
+        });
+      },
+      close() {
+        this.QRvisible = false;
+        this.codeList = [];
+        this.formData = {};
+      },
+      sumField(field) {
+        const total = this.codeList.reduce((sum, item) => {
+          const num = Number(item[field]);
+          return sum + (Number.isFinite(num) ? num : 0);
+        }, 0);
+        if (!total) return '';
+        // 去掉多余小数位
+        return Number(total.toFixed(3));
+      },
+      formatDate(val) {
+        if (!val) return '';
+        const date = new Date(String(val).replace(/-/g, '/'));
+        if (Number.isNaN(date.getTime())) return String(val);
+        return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`;
+      },
+      print() {
+        const printSection = document.getElementById(this.printSectionId);
+        if (!printSection) return;
+        // 创建打印任务
+        const printWindow = window.open('', '_blank');
+        if (!printWindow) return;
+        printWindow.document.open();
+        printWindow.document.write('<html><head><title>出库单打印</title>');
+        printWindow.document.write('<style>');
+        printWindow.document.write(`
+          @page { size: A4 landscape; margin: 8mm; }
+          body { margin: 0; padding: 0; font-family: "Microsoft YaHei", "SimSun", sans-serif; }
+          table { border-collapse: collapse; width: 100%; table-layout: fixed; }
+          th, td { border: 1px solid #333; padding: 4px 6px; font-size: 12px; word-break: break-all; }
+          .dp-td-center { text-align: center; }
+          .dp-info-table td, .dp-sign-table td { border: none; }
+          .dp-header { display: flex; align-items: flex-start; justify-content: space-between; min-height: var(--qr-header); margin-bottom: 6px; }
+          .dp-title-box { flex: 1; text-align: center; padding-top: 4px; }
+          .dp-qrcode { flex: 0 0 var(--qr-box); text-align: center; order: -1; }
+          .dp-qrcode.right { order: 1; }
+          /* 与弹框预览保持同一尺寸 */
+          .dp-qr-svg, .dp-qrcode .ele-qr-code, .dp-qrcode svg { width: var(--qr-size); height: var(--qr-size); }
+          .dp-company { font-size: 20px; font-weight: bold; letter-spacing: 1px; margin-bottom: 6px; }
+          .dp-title { font-size: 26px; font-weight: bold; letter-spacing: 6px; text-indent: 6px; }
+          .dp-qrcode-tip { font-size: 11px; margin-top: 4px; text-align: center; color: #666; }
+          .dp-inline { margin-left: 18px; white-space: nowrap; }
+          .dp-sum-row td { font-weight: bold; background: #fafafa; }
+          .dp-detail-table th { background: #f5f7fa; font-weight: bold; }
+        `);
+        printWindow.document.write('</style>');
+        printWindow.document.write('</head><body>');
+        printWindow.document.write(printSection.innerHTML);
+        printWindow.document.write('</body></html>');
+        printWindow.document.close();
+        printWindow.onload = function () {
+          printWindow.focus();
+          printWindow.print();
+        };
+      }
+    }
+  };
+</script>
+
+<style lang="scss">
+  .delivery-print {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+
+    .dp-table {
+      width: 100%;
+      border-collapse: collapse;
+      table-layout: fixed;
+      word-break: break-all;
+      word-wrap: break-word;
+
+      th,
+      td {
+        border: 1px solid #333;
+        padding: 5px 6px;
+        font-size: 12px;
+      }
+    }
+
+    .dp-td-center {
+      text-align: center;
+    }
+
+    /* 抬头 */
+.dp-header {
+      display: flex;
+      align-items: flex-start;
+      justify-content: space-between;
+      width: 100%;
+      margin-bottom: 6px;
+      position: relative;
+      min-height: var(--qr-header);
+    }
+    .dp-title-box {
+      flex: 1;
+      text-align: center;
+      padding-top: 4px;
+    }
+    .dp-company {
+      font-size: 20px;
+      font-weight: bold;
+      letter-spacing: 1px;
+      margin-bottom: 6px;
+    }
+    .dp-title {
+      font-size: 26px;
+      font-weight: bold;
+      letter-spacing: 6px;
+      text-indent: 6px;
+    }
+    /* 二维码尺寸统一由根节点的 --qr-size 变量控制 */
+.dp-qrcode {
+      flex: 0 0 var(--qr-box);
+      text-align: center;
+      /* 二维码默认左上角,qrPosition=right 时置于右上角 */
+      order: -1;
+      &.right {
+        order: 1;
+      }
+    }
+    .dp-qr-svg,
+    .dp-qrcode .ele-qr-code,
+    .dp-qrcode svg {
+      width: var(--qr-size);
+      height: var(--qr-size);
+    }
+    .dp-qrcode-tip {
+      font-size: 11px;
+      margin-top: 4px;
+      text-align: center;
+      color: #666;
+    }
+
+    /* 单据信息 */
+    .dp-info-table {
+      margin-bottom: 8px;
+      td {
+        border: none;
+        padding: 3px 6px;
+      }
+      .dp-info-label {
+        white-space: nowrap;
+        text-align: right;
+      }
+      .dp-inline {
+        margin-left: 18px;
+        white-space: nowrap;
+      }
+    }
+
+    /* 明细 */
+    .dp-detail-table {
+      th {
+        background: #f5f7fa;
+        font-weight: bold;
+      }
+      .dp-sum-row td {
+        font-weight: bold;
+        background: #fafafa;
+      }
+    }
+
+    /* 签收 */
+    .dp-sign-table {
+      margin-top: 10px;
+      td {
+        border: none;
+        padding: 4px 6px;
+      }
+    }
+  }
+</style>

+ 23 - 1
src/views/warehouseManagement/outgoingManagement/index.vue

@@ -189,6 +189,12 @@
             @click="putIntoStorage"
             >退货入库</el-button
           >
+          <el-button
+            icon="el-icon-printer"
+            type="primary"
+            @click="openPrint('printDelivery')"
+            >出库单打印</el-button
+          >
           <!-- <el-button
             icon="el-icon-printer"
             type="primary"
@@ -312,6 +318,7 @@
     <print2 ref="print2Ref"></print2>
     <print3 ref="print3Ref"></print3>
     <print4 ref="print4Ref"></print4>
+    <printDelivery ref="printDeliveryRef" :groupName="groupName"></printDelivery>
   </div>
 </template>
 
@@ -322,7 +329,9 @@
   import print2 from './components/print2.vue';
   import print3 from './components/print3.vue';
   import print4 from './components/print4.vue';
+  import printDelivery from './components/printDelivery.vue';
   import { allCategoryLevel } from '@/api/classifyManage';
+  import { enterprisePage } from '@/api/bpm/index.js';
   import outin from '@/api/warehouseManagement/outin';
   import warehouseDefinition from '@/api/warehouseManagement/warehouseDefinition';
   import outboundOrderPrint from '../components/outboundOrderPrint.vue';
@@ -331,7 +340,7 @@
   // import { getPage } from '@/api/stockManagement'
   // import { deepClone } from '@/utils'
   export default {
-    components: { outboundOrderPrint, print, print1, print2, print3, print4 },
+    components: { outboundOrderPrint, print, print1, print2, print3, print4, printDelivery },
     computed: {
       // 是否开启响应式布局
       styleResponsive() {
@@ -342,6 +351,7 @@
       return {
         currentId: '',
         currentRow: {},
+        groupName: '',
         auditStatus,
         outputSceneState,
         codeList: [],
@@ -530,6 +540,7 @@
       this.getTypeList();
       this.getFactoryList();
       this.getTreeData();
+      this.getCompanyInfo();
     },
     methods: {
       async getTreeData() {
@@ -554,6 +565,17 @@
         });
         this.factoryList = res.list;
       },
+      // 获取企业信息(单据标题)
+      getCompanyInfo() {
+        enterprisePage({
+          pageNum: 1,
+          size: 200
+        }).then((res) => {
+          if (res.list?.length > 0) {
+            this.groupName = res.list[0].name;
+          }
+        });
+      },
       openPrint(ref) {
         if (this.currentId) {
           this.$refs[ref + 'Ref'].open(this.currentId);