355 lines
11 KiB
Vue
355 lines
11 KiB
Vue
<template>
|
||
<div class="barcode-3col-layout">
|
||
<!-- 预览区 -->
|
||
<div class="barcode-preview-col">
|
||
<div class="iframe-wrapper">
|
||
<iframe ref="previewIframe" class="barcode-iframe" frameborder="0" :style="iframeStyle"></iframe>
|
||
</div>
|
||
</div>
|
||
<!-- 右侧控制+设置区 -->
|
||
<div class="barcode-right-col">
|
||
<!-- 控制区 -->
|
||
<div class="barcode-control-bar">
|
||
<el-tabs v-model="activeTab" stretch>
|
||
<el-tab-pane label="排版设置" name="layout" />
|
||
<el-tab-pane label="条码明细" name="detail" />
|
||
</el-tabs>
|
||
</div>
|
||
<!-- 设置区 -->
|
||
<div class="barcode-settings-panel">
|
||
<el-form v-if="activeTab==='layout'" label-width="80px" size="small" label-position="top">
|
||
<el-form-item label="每行数量">
|
||
<el-input-number v-model="perRow" :min="1" :max="10" />
|
||
</el-form-item>
|
||
<el-form-item label="条码宽度">
|
||
<el-input-number v-model="barcodeWidth" :min="60" :max="600" />
|
||
</el-form-item>
|
||
<el-form-item label="条码高度">
|
||
<el-input-number v-model="barcodeHeight" :min="20" :max="200" />
|
||
</el-form-item>
|
||
<el-form-item label="纸张尺寸">
|
||
<el-select v-model="paperSize" placeholder="请选择纸张尺寸" style="width: 160px">
|
||
<el-option label="A4 (210mm x 297mm)" value="A4" />
|
||
<el-option label="A5 (148mm x 210mm)" value="A5" />
|
||
<el-option label="A6 (105mm x 148mm)" value="A6" />
|
||
<el-option label="自定义" value="custom" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item v-if="paperSize==='custom'" label="自定义宽度(mm)">
|
||
<el-input-number v-model="customPaperWidth" :min="50" :max="500" />
|
||
</el-form-item>
|
||
<el-form-item v-if="paperSize==='custom'" label="自定义高度(mm)">
|
||
<el-input-number v-model="customPaperHeight" :min="50" :max="500" />
|
||
</el-form-item>
|
||
<el-form-item label="方向">
|
||
<el-radio-group v-model="paperOrientation">
|
||
<el-radio label="portrait">纵向</el-radio>
|
||
<el-radio label="landscape">横向</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="handlePrint">打印</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<el-form v-else label-width="80px" size="small" label-position="top">
|
||
<el-form-item label="条码明细">
|
||
<div v-for="(cfg, idx) in barcodeConfigs" :key="idx" style="margin-bottom: 16px; border: 1px solid #eee; border-radius: 4px; padding: 12px 16px; background: #fafbfc;">
|
||
<div style="margin-bottom: 8px; font-weight: bold; color: #666;">条码 {{ idx + 1 }}</div>
|
||
<el-form-item label="条码内容" label-width="70px" style="margin-bottom: 8px;">
|
||
<el-input type="textarea" v-model="cfg.code" size="mini" :autosize="{ minRows: 1, maxRows: 3 }" placeholder="请输入条码内容" />
|
||
</el-form-item>
|
||
<el-form-item label="数量" label-width="70px" style="margin-bottom: 8px;">
|
||
<el-input-number v-model.number="cfg.count" :min="1" :max="100" size="mini" />
|
||
</el-form-item>
|
||
<el-form-item label="下方文字" label-width="70px" style="margin-bottom: 0;">
|
||
<el-input type="textarea" v-model="cfg.textTpl" size="mini" placeholder="如 箱号" />
|
||
</el-form-item>
|
||
</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
// import JsBarcode from 'jsbarcode';
|
||
import QRCode from 'qrcode';
|
||
export default {
|
||
name: 'BarcodeGenerator',
|
||
props: {
|
||
barcodes: {
|
||
type: Array,
|
||
required: true
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
perRow: 3,
|
||
barcodeWidth: 180,
|
||
barcodeHeight: 180, // 二维码建议宽高一致
|
||
barcodeConfigs: [], // [{code, count, textTpl}]
|
||
previewScale: 1, // 预览缩放比例
|
||
activeTab: 'layout',
|
||
paperSize: 'A4',
|
||
paperOrientation: 'portrait',
|
||
customPaperWidth: 210,
|
||
customPaperHeight: 297
|
||
};
|
||
},
|
||
computed: {
|
||
expandedBarcodes() {
|
||
let arr = [];
|
||
this.barcodeConfigs.forEach(cfg => {
|
||
for (let i = 0; i < (cfg.count || 1); i++) {
|
||
arr.push(cfg.code);
|
||
}
|
||
});
|
||
return arr;
|
||
},
|
||
expandedBarcodeTexts() {
|
||
let arr = [];
|
||
this.barcodeConfigs.forEach(cfg => {
|
||
for (let i = 0; i < (cfg.count || 1); i++) {
|
||
// 模板替换 {{n}}
|
||
let text = cfg.textTpl || cfg.code;
|
||
text = text.replace(/\{\{n\}\}/g, i + 1);
|
||
arr.push(text);
|
||
}
|
||
});
|
||
return arr;
|
||
},
|
||
expandedBarcodeTableData() {
|
||
return this.expandedBarcodes.map((code, idx) => ({ code, text: this.expandedBarcodeTexts[idx] }));
|
||
},
|
||
barcodeRows() {
|
||
const rows = [];
|
||
const arr = this.expandedBarcodes;
|
||
for (let i = 0; i < arr.length; i += this.perRow) {
|
||
rows.push(arr.slice(i, i + this.perRow));
|
||
}
|
||
return rows;
|
||
},
|
||
barcodeTextRows() {
|
||
const rows = [];
|
||
const arr = this.expandedBarcodeTexts;
|
||
for (let i = 0; i < arr.length; i += this.perRow) {
|
||
rows.push(arr.slice(i, i + this.perRow));
|
||
}
|
||
return rows;
|
||
},
|
||
iframeStyle() {
|
||
const { width, height } = this.getPaperPx();
|
||
return {
|
||
width: width + 'px',
|
||
minHeight: height + 'px',
|
||
height: height + 'px',
|
||
background: '#fff',
|
||
border: 'none',
|
||
display: 'block',
|
||
margin: 0,
|
||
padding: 0
|
||
};
|
||
}
|
||
},
|
||
watch: {
|
||
barcodes: {
|
||
handler(newVal) {
|
||
// 初始化barcodeConfigs
|
||
this.barcodeConfigs = newVal.map((b, i) => {
|
||
const old = this.barcodeConfigs[i] || {};
|
||
return {
|
||
code: b,
|
||
count: old.count || 1,
|
||
textTpl: typeof old.textTpl === 'string' ? old.textTpl : b
|
||
};
|
||
});
|
||
this.$nextTick(this.renderPreviewIframe);
|
||
},
|
||
immediate: true
|
||
},
|
||
barcodeConfigs: {
|
||
handler() { this.$nextTick(this.renderPreviewIframe); },
|
||
deep: true
|
||
},
|
||
perRow() { this.$nextTick(this.renderPreviewIframe); },
|
||
barcodeWidth() { this.$nextTick(this.renderPreviewIframe); },
|
||
barcodeHeight() { this.$nextTick(this.renderPreviewIframe); },
|
||
previewScale() { this.$nextTick(this.renderPreviewIframe); },
|
||
paperSize() { this.$nextTick(this.renderPreviewIframe); },
|
||
paperOrientation() { this.$nextTick(this.renderPreviewIframe); },
|
||
customPaperWidth() { this.$nextTick(this.renderPreviewIframe); },
|
||
customPaperHeight() { this.$nextTick(this.renderPreviewIframe); }
|
||
},
|
||
methods: {
|
||
getBarcodeId(row, col) {
|
||
return `barcode-canvas-${row}-${col}`;
|
||
},
|
||
getBarcodeText(row, col, code) {
|
||
if (
|
||
this.barcodeTextRows[row] &&
|
||
typeof this.barcodeTextRows[row][col] !== 'undefined' &&
|
||
this.barcodeTextRows[row][col] !== null &&
|
||
this.barcodeTextRows[row][col] !== ''
|
||
) {
|
||
return this.barcodeTextRows[row][col];
|
||
}
|
||
return code;
|
||
},
|
||
getPaperPx() {
|
||
// mm to px, 1mm ≈ 3.78px
|
||
let width, height;
|
||
if (this.paperSize === 'A4') {
|
||
width = 210 * 3.78;
|
||
height = 297 * 3.78;
|
||
} else if (this.paperSize === 'A5') {
|
||
width = 148 * 3.78;
|
||
height = 210 * 3.78;
|
||
} else if (this.paperSize === 'A6') {
|
||
width = 105 * 3.78;
|
||
height = 148 * 3.78;
|
||
} else {
|
||
width = this.customPaperWidth * 3.78;
|
||
height = this.customPaperHeight * 3.78;
|
||
}
|
||
if (this.paperOrientation === 'landscape') {
|
||
return { width: height, height: width };
|
||
}
|
||
return { width, height };
|
||
},
|
||
getPrintHtml() {
|
||
const { width, height } = this.getPaperPx();
|
||
let html = `
|
||
<html>
|
||
<head>
|
||
<title>打印二维码</title>
|
||
<style>
|
||
body { margin: 0; padding: 0; background: #fff; }
|
||
.barcode-list { width: ${width}px; min-height: ${height}px; margin: 0 auto; background: #fff; }
|
||
.barcode-row { display: flex; margin-bottom: 24px; }
|
||
.barcode-item { flex: 1; text-align: center; }
|
||
.barcode-text { margin-top: 8px; font-size: 14px; }
|
||
html, body { overflow-x: hidden; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="barcode-list">
|
||
`;
|
||
this.barcodeRows.forEach((row, rowIndex) => {
|
||
html += '<div class="barcode-row">';
|
||
row.forEach((code, colIndex) => {
|
||
const id = this.getBarcodeId(rowIndex, colIndex);
|
||
const text = this.getBarcodeText(rowIndex, colIndex, code);
|
||
html += `<div class="barcode-item">
|
||
<canvas id="${id}" width="${this.barcodeWidth}" height="${this.barcodeHeight}"></canvas>
|
||
<div class="barcode-text">${text}</div>
|
||
</div>`;
|
||
});
|
||
html += '</div>';
|
||
});
|
||
html += '</div></body></html>';
|
||
return html;
|
||
},
|
||
async renderPreviewIframe() {
|
||
const iframe = this.$refs.previewIframe;
|
||
if (!iframe) return;
|
||
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
||
doc.open();
|
||
doc.write(this.getPrintHtml());
|
||
doc.close();
|
||
// 渲染二维码
|
||
setTimeout(async () => {
|
||
for (let rowIndex = 0; rowIndex < this.barcodeRows.length; rowIndex++) {
|
||
const row = this.barcodeRows[rowIndex];
|
||
for (let colIndex = 0; colIndex < row.length; colIndex++) {
|
||
const code = row[colIndex];
|
||
const id = this.getBarcodeId(rowIndex, colIndex);
|
||
const el = doc.getElementById(id);
|
||
if (el) {
|
||
await QRCode.toCanvas(el, code, {
|
||
width: this.barcodeWidth,
|
||
height: this.barcodeHeight,
|
||
margin: 0
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}, 50);
|
||
},
|
||
handlePrint() {
|
||
const iframe = this.$refs.previewIframe;
|
||
if (!iframe) return;
|
||
iframe.contentWindow.focus();
|
||
iframe.contentWindow.print();
|
||
}
|
||
},
|
||
mounted() {
|
||
this.barcodeConfigs = this.barcodes.map(b => ({ code: b, count: 1, textTpl: b }));
|
||
this.renderPreviewIframe();
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.barcode-3col-layout {
|
||
display: flex;
|
||
flex-direction: row;
|
||
width: 100%;
|
||
min-height: 400px;
|
||
}
|
||
.barcode-preview-col {
|
||
flex: 1 1 0;
|
||
min-width: 0;
|
||
background: #fff;
|
||
border-right: 1px solid #eee;
|
||
padding: 24px 0 24px 24px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 90vh;
|
||
overflow: auto;
|
||
}
|
||
.barcode-right-col {
|
||
width: 420px;
|
||
min-width: 320px;
|
||
padding: 16px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #fafbfc;
|
||
height: 90vh;
|
||
}
|
||
.barcode-control-bar {
|
||
border-bottom: 1px solid #eee;
|
||
background: #fafbfc;
|
||
padding: 0 16px;
|
||
}
|
||
.barcode-settings-panel {
|
||
flex: 1;
|
||
overflow: auto;
|
||
min-height: 0;
|
||
max-height: calc(90vh - 48px); /* 48px 约为tabs高度,可根据实际调整 */
|
||
}
|
||
.preview-toolbar {
|
||
margin-bottom: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.iframe-wrapper {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: flex-start;
|
||
background: #f8f8f8;
|
||
overflow: auto;
|
||
}
|
||
.barcode-iframe {
|
||
/* width、height 由:style绑定动态控制 */
|
||
min-width: 0;
|
||
min-height: 0;
|
||
border: none;
|
||
background: #fff;
|
||
border-radius: 4px;
|
||
display: block;
|
||
}
|
||
</style>
|