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.

771 lines
20 KiB

5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 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 {
  413. // 默认使用 A001 模板
  414. console.warn('未知标签模板:', labelNo, ',使用默认模板 A001');
  415. await this.printLabelA001(LODOP, printData, false);
  416. }
  417. // 执行打印
  418. LODOP.PRINT();
  419. }
  420. }
  421. },
  422. mounted() {
  423. // 聚焦扫描框
  424. this.$nextTick(() => {
  425. if (this.$refs.scanInput) {
  426. this.$refs.scanInput.focus();
  427. }
  428. });
  429. }
  430. };
  431. </script>
  432. <style scoped>
  433. .pda-container {
  434. width: 100vw;
  435. height: 100vh;
  436. display: flex;
  437. flex-direction: column;
  438. background: #f5f5f5;
  439. }
  440. /* 头部栏 */
  441. .header-bar {
  442. display: flex;
  443. justify-content: space-between;
  444. align-items: center;
  445. padding: 8px 16px;
  446. background: #17B3A3;
  447. color: white;
  448. height: 40px;
  449. min-height: 40px;
  450. }
  451. .header-left {
  452. display: flex;
  453. align-items: center;
  454. cursor: pointer;
  455. font-size: 16px;
  456. font-weight: 500;
  457. }
  458. .header-left i {
  459. margin-right: 8px;
  460. font-size: 18px;
  461. }
  462. .header-right {
  463. cursor: pointer;
  464. font-size: 16px;
  465. font-weight: 500;
  466. }
  467. /* 搜索容器 */
  468. .search-container {
  469. padding: 12px 16px;
  470. background: white;
  471. }
  472. .search-container .el-input {
  473. width: 100%;
  474. }
  475. /* 标签信息卡片 */
  476. .label-info-card {
  477. background: white;
  478. margin: 8px 16px;
  479. padding: 16px;
  480. border-radius: 8px;
  481. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  482. flex: 1;
  483. overflow-y: auto;
  484. }
  485. .info-row {
  486. display: flex;
  487. align-items: center;
  488. margin-bottom: 16px;
  489. min-height: 40px;
  490. }
  491. .info-label {
  492. width: 80px;
  493. font-size: 14px;
  494. color: #333;
  495. font-weight: 500;
  496. flex-shrink: 0;
  497. }
  498. .info-value {
  499. flex: 1;
  500. font-size: 14px;
  501. color: #666;
  502. margin-left: 12px;
  503. }
  504. .status-frozen {
  505. color: #ff4949;
  506. font-weight: 500;
  507. }
  508. .status-normal {
  509. color: #17B3A3;
  510. font-weight: 500;
  511. }
  512. /* 操作按钮 */
  513. .action-buttons {
  514. display: flex;
  515. padding: 16px;
  516. gap: 12px;
  517. background: white;
  518. margin-top: auto;
  519. }
  520. .action-btn {
  521. flex: 1;
  522. padding: 12px;
  523. border-radius: 20px;
  524. font-size: 14px;
  525. cursor: pointer;
  526. transition: all 0.2s ease;
  527. border: none;
  528. }
  529. .split-btn {
  530. background: #17B3A3;
  531. color: white;
  532. }
  533. .split-btn:hover {
  534. background: #0d8f7f;
  535. }
  536. .merge-btn {
  537. background: white;
  538. color: #17B3A3;
  539. border: 1px solid #17B3A3;
  540. }
  541. .merge-btn:hover {
  542. background: #17B3A3;
  543. color: white;
  544. }
  545. .action-btn:active {
  546. transform: scale(0.98);
  547. }
  548. /* 对话框样式 */
  549. .dialog-overlay {
  550. position: fixed;
  551. top: 0;
  552. left: 0;
  553. right: 0;
  554. bottom: 0;
  555. background: rgba(0, 0, 0, 0.5);
  556. z-index: 9999;
  557. display: flex;
  558. align-items: center;
  559. justify-content: center;
  560. padding: 20px;
  561. }
  562. .dialog-modal {
  563. background: white;
  564. border-radius: 12px;
  565. width: 100%;
  566. max-width: 400px;
  567. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
  568. overflow: hidden;
  569. }
  570. .dialog-header {
  571. background: #17B3A3;
  572. color: white;
  573. padding: 16px 20px;
  574. text-align: center;
  575. }
  576. .dialog-title {
  577. font-size: 16px;
  578. font-weight: 500;
  579. }
  580. .dialog-body {
  581. padding: 20px;
  582. }
  583. .split-input-section,
  584. .merge-input-section {
  585. margin-bottom: 20px;
  586. }
  587. .split-input,
  588. .merge-input {
  589. width: 100%;
  590. }
  591. .split-input ::v-deep .el-input__inner,
  592. .numInput /deep/ .el-input__inner{
  593. text-align: right;
  594. }
  595. /deep/ .inlineNumber input::-webkit-outer-spin-button,
  596. /deep/ .inlineNumber input::-webkit-inner-spin-button {
  597. -webkit-appearance: none;
  598. }
  599. /deep/ .inlineNumber input[type="number"]{
  600. -moz-appearance: textfield;
  601. padding-right: 5px !important;
  602. }
  603. .merge-input ::v-deep .el-input__inner {
  604. height: 48px;
  605. border: 1px solid #17B3A3;
  606. border-radius: 8px;
  607. font-size: 16px;
  608. text-align: center;
  609. }
  610. .dialog-footer {
  611. padding: 16px 20px;
  612. display: flex;
  613. justify-content: center;
  614. gap: 12px;
  615. border-top: 1px solid #f0f0f0;
  616. }
  617. .btn-split,
  618. .btn-merge,
  619. .btn-cancel {
  620. padding: 10px 20px;
  621. border-radius: 6px;
  622. font-size: 14px;
  623. cursor: pointer;
  624. transition: all 0.2s;
  625. border: none;
  626. outline: none;
  627. }
  628. .btn-split,
  629. .btn-merge {
  630. background: #17B3A3;
  631. color: white;
  632. }
  633. .btn-split:hover:not(:disabled),
  634. .btn-merge:hover:not(:disabled) {
  635. background: #0d8f7f;
  636. }
  637. .btn-split:disabled,
  638. .btn-merge:disabled {
  639. background: #c0c4cc;
  640. cursor: not-allowed;
  641. }
  642. .btn-cancel {
  643. background: #f5f5f5;
  644. color: #666;
  645. }
  646. .btn-cancel:hover {
  647. background: #e6e6e6;
  648. }
  649. /* 响应式设计 */
  650. @media (max-width: 360px) {
  651. .header-bar {
  652. padding: 8px 12px;
  653. }
  654. .search-container {
  655. padding: 8px 12px;
  656. }
  657. .label-info-card {
  658. margin: 6px 12px;
  659. padding: 12px;
  660. }
  661. .info-label {
  662. width: 70px;
  663. font-size: 13px;
  664. }
  665. .info-value {
  666. font-size: 13px;
  667. }
  668. .action-buttons {
  669. padding: 12px;
  670. }
  671. }
  672. </style>