index80x40.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714
  1. <template>
  2. <ele-modal
  3. title="打码"
  4. :visible.sync="visible"
  5. v-if="visible"
  6. width="40%"
  7. :maxable="true"
  8. append-to-body
  9. >
  10. <div class="encoding-dialog">
  11. <div class="encoding-header">
  12. <el-button type="primary" size="small" @click="batchEncoding">
  13. 批量打码
  14. </el-button>
  15. <el-button size="small" @click="refresh">刷新</el-button>
  16. <el-button type="warning" size="small" @click="batchBarcodeEncoding">
  17. 批量条码打码
  18. </el-button>
  19. </div>
  20. <div class="encoding-content" v-if="mode === 'qrcode'">
  21. <div v-for="(item, index) in batchList" :key="index" class="batch-card">
  22. <div class="card-header">
  23. <span class="card-title">{{
  24. item.partName || item.productName || '产品标识卡'
  25. }}</span>
  26. </div>
  27. <div class="card-body">
  28. <div class="info-area">
  29. <div class="info-row">
  30. <span class="label">产品编码</span>
  31. <span class="value">{{ item.productCode || '-' }}</span>
  32. </div>
  33. <div class="info-row">
  34. <span class="label">{{
  35. sourceType == 'jobBom' ? '单件码' : '合格证号'
  36. }}</span>
  37. <span class="value">{{
  38. (sourceType == 'jobBom'
  39. ? item.engrave
  40. : item.certificateNo) || '-'
  41. }}</span>
  42. </div>
  43. <div class="info-row">
  44. <span class="label">零(部)件编码</span>
  45. <span class="value">{{ item.partCode || '-' }}</span>
  46. </div>
  47. <div class="info-row">
  48. <span class="label">零(部)件名称</span>
  49. <span class="value">{{ item.partName || '-' }}</span>
  50. </div>
  51. <div class="info-row">
  52. <span class="label">本厂批次号</span>
  53. <span class="value">{{ item.batchNo || '-' }}</span>
  54. </div>
  55. <div class="info-row">
  56. <span class="label">{{
  57. sourceType == 'jobBom' ? '数量' : '本批数量'
  58. }}</span>
  59. <span class="value">{{ item.feedQuantity || '-' }}</span>
  60. </div>
  61. <template v-if="sourceType !== 'jobBom'">
  62. <div class="info-row">
  63. <span class="label">原厂批号</span>
  64. <span class="value">{{ item.originalBatchNo || '-' }}</span>
  65. </div>
  66. <div class="info-row">
  67. <span class="label">原厂合格证号</span>
  68. <span class="value">{{
  69. item.originalCertificateNo || '-'
  70. }}</span>
  71. </div>
  72. </template>
  73. </div>
  74. <div class="qr-area">
  75. <div class="qr-box">
  76. <img
  77. v-if="item.qrCodeUrl"
  78. :src="item.qrCodeUrl"
  79. alt="二维码"
  80. class="qr-img"
  81. />
  82. </div>
  83. </div>
  84. </div>
  85. </div>
  86. </div>
  87. <div class="encoding-content" v-if="mode === 'barcode'">
  88. <div
  89. v-for="(item, index) in batchList"
  90. :key="'bar-' + index"
  91. class="barcode-card"
  92. >
  93. <div class="barcode-card-header">
  94. <span class="barcode-card-title">
  95. {{ item.partName || item.productName || '产品标识' }}
  96. </span>
  97. </div>
  98. <div class="barcode-meta">
  99. <div class="meta-item">
  100. <span class="meta-label">型号:</span>
  101. <span class="meta-value">{{ item.model || '-' }}</span>
  102. </div>
  103. <div class="meta-item">
  104. <span class="meta-label">规格:</span>
  105. <span class="meta-value">{{ item.specification || '-' }}</span>
  106. </div>
  107. </div>
  108. <div class="barcode-wrap">
  109. <canvas :ref="'barcode_' + index"></canvas>
  110. </div>
  111. <div class="barcode-raw" v-if="item.codeStr !== item.barcodeValue">
  112. {{ item.codeStr }}
  113. </div>
  114. </div>
  115. </div>
  116. </div>
  117. <div slot="footer">
  118. <el-button size="small" @click="printPreview">打印预览</el-button>
  119. <el-button size="small" @click="close">关闭</el-button>
  120. </div>
  121. </ele-modal>
  122. </template>
  123. <script>
  124. import QRCode from 'qrcode';
  125. import JsBarcode from 'jsbarcode';
  126. export default {
  127. name: 'EncodingDialog80x40',
  128. data() {
  129. return {
  130. visible: false,
  131. workOrderList: [],
  132. batchList: [],
  133. mode: 'qrcode',
  134. sourceType: 'default'
  135. };
  136. },
  137. methods: {
  138. open(workOrders, type) {
  139. this.visible = true;
  140. this.mode = 'qrcode';
  141. this.sourceType = type || 'default';
  142. this.workOrderList = workOrders || [];
  143. this.buildBatchList();
  144. },
  145. close() {
  146. this.visible = false;
  147. this.batchList = [];
  148. },
  149. buildBatchList() {
  150. const isJobBom = this.sourceType === 'jobBom';
  151. const sanitize = (v) => String(v == null ? '' : v).replace(/;/g, '');
  152. this.batchList = this.workOrderList.map((item) => {
  153. const productCode = item.topCategoryCode || '';
  154. const partCode = isJobBom
  155. ? item.categoryCode || ''
  156. : item.productCode || '';
  157. const partName = isJobBom ? item.name || '' : item.productName || '';
  158. const batchNo = item.batchNo || '';
  159. const feedQuantity = isJobBom
  160. ? (item.feedQuantity || item.quantity || item.planQuantity || '') +
  161. (item.unit || '')
  162. : (item.formingNum || '') + (item.unit || '');
  163. const originalBatchNo = item.originalBatchNo || '';
  164. const certificateNo = item.certificateNo || '';
  165. const originalCertificateNo = item.originalCertificateNo || '';
  166. const codeStr = isJobBom
  167. ? [
  168. item.categoryCode,
  169. item.batchNo,
  170. item.extInfo && item.extInfo.engrave
  171. ]
  172. .filter(Boolean)
  173. .join('-')
  174. : [
  175. productCode,
  176. partCode,
  177. partName,
  178. batchNo,
  179. originalBatchNo,
  180. certificateNo,
  181. originalCertificateNo,
  182. feedQuantity
  183. ]
  184. .map(sanitize)
  185. .join(';');
  186. const barcodeValue = isJobBom
  187. ? codeStr
  188. : [productCode, partCode, batchNo, certificateNo]
  189. .map((v) =>
  190. String(v == null ? '' : v).replace(/[^\x20-\x7E]/g, '')
  191. )
  192. .filter(Boolean)
  193. .join('-') ||
  194. productCode ||
  195. partCode ||
  196. 'NA';
  197. return {
  198. productCode,
  199. productName: item.topCategoryName || '',
  200. partCode,
  201. partName,
  202. batchNo,
  203. feedQuantity,
  204. codeStr,
  205. barcodeValue,
  206. qrCodeUrl: '',
  207. model: isJobBom
  208. ? item.modelType || item.model || ''
  209. : item.model || '',
  210. specification: item.specification || '',
  211. engrave: item.extInfo && item.extInfo.engrave,
  212. originalBatchNo,
  213. certificateNo,
  214. originalCertificateNo
  215. };
  216. });
  217. if (this.mode === 'qrcode') {
  218. this.generateQRCodes();
  219. } else {
  220. this.$nextTick(() => {
  221. this.generateBarcodes();
  222. });
  223. }
  224. },
  225. generateQRCodes() {
  226. this.batchList.forEach((item, index) => {
  227. const content = item.codeStr || item.productCode || String(index);
  228. QRCode.toDataURL(content, { width: 200, margin: 1 })
  229. .then((url) => {
  230. this.$set(this.batchList[index], 'qrCodeUrl', url);
  231. })
  232. .catch((err) => {
  233. console.error('QRCode生成失败:', err);
  234. });
  235. });
  236. },
  237. generateBarcodes() {
  238. this.batchList.forEach((item, index) => {
  239. const refKey = 'barcode_' + index;
  240. const canvasArr = this.$refs[refKey];
  241. if (canvasArr && canvasArr[0]) {
  242. const content = item.barcodeValue || item.productCode || 'NA';
  243. try {
  244. JsBarcode(canvasArr[0], content, {
  245. format: 'CODE128',
  246. width: 2,
  247. height: 50,
  248. displayValue: false,
  249. margin: 4,
  250. background: '#ffffff',
  251. lineColor: '#1f2329'
  252. });
  253. } catch (err) {
  254. console.error('Barcode生成失败:', err, content);
  255. }
  256. }
  257. });
  258. },
  259. refresh() {
  260. this.buildBatchList();
  261. },
  262. batchEncoding() {
  263. this.mode = 'qrcode';
  264. this.$nextTick(() => {
  265. this.generateQRCodes();
  266. });
  267. },
  268. batchBarcodeEncoding() {
  269. this.mode = 'barcode';
  270. this.$nextTick(() => {
  271. this.generateBarcodes();
  272. });
  273. },
  274. printPreview() {
  275. const printContent = this.$el.querySelector('.encoding-content');
  276. if (!printContent) return;
  277. let html = printContent.innerHTML;
  278. if (this.mode === 'barcode') {
  279. const container = document.createElement('div');
  280. container.innerHTML = html;
  281. const canvases = this.$el.querySelectorAll(
  282. '.encoding-content canvas'
  283. );
  284. const imgPlaceholders = container.querySelectorAll('canvas');
  285. canvases.forEach((canvas, i) => {
  286. if (imgPlaceholders[i]) {
  287. const img = document.createElement('img');
  288. img.src = canvas.toDataURL('image/png');
  289. img.style.width = '72mm';
  290. img.style.height = '14mm';
  291. imgPlaceholders[i].parentNode.replaceChild(
  292. img,
  293. imgPlaceholders[i]
  294. );
  295. }
  296. });
  297. html = container.innerHTML;
  298. }
  299. const printWindow = window.open('', '_blank');
  300. printWindow.document.write(`
  301. <html>
  302. <head>
  303. <title>打码打印预览</title>
  304. <style>
  305. @page {
  306. size: 80mm 40mm;
  307. margin: 0;
  308. }
  309. * { box-sizing: border-box; margin: 0; padding: 0; }
  310. html, body {
  311. font-family: 'Microsoft Yahei', Arial, sans-serif;
  312. width: 80mm;
  313. margin: 0;
  314. padding: 0;
  315. color: #1f2329;
  316. -webkit-print-color-adjust: exact;
  317. print-color-adjust: exact;
  318. }
  319. .encoding-content { width: 80mm; }
  320. .batch-card {
  321. width: 80mm;
  322. height: 40mm;
  323. padding: 1.5mm;
  324. page-break-after: always;
  325. page-break-inside: avoid;
  326. break-inside: avoid;
  327. overflow: hidden;
  328. display: flex;
  329. flex-direction: column;
  330. }
  331. .batch-card:last-child { page-break-after: avoid; }
  332. .batch-card > .card-header,
  333. .batch-card > .card-body {
  334. width: 100%;
  335. }
  336. .card-header {
  337. min-height: 5mm;
  338. background: #1f2329;
  339. color: #fff;
  340. display: flex;
  341. align-items: center;
  342. justify-content: space-between;
  343. padding: 0.5mm 2mm;
  344. border-radius: 0.8mm 0.8mm 0 0;
  345. flex-shrink: 0;
  346. }
  347. .card-header .card-title {
  348. flex: 1;
  349. font-size: 7pt;
  350. font-weight: 700;
  351. letter-spacing: 0.2mm;
  352. line-height: 1.2;
  353. word-break: break-all;
  354. white-space: nowrap;
  355. overflow: hidden;
  356. text-overflow: ellipsis;
  357. }
  358. .card-body {
  359. flex: 1;
  360. border: 1px solid #000;
  361. border-top: none;
  362. border-radius: 0 0 0.8mm 0.8mm;
  363. display: flex;
  364. align-items: stretch;
  365. overflow: hidden;
  366. }
  367. .info-area {
  368. flex: 1;
  369. display: flex;
  370. flex-direction: column;
  371. border-right: 1px solid #000;
  372. }
  373. .info-row {
  374. flex: 1;
  375. display: flex;
  376. align-items: center;
  377. border-bottom: 1px dashed #c0c4cc;
  378. padding: 0 1.2mm;
  379. min-height: 0;
  380. }
  381. @media print {
  382. .info-row {
  383. border-bottom: none;
  384. background-image: linear-gradient(
  385. to right,
  386. #000 50%,
  387. transparent 50%
  388. );
  389. background-size: 2px 1px;
  390. background-repeat: repeat-x;
  391. background-position: left bottom;
  392. }
  393. .info-row:last-child { background-image: none; }
  394. }
  395. .info-row:last-child { border-bottom: none; }
  396. .info-row .label {
  397. width: 16mm;
  398. font-size: 5pt;
  399. color: #606266;
  400. flex-shrink: 0;
  401. line-height: 1.1;
  402. height: 100%;
  403. display: flex;
  404. align-items: center;
  405. background-image: linear-gradient(
  406. to bottom,
  407. #000 50%,
  408. transparent 50%
  409. );
  410. background-size: 1px 2px;
  411. background-repeat: repeat-y;
  412. background-position: right top;
  413. }
  414. .info-row .value {
  415. flex: 1;
  416. font-size: 5pt;
  417. font-weight: 600;
  418. color: #1f2329;
  419. word-break: break-all;
  420. line-height: 1.1;
  421. padding-left: 1.2mm;
  422. }
  423. .qr-area {
  424. width: 22mm;
  425. display: flex;
  426. flex-direction: column;
  427. align-items: center;
  428. justify-content: center;
  429. padding: 0.8mm;
  430. background: #fafbfc;
  431. }
  432. .qr-box {
  433. width: 18mm;
  434. height: 18mm;
  435. padding: 0.4mm;
  436. background: #fff;
  437. border: 1px solid #000;
  438. border-radius: 0.5mm;
  439. display: flex;
  440. align-items: center;
  441. justify-content: center;
  442. }
  443. .qr-img { width: 100%; height: 100%; }
  444. .barcode-card {
  445. width: 80mm;
  446. height: 40mm;
  447. padding: 0;
  448. display: flex;
  449. flex-direction: column;
  450. page-break-after: always;
  451. overflow: hidden;
  452. border: 1px solid #000;
  453. border-radius: 0.8mm;
  454. }
  455. .barcode-card:last-child { page-break-after: avoid; }
  456. .barcode-card-header {
  457. display: flex;
  458. align-items: center;
  459. justify-content: space-between;
  460. padding: 0.8mm 2mm;
  461. background: #1f2329;
  462. color: #fff;
  463. }
  464. .barcode-card-title {
  465. flex: 1;
  466. font-size: 7pt;
  467. font-weight: 700;
  468. letter-spacing: 0.2mm;
  469. text-align: left;
  470. word-break: break-all;
  471. white-space: nowrap;
  472. overflow: hidden;
  473. text-overflow: ellipsis;
  474. }
  475. .barcode-meta {
  476. display: flex;
  477. justify-content: center;
  478. gap: 4mm;
  479. padding: 1mm 2mm 0.5mm;
  480. flex-wrap: wrap;
  481. font-size: 6.5pt;
  482. }
  483. .barcode-meta .meta-item { display: inline-flex; align-items: center; }
  484. .barcode-meta .meta-label { color: #606266; margin-right: 0.6mm; }
  485. .barcode-meta .meta-value { color: #1f2329; font-weight: 600; }
  486. .barcode-wrap { margin: 0 auto; text-align: center; padding: 0.5mm; }
  487. .barcode-wrap img { width: 72mm; height: 14mm; image-rendering: pixelated; }
  488. .barcode-raw {
  489. margin-top: 0.5mm;
  490. padding: 0 2mm;
  491. font-size: 5.5pt;
  492. color: #606266;
  493. text-align: center;
  494. word-break: break-all;
  495. line-height: 1.2;
  496. }
  497. </style>
  498. </head>
  499. <body>${html}</body>
  500. </html>
  501. `);
  502. printWindow.document.close();
  503. printWindow.onload = function () {
  504. setTimeout(() => {
  505. printWindow.print();
  506. }, 300);
  507. };
  508. }
  509. }
  510. };
  511. </script>
  512. <style lang="scss" scoped>
  513. .encoding-dialog {
  514. .encoding-header {
  515. display: flex;
  516. align-items: center;
  517. margin-bottom: 16px;
  518. gap: 10px;
  519. }
  520. .encoding-content {
  521. max-height: 60vh;
  522. overflow-y: auto;
  523. }
  524. .batch-card {
  525. margin-bottom: 16px;
  526. border-radius: 6px;
  527. overflow: hidden;
  528. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
  529. border: 1px solid #ebeef5;
  530. background: #fff;
  531. .card-header {
  532. min-height: 32px;
  533. background: linear-gradient(90deg, #1f2329 0%, #2c3138 100%);
  534. color: #fff;
  535. display: flex;
  536. align-items: center;
  537. justify-content: space-between;
  538. padding: 6px 12px;
  539. .card-title {
  540. flex: 1;
  541. font-size: 13px;
  542. font-weight: 600;
  543. letter-spacing: 1px;
  544. line-height: 1.4;
  545. word-break: break-all;
  546. }
  547. }
  548. .card-body {
  549. display: flex;
  550. align-items: stretch;
  551. }
  552. .info-area {
  553. flex: 1;
  554. display: flex;
  555. flex-direction: column;
  556. border-right: 1px dashed #dcdfe6;
  557. }
  558. .info-row {
  559. flex: 1;
  560. display: flex;
  561. align-items: center;
  562. padding: 4px 8px;
  563. border-bottom: 1px solid #f2f3f5;
  564. min-height: 22px;
  565. &:last-child {
  566. border-bottom: none;
  567. }
  568. .label {
  569. width: 70px;
  570. font-size: 10px;
  571. color: #909399;
  572. flex-shrink: 0;
  573. }
  574. .value {
  575. flex: 1;
  576. font-size: 10px;
  577. color: #1f2329;
  578. font-weight: 500;
  579. word-break: break-all;
  580. }
  581. }
  582. .qr-area {
  583. width: 110px;
  584. display: flex;
  585. flex-direction: column;
  586. align-items: center;
  587. justify-content: center;
  588. padding: 8px;
  589. background: #fafbfc;
  590. .qr-box {
  591. width: 86px;
  592. height: 86px;
  593. padding: 4px;
  594. background: #fff;
  595. border: 1px solid #ebeef5;
  596. border-radius: 4px;
  597. display: flex;
  598. align-items: center;
  599. justify-content: center;
  600. .qr-img {
  601. width: 100%;
  602. height: 100%;
  603. }
  604. }
  605. }
  606. }
  607. .barcode-card {
  608. position: relative;
  609. border: 1px solid #ebeef5;
  610. border-radius: 8px;
  611. padding: 0 0 16px;
  612. margin-bottom: 16px;
  613. text-align: center;
  614. background: #fff;
  615. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
  616. overflow: hidden;
  617. .barcode-card-header {
  618. display: flex;
  619. align-items: center;
  620. justify-content: space-between;
  621. padding: 8px 14px;
  622. background: linear-gradient(90deg, #1f2329 0%, #2c3138 100%);
  623. color: #fff;
  624. .barcode-card-title {
  625. font-size: 14px;
  626. font-weight: 600;
  627. letter-spacing: 1px;
  628. text-align: left;
  629. flex: 1;
  630. word-break: break-all;
  631. }
  632. }
  633. .barcode-meta {
  634. display: flex;
  635. justify-content: center;
  636. gap: 20px;
  637. padding: 10px 14px 4px;
  638. flex-wrap: wrap;
  639. .meta-item {
  640. display: flex;
  641. align-items: center;
  642. font-size: 12px;
  643. .meta-label {
  644. color: #909399;
  645. margin-right: 6px;
  646. }
  647. .meta-value {
  648. color: #1f2329;
  649. font-weight: 600;
  650. }
  651. }
  652. }
  653. .barcode-wrap {
  654. margin: 6px auto 4px;
  655. display: flex;
  656. justify-content: center;
  657. padding: 4px 12px;
  658. background: #fff;
  659. canvas {
  660. max-width: 100%;
  661. }
  662. }
  663. .barcode-raw {
  664. margin-top: 4px;
  665. padding: 0 14px;
  666. font-size: 11px;
  667. color: #909399;
  668. word-break: break-all;
  669. line-height: 1.5;
  670. }
  671. }
  672. }
  673. </style>