|
|
@@ -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, '&')
|
|
|
+ .replace(/</g, '<')
|
|
|
+ .replace(/>/g, '>')
|
|
|
+ .replace(/"/g, '"');
|
|
|
+ },
|
|
|
+ // 文件名用日期
|
|
|
+ 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>
|