You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

773 lines
20 KiB

7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
  1. <template>
  2. <div class="pda-container">
  3. <!-- 头部栏 -->
  4. <div class="header-bar">
  5. <div class="header-left" @click="$router.back()">
  6. <i class="el-icon-arrow-left"></i>
  7. <span>拆合组托</span>
  8. </div>
  9. <div class="header-right" @click="$router.push({ path: '/' })">
  10. 首页
  11. </div>
  12. </div>
  13. <!-- 搜索框 -->
  14. <div class="search-container">
  15. <el-input
  16. v-model="scanCode"
  17. placeholder="请扫描标签条码"
  18. prefix-icon="el-icon-search"
  19. @keyup.enter.native="handleScan"
  20. ref="scanInput"
  21. clearable
  22. />
  23. </div>
  24. <!-- 标签信息卡片 -->
  25. <div class="label-info-card" v-if="currentLabel.labelCode">
  26. <div class="info-row">
  27. <span class="info-label">标签条码</span>
  28. <span class="info-value">{{ currentLabel.labelCode || ''}}</span>
  29. </div>
  30. <div class="info-row">
  31. <span class="info-label">标签类型</span>
  32. <span class="info-value">{{ currentLabel.labelType || '' }}</span>
  33. </div>
  34. <div class="info-row">
  35. <span class="info-label">标签张数</span>
  36. <span class="info-value">{{ currentLabel.labelQty || ''}}</span>
  37. </div>
  38. <div class="info-row">
  39. <span class="info-label">标签数量</span>
  40. <span class="info-value">{{ currentLabel.qtyOnHand || ''}}</span>
  41. </div>
  42. <div class="info-row">
  43. <span class="info-label">所在仓库</span>
  44. <span class="info-value">{{ currentLabel.warehouseName || ''}}</span>
  45. </div>
  46. <div class="info-row">
  47. <span class="info-label">所在库位</span>
  48. <span class="info-value">{{ currentLabel.locationId || ''}}</span>
  49. </div>
  50. <div class="info-row">
  51. <span class="info-label">标签状态</span>
  52. <span class="info-value" :class="currentLabel.status === '冻结' ? 'status-frozen' : 'status-normal'">{{ currentLabel.status || ''}}</span>
  53. </div>
  54. </div>
  55. <!-- 操作按钮 -->
  56. <div class="action-buttons" v-if="currentLabel.labelCode">
  57. <button class="action-btn split-btn" @click="showSplitDialog">
  58. 拆分
  59. </button>
  60. <button class="action-btn merge-btn" @click="showMergeDialog">
  61. 合并
  62. </button>
  63. </div>
  64. <!-- 拆分对话框 -->
  65. <div v-if="splitDialogVisible" class="dialog-overlay">
  66. <div class="dialog-modal">
  67. <div class="dialog-header">
  68. <span class="dialog-title">标签的拆分</span>
  69. </div>
  70. <div class="dialog-body">
  71. <div class="split-input-section">
  72. <el-input v-model="splitQuantity" placeholder="请输入拆分数量" type="number" class="split-input inlineNumber numInput"/>
  73. </div>
  74. </div>
  75. <div class="dialog-footer">
  76. <button class="btn-split" @click="confirmSplit" :disabled="!splitQuantity || splitQuantity <= 0">
  77. 拆分
  78. </button>
  79. <button class="btn-cancel" @click="closeSplitDialog">
  80. 取消
  81. </button>
  82. </div>
  83. </div>
  84. </div>
  85. <!-- 合并对话框 -->
  86. <div v-if="mergeDialogVisible" class="dialog-overlay">
  87. <div class="dialog-modal">
  88. <div class="dialog-header">
  89. <span class="dialog-title">标签的合并</span>
  90. </div>
  91. <div class="dialog-body">
  92. <div class="merge-input-section">
  93. <el-input
  94. v-model="mergeTargetCode"
  95. placeholder="请扫描合并标签"
  96. prefix-icon="el-icon-search"
  97. class="merge-input"
  98. ref="mergeInput"
  99. />
  100. </div>
  101. </div>
  102. <div class="dialog-footer">
  103. <button class="btn-merge" @click="confirmMerge" :disabled="!mergeTargetCode.trim()">
  104. 合并
  105. </button>
  106. <button class="btn-cancel" @click="closeMergeDialog">
  107. 取消
  108. </button>
  109. </div>
  110. </div>
  111. </div>
  112. </div>
  113. </template>
  114. <script>
  115. import { getStockInfoByLabelCode, splitLabel, mergeLabel, getUserDefaultPrinter } from "@/api/label-split-merge/label-split-merge.js";
  116. import { getCurrentWarehouse } from '@/utils'
  117. import getLodop from '@/utils/LodopFuncs.js';
  118. import labelPrintTemplates from '@/mixins/labelPrintTemplates.js';
  119. export default {
  120. mixins: [labelPrintTemplates],
  121. data() {
  122. return {
  123. scanCode: '',
  124. currentLabel: {},
  125. splitDialogVisible: false,
  126. mergeDialogVisible: false,
  127. splitQuantity: '',
  128. mergeTargetCode: '',
  129. mergeTargetLabel: {}
  130. };
  131. },
  132. methods: {
  133. // 处理扫描
  134. handleScan() {
  135. if (!this.scanCode.trim()) {
  136. return;
  137. }
  138. this.getStockInfo(this.scanCode.trim());
  139. this.scanCode = '';
  140. },
  141. // 获取库存信息
  142. getStockInfo(labelCode) {
  143. const params = {
  144. labelCode: labelCode,
  145. site: localStorage.getItem('site'),
  146. warehouseId: getCurrentWarehouse()
  147. };
  148. getStockInfoByLabelCode(params).then(({ data }) => {
  149. if (data && data.code === 0) {
  150. this.currentLabel = data.data;
  151. // this.$message.success('获取标签信息成功');
  152. } else {
  153. this.$message.error(data.msg || '未找到该标签的库存信息');
  154. this.currentLabel = {};
  155. }
  156. }).catch(error => {
  157. console.error('获取库存信息失败:', error);
  158. this.$message.error('获取库存信息失败');
  159. this.currentLabel = {};
  160. });
  161. },
  162. // 显示拆分对话框
  163. showSplitDialog() {
  164. if (this.currentLabel.qtyOnHand <= 1) {
  165. this.$message.warning('标签数量必须大于1才能拆分');
  166. return;
  167. }
  168. this.splitDialogVisible = true;
  169. this.splitQuantity = '';
  170. },
  171. // 关闭拆分对话框
  172. closeSplitDialog() {
  173. this.splitDialogVisible = false;
  174. this.splitQuantity = '';
  175. },
  176. // 确认拆分
  177. confirmSplit() {
  178. const splitQty = parseFloat(this.splitQuantity);
  179. const currentQty = parseFloat(this.currentLabel.qtyOnHand);
  180. if (!splitQty || splitQty <= 0) {
  181. this.$message.warning('请输入有效的拆分数量');
  182. return;
  183. }
  184. if (splitQty >= currentQty) {
  185. this.$message.warning('拆分数量必须小于当前数量');
  186. return;
  187. }
  188. const params = {
  189. site: localStorage.getItem('site'),
  190. buNo: this.currentLabel.buNo,
  191. warehouseId: getCurrentWarehouse(),
  192. originalLabelCode: this.currentLabel.labelCode,
  193. wdr: this.currentLabel.wdr,
  194. partNo: this.currentLabel.partNo,
  195. batchNo: this.currentLabel.batchNo,
  196. locationId: this.currentLabel.locationId,
  197. originalQuantity: currentQty,
  198. splitQuantity: splitQty,
  199. labelTypeTb: this.currentLabel.labelTypeTb,
  200. labelType: this.currentLabel.labelType,
  201. freezeFlag: this.currentLabel.freezeFlag,
  202. manufactureDate: this.currentLabel.productionDate,
  203. expiredDate: this.currentLabel.expiryDate,
  204. orderref1: this.currentLabel.orderref1,
  205. orderref2: this.currentLabel.orderref2,
  206. orderref3: this.currentLabel.orderref3,
  207. status: this.currentLabel.status,
  208. statusTb: this.currentLabel.statusTb
  209. };
  210. splitLabel(params).then(async ({ data }) => {
  211. if (data && data.code === 0) {
  212. this.$message.success(`拆分成功!新标签: ${data.data.newLabelCode}`);
  213. this.closeSplitDialog();
  214. // 自动打印标签(拆分打印两张:原标签和新标签)
  215. const printList = data.data.printList || [];
  216. if (printList.length > 0) {
  217. await this.printLabelsWithTemplate(printList);
  218. }
  219. // 刷新当前标签信息
  220. this.getStockInfo(this.currentLabel.labelCode);
  221. } else {
  222. this.$message.error(data.msg || '拆分失败');
  223. }
  224. }).catch(error => {
  225. console.error('拆分失败:', error);
  226. this.$message.error('拆分失败');
  227. });
  228. },
  229. // 显示合并对话框
  230. showMergeDialog() {
  231. this.mergeDialogVisible = true;
  232. this.mergeTargetCode = '';
  233. this.mergeTargetLabel = {};
  234. this.$nextTick(() => {
  235. if (this.$refs.mergeInput) {
  236. this.$refs.mergeInput.focus();
  237. }
  238. });
  239. },
  240. // 关闭合并对话框
  241. closeMergeDialog() {
  242. this.mergeDialogVisible = false;
  243. this.mergeTargetCode = '';
  244. this.mergeTargetLabel = {};
  245. },
  246. // 确认合并
  247. confirmMerge() {
  248. if (!this.mergeTargetCode.trim()) {
  249. this.$message.warning('请扫描目标标签');
  250. return;
  251. }
  252. if (this.mergeTargetCode.trim() === this.currentLabel.labelCode) {
  253. this.$message.warning('不能合并到自己');
  254. return;
  255. }
  256. // 在合并前再次验证目标标签
  257. const params = {
  258. labelCode: this.mergeTargetCode.trim(),
  259. site: localStorage.getItem('site'),
  260. warehouseId: getCurrentWarehouse()
  261. };
  262. getStockInfoByLabelCode(params).then(({ data }) => {
  263. if (data && data.code === 0) {
  264. const targetLabel = data.data;
  265. // 检查是否可以合并(同物料、同批次)
  266. if (targetLabel.partNo !== this.currentLabel.partNo) {
  267. this.$message.error('不同物料不能合并');
  268. return;
  269. }
  270. if (targetLabel.batchNo !== this.currentLabel.batchNo) {
  271. this.$message.error('不同批次不能合并');
  272. return;
  273. }
  274. // 验证通过,执行合并
  275. const mergeParams = {
  276. site: localStorage.getItem('site'),
  277. buNo: this.currentLabel.buNo,
  278. warehouseId: getCurrentWarehouse(),
  279. targetLabelCode: targetLabel.labelCode,
  280. sourceLabelCode: this.currentLabel.labelCode,
  281. partNo: this.currentLabel.partNo,
  282. batchNo: this.currentLabel.batchNo,
  283. locationId: this.currentLabel.locationId,
  284. targetQuantity: parseFloat(targetLabel.qtyOnHand),
  285. sourceQuantity: parseFloat(this.currentLabel.qtyOnHand),
  286. status: this.currentLabel.status,
  287. statusTb: this.currentLabel.statusTb
  288. };
  289. mergeLabel(mergeParams).then(async ({ data }) => {
  290. if (data && data.code === 0) {
  291. this.$message.success('合并成功!');
  292. this.closeMergeDialog();
  293. // 自动打印标签(合并只打印一张:源标签/合并后的标签)
  294. const printList = data.data.printList || [];
  295. if (printList.length > 0) {
  296. await this.printLabelsWithTemplate(printList);
  297. }
  298. // 清空当前标签信息,因为已经合并出库
  299. this.currentLabel = {};
  300. // 聚焦扫描框
  301. this.$nextTick(() => {
  302. if (this.$refs.scanInput) {
  303. this.$refs.scanInput.focus();
  304. }
  305. });
  306. } else {
  307. this.$message.error(data.msg || '合并失败');
  308. }
  309. }).catch(error => {
  310. console.error('合并失败:', error);
  311. this.$message.error('合并失败');
  312. });
  313. } else {
  314. this.$message.error(data.msg || '未找到目标标签的库存信息');
  315. }
  316. }).catch(error => {
  317. console.error('获取目标标签信息失败:', error);
  318. this.$message.error('获取目标标签信息失败');
  319. });
  320. },
  321. /**
  322. * 获取用户默认打印机配置
  323. */
  324. async fetchUserDefaultPrinter(labelNo) {
  325. try {
  326. const params = {
  327. userName: localStorage.getItem('userName'),
  328. labelNo: labelNo || ''
  329. };
  330. const { data } = await getUserDefaultPrinter(params);
  331. if (data && data.code === 0 && data.printerName) {
  332. return {
  333. printerName: data.printerName,
  334. printerIp: data.printerIp,
  335. labelNo: data.labelNo
  336. };
  337. }
  338. return null;
  339. } catch (error) {
  340. console.error('获取用户打印机配置失败:', error);
  341. return null;
  342. }
  343. },
  344. /**
  345. * 使用模板打印标签
  346. * @param {Array} printList - 打印数据列表存储过程UspPartLabelTemplate返回
  347. */
  348. async printLabelsWithTemplate(printList) {
  349. try {
  350. // 1. 获取 LODOP 打印控件
  351. const LODOP = getLodop();
  352. if (!LODOP) {
  353. console.warn('无法连接到打印控件,跳过打印');
  354. this.$message.warning('无法连接到打印控件,请确保已安装并启动打印服务');
  355. return;
  356. }
  357. // 2. 检测打印机数量
  358. const printerCount = LODOP.GET_PRINTER_COUNT();
  359. if (printerCount === 0) {
  360. console.warn('未检测到打印机,跳过打印');
  361. this.$message.warning('未检测到打印机');
  362. return;
  363. }
  364. // 3. 获取用户配置的打印机
  365. const firstLabel = printList[0] || {};
  366. const printerConfig = await this.fetchUserDefaultPrinter(firstLabel.labelNo);
  367. let printerName = null;
  368. if (printerConfig && printerConfig.printerName) {
  369. printerName = printerConfig.printerName;
  370. console.log('使用用户配置的打印机:', printerName);
  371. } else {
  372. console.warn('未找到用户打印机配置,跳过打印');
  373. this.$message.warning('未配置用户打印机,请在系统中配置默认打印机后再打印');
  374. return;
  375. }
  376. // 4. 执行打印
  377. await this.executePrintWithTemplate(LODOP, printList, printerName);
  378. this.$message.success('标签打印任务已发送!');
  379. } catch (error) {
  380. console.error('模板打印失败:', error);
  381. this.$message.warning('标签打印失败,请手动打印');
  382. }
  383. },
  384. /**
  385. * 执行模板打印
  386. * @param {Object} LODOP - 打印控件对象
  387. * @param {Array} printDataList - 打印数据列表
  388. * @param {String} printerName - 用户配置的打印机名称可选
  389. */
  390. async executePrintWithTemplate(LODOP, printDataList, printerName) {
  391. console.log('开始打印,标签数量:', printDataList.length, '打印机:', printerName || '默认', '标签数据:', printDataList);
  392. // 循环打印每个标签(每个标签单独打印一次)
  393. for (let i = 0; i < printDataList.length; i++) {
  394. const printData = printDataList[i];
  395. // 获取标签模板编号(存储过程返回)
  396. const labelNo = printData.labelNo;
  397. // 每个标签单独初始化一个打印任务
  398. LODOP.PRINT_INIT('拆合组托标签打印_' + (i + 1));
  399. // 设置用户配置的打印机(如果有)
  400. if (printerName) {
  401. LODOP.SET_PRINTER_INDEX(printerName);
  402. }
  403. // 设置打印模式
  404. LODOP.SET_PRINT_MODE("PRINT_NOCOLLATE", true);
  405. // 根据标签模板编号调用对应的打印方法
  406. if (labelNo === 'A001') {
  407. await this.printLabelA001(LODOP, printData, false);
  408. } else if (labelNo === 'A002') {
  409. this.printLabelA002(LODOP, printData, false);
  410. } else if (labelNo === 'A003') {
  411. this.printLabelA003(LODOP, printData, false);
  412. } else if (labelNo === 'A004') {
  413. this.printLabelA004(LODOP, printData, false);
  414. } else {
  415. // 默认使用 A001 模板
  416. console.warn('未知标签模板:', labelNo, ',使用默认模板 A001');
  417. await this.printLabelA001(LODOP, printData, false);
  418. }
  419. // 执行打印
  420. LODOP.PRINT();
  421. }
  422. }
  423. },
  424. mounted() {
  425. // 聚焦扫描框
  426. this.$nextTick(() => {
  427. if (this.$refs.scanInput) {
  428. this.$refs.scanInput.focus();
  429. }
  430. });
  431. }
  432. };
  433. </script>
  434. <style scoped>
  435. .pda-container {
  436. width: 100vw;
  437. height: 100vh;
  438. display: flex;
  439. flex-direction: column;
  440. background: #f5f5f5;
  441. }
  442. /* 头部栏 */
  443. .header-bar {
  444. display: flex;
  445. justify-content: space-between;
  446. align-items: center;
  447. padding: 8px 16px;
  448. background: #17B3A3;
  449. color: white;
  450. height: 40px;
  451. min-height: 40px;
  452. }
  453. .header-left {
  454. display: flex;
  455. align-items: center;
  456. cursor: pointer;
  457. font-size: 16px;
  458. font-weight: 500;
  459. }
  460. .header-left i {
  461. margin-right: 8px;
  462. font-size: 18px;
  463. }
  464. .header-right {
  465. cursor: pointer;
  466. font-size: 16px;
  467. font-weight: 500;
  468. }
  469. /* 搜索容器 */
  470. .search-container {
  471. padding: 12px 16px;
  472. background: white;
  473. }
  474. .search-container .el-input {
  475. width: 100%;
  476. }
  477. /* 标签信息卡片 */
  478. .label-info-card {
  479. background: white;
  480. margin: 8px 16px;
  481. padding: 16px;
  482. border-radius: 8px;
  483. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  484. flex: 1;
  485. overflow-y: auto;
  486. }
  487. .info-row {
  488. display: flex;
  489. align-items: center;
  490. margin-bottom: 16px;
  491. min-height: 40px;
  492. }
  493. .info-label {
  494. width: 80px;
  495. font-size: 14px;
  496. color: #333;
  497. font-weight: 500;
  498. flex-shrink: 0;
  499. }
  500. .info-value {
  501. flex: 1;
  502. font-size: 14px;
  503. color: #666;
  504. margin-left: 12px;
  505. }
  506. .status-frozen {
  507. color: #ff4949;
  508. font-weight: 500;
  509. }
  510. .status-normal {
  511. color: #17B3A3;
  512. font-weight: 500;
  513. }
  514. /* 操作按钮 */
  515. .action-buttons {
  516. display: flex;
  517. padding: 16px;
  518. gap: 12px;
  519. background: white;
  520. margin-top: auto;
  521. }
  522. .action-btn {
  523. flex: 1;
  524. padding: 12px;
  525. border-radius: 20px;
  526. font-size: 14px;
  527. cursor: pointer;
  528. transition: all 0.2s ease;
  529. border: none;
  530. }
  531. .split-btn {
  532. background: #17B3A3;
  533. color: white;
  534. }
  535. .split-btn:hover {
  536. background: #0d8f7f;
  537. }
  538. .merge-btn {
  539. background: white;
  540. color: #17B3A3;
  541. border: 1px solid #17B3A3;
  542. }
  543. .merge-btn:hover {
  544. background: #17B3A3;
  545. color: white;
  546. }
  547. .action-btn:active {
  548. transform: scale(0.98);
  549. }
  550. /* 对话框样式 */
  551. .dialog-overlay {
  552. position: fixed;
  553. top: 0;
  554. left: 0;
  555. right: 0;
  556. bottom: 0;
  557. background: rgba(0, 0, 0, 0.5);
  558. z-index: 9999;
  559. display: flex;
  560. align-items: center;
  561. justify-content: center;
  562. padding: 20px;
  563. }
  564. .dialog-modal {
  565. background: white;
  566. border-radius: 12px;
  567. width: 100%;
  568. max-width: 400px;
  569. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
  570. overflow: hidden;
  571. }
  572. .dialog-header {
  573. background: #17B3A3;
  574. color: white;
  575. padding: 16px 20px;
  576. text-align: center;
  577. }
  578. .dialog-title {
  579. font-size: 16px;
  580. font-weight: 500;
  581. }
  582. .dialog-body {
  583. padding: 20px;
  584. }
  585. .split-input-section,
  586. .merge-input-section {
  587. margin-bottom: 20px;
  588. }
  589. .split-input,
  590. .merge-input {
  591. width: 100%;
  592. }
  593. .split-input ::v-deep .el-input__inner,
  594. .numInput /deep/ .el-input__inner{
  595. text-align: right;
  596. }
  597. /deep/ .inlineNumber input::-webkit-outer-spin-button,
  598. /deep/ .inlineNumber input::-webkit-inner-spin-button {
  599. -webkit-appearance: none;
  600. }
  601. /deep/ .inlineNumber input[type="number"]{
  602. -moz-appearance: textfield;
  603. padding-right: 5px !important;
  604. }
  605. .merge-input ::v-deep .el-input__inner {
  606. height: 48px;
  607. border: 1px solid #17B3A3;
  608. border-radius: 8px;
  609. font-size: 16px;
  610. text-align: center;
  611. }
  612. .dialog-footer {
  613. padding: 16px 20px;
  614. display: flex;
  615. justify-content: center;
  616. gap: 12px;
  617. border-top: 1px solid #f0f0f0;
  618. }
  619. .btn-split,
  620. .btn-merge,
  621. .btn-cancel {
  622. padding: 10px 20px;
  623. border-radius: 6px;
  624. font-size: 14px;
  625. cursor: pointer;
  626. transition: all 0.2s;
  627. border: none;
  628. outline: none;
  629. }
  630. .btn-split,
  631. .btn-merge {
  632. background: #17B3A3;
  633. color: white;
  634. }
  635. .btn-split:hover:not(:disabled),
  636. .btn-merge:hover:not(:disabled) {
  637. background: #0d8f7f;
  638. }
  639. .btn-split:disabled,
  640. .btn-merge:disabled {
  641. background: #c0c4cc;
  642. cursor: not-allowed;
  643. }
  644. .btn-cancel {
  645. background: #f5f5f5;
  646. color: #666;
  647. }
  648. .btn-cancel:hover {
  649. background: #e6e6e6;
  650. }
  651. /* 响应式设计 */
  652. @media (max-width: 360px) {
  653. .header-bar {
  654. padding: 8px 12px;
  655. }
  656. .search-container {
  657. padding: 8px 12px;
  658. }
  659. .label-info-card {
  660. margin: 6px 12px;
  661. padding: 12px;
  662. }
  663. .info-label {
  664. width: 70px;
  665. font-size: 13px;
  666. }
  667. .info-value {
  668. font-size: 13px;
  669. }
  670. .action-buttons {
  671. padding: 12px;
  672. }
  673. }
  674. </style>