二维码生成

This commit is contained in:
砂糖
2025-07-23 16:26:33 +08:00
parent c28fe1a741
commit 9ec1cc23d9
5 changed files with 386 additions and 2 deletions

20
klp-ui/.env.stage Normal file
View File

@@ -0,0 +1,20 @@
# 页面标题
VUE_APP_TITLE = RuoYi-Flowable-Plus后台管理系统
# 开发环境配置
ENV = 'development'
# 若依管理系统/开发环境
VUE_APP_BASE_API = 'http://140.143.206.120:8080'
# 应用访问路径 例如使用前缀 /admin/
VUE_APP_CONTEXT_PATH = '/'
# 监控地址
VUE_APP_MONITRO_ADMIN = 'http://localhost:9090/admin/login'
# xxl-job 控制台地址
VUE_APP_XXL_JOB_ADMIN = 'http://localhost:9100/xxl-job-admin'
# 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true

View File

@@ -7,6 +7,7 @@
"scripts": {
"dev": "vue-cli-service serve",
"build:prod": "vue-cli-service build",
"stage": "vue-cli-service serve --mode stage",
"preview": "node build/index.js --preview",
"lint": "eslint --ext .js,.vue src"
},
@@ -48,8 +49,10 @@
"highlight.js": "10.5.0",
"js-beautify": "1.13.0",
"js-cookie": "3.0.1",
"jsbarcode": "^3.12.1",
"jsencrypt": "3.0.0-rc.1",
"nprogress": "0.2.0",
"qrcode": "^1.5.4",
"quill": "1.3.7",
"screenfull": "5.0.2",
"sortablejs": "1.10.2",

View File

@@ -147,7 +147,7 @@ export default {
description: '出库入库管理',
icon: 'fas fa-tools',
bgColor: 'bg-red-500',
link: '/wms/stcokIo'
link: '/wms/stockIo'
},
{
title: '系统设置',

View File

@@ -136,6 +136,12 @@
icon="el-icon-document"
@click="showDetail(scope.row)"
>明细</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-printer"
@click="handleShowBarcodeDrawer(scope.row)"
>打印条码</el-button>
</template>
</el-table-column>
</el-table>
@@ -199,17 +205,35 @@
@status-changed="onStatusChanged"
/>
</el-dialog>
<!-- 条码打印抽屉 -->
<el-drawer
title="条码打印"
:visible.sync="drawerBarcodeVisible"
size="90%"
direction="btt"
:with-header="true"
>
<BarcodeGenerator
v-if="drawerBarcodeVisible"
:barcodes="drawerBarcodeData.barcodes"
:perRow="drawerBarcodeData.perRow"
:barcodeWidth="drawerBarcodeData.barcodeWidth"
:barcodeHeight="drawerBarcodeData.barcodeHeight"
/>
</el-drawer>
</div>
</template>
<script>
import { listStockIo, getStockIo, delStockIo, addStockIo, updateStockIo } from "@/api/wms/stockIo";
import { listStockIoDetail } from "@/api/wms/stockIoDetail";
import StockIoDetailPanel from './panels/detail.vue';
import BarcodeGenerator from './panels/barcode.vue';
export default {
name: "StockIo",
dicts: ['stock_biz_type', 'stock_io_type', 'stock_status'],
components: { StockIoDetailPanel },
components: { StockIoDetailPanel, BarcodeGenerator },
data() {
return {
// 按钮loading
@@ -272,6 +296,14 @@ export default {
},
detailDialogVisible: false,
detailStockIo: null,
// 条码打印抽屉相关
drawerBarcodeVisible: false,
drawerBarcodeData: {
barcodes: [],
perRow: 3,
barcodeWidth: 180,
barcodeHeight: 60
},
};
},
created() {
@@ -406,6 +438,22 @@ export default {
// 刷新列表
this.getList();
},
async handleShowBarcodeDrawer(row) {
// 获取明细列表
const res = await listStockIoDetail({ stockIoId: row.stockIoId });
const details = res.data || res.rows || [];
// 拼接条码内容 stockIoId_warehouseId_materialId_quantity
const barcodes = details.map(item => {
return encodeURIComponent(`${row.stockIoId}_${item.warehouseId || ''}_${item.itemId || ''}_${item.quantity || ''}`);
});
this.drawerBarcodeData = {
barcodes,
perRow: 3,
barcodeWidth: 180,
barcodeHeight: 60
};
this.drawerBarcodeVisible = true;
},
getIoTypeTagType(type) {
if (type === 'in') return 'success';
if (type === 'out') return 'primary';

View File

@@ -0,0 +1,313 @@
<template>
<div class="barcode-3col-layout">
<!-- 预览区 -->
<div class="barcode-preview-col">
<div class="preview-toolbar">
<span>缩放</span>
<el-slider v-model="previewScale" :min="0.3" :max="2" :step="0.01" style="width:180px;display:inline-block;" />
<span style="margin-left:8px;">{{ Math.round(previewScale*100) }}%</span>
</div>
<div class="iframe-wrapper" :style="iframeWrapperStyle">
<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>
<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="条码明细">
<el-table :data="barcodeConfigs" size="mini" border style="width:100%;">
<el-table-column label="条码内容" prop="code" width="90" align="center" show-overflow-tooltip />
<el-table-column label="数量" width="180" align="center">
<template slot-scope="scope">
<el-input-number v-model.number="scope.row.count" :min="1" :max="100" size="mini" />
</template>
</el-table-column>
<el-table-column label="下方文字模板" align="center">
<template slot-scope="scope">
<el-input v-model="scope.row.textTpl" size="mini" placeholder="如 箱号" />
</template>
</el-table-column>
</el-table>
</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'
};
},
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() {
return {
transform: `scale(${this.previewScale})`,
'transform-origin': 'top left',
width: '794px',
height: 'auto',
overflow: 'hidden',
background: '#fff',
border: 'none',
display: 'block'
};
},
iframeWrapperStyle() {
return {
width: `${794 * this.previewScale}px`,
minHeight: `${1123 * this.previewScale}px`,
overflow: 'auto',
background: '#f8f8f8',
padding: '16px 0',
display: 'flex',
justifyContent: 'center'
};
}
},
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() {}
},
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;
},
getPrintHtml() {
let html = `
<html>
<head>
<title>打印二维码</title>
<style>
body { margin: 0; padding: 0; background: #fff; }
.barcode-list { width: 794px; 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;
}
.barcode-right-col {
width: 420px;
min-width: 320px;
display: flex;
flex-direction: column;
background: #fafbfc;
}
.barcode-control-bar {
border-bottom: 1px solid #eee;
background: #fafbfc;
padding: 0 16px;
}
.barcode-settings-panel {
flex: 1;
padding: 16px;
overflow: auto;
}
.preview-toolbar {
margin-bottom: 12px;
display: flex;
align-items: center;
}
.iframe-wrapper {
width: 100%;
display: flex;
justify-content: center;
align-items: flex-start;
background: #f8f8f8;
overflow-x: auto;
}
.barcode-iframe {
width: 794px;
min-height: 1123px;
border: none;
background: #fff;
border-radius: 4px;
display: block;
}
</style>