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.

782 lines
19 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
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>
  3. <div class="pda-container">
  4. <div class="status-bar">
  5. <div class="goBack" @click="$router.back()"><i class="el-icon-arrow-left"></i>上一页</div>
  6. <div class="goBack">库存移库</div>
  7. <div class="network" style="color: #fff" @click="$router.push({ path: '/' })">🏠首页</div>
  8. </div>
  9. <div style="overflow-y: auto">
  10. <!-- 搜索框 -->
  11. <div class="search-container">
  12. <el-input clearable class="compact-input"
  13. v-model="scanCode"
  14. placeholder="请扫描HandlingUnit条码"
  15. prefix-icon="el-icon-search"
  16. @keyup.enter.native="handleScan"
  17. ref="scanInput"
  18. />
  19. <div class="mode-switch">
  20. <el-switch
  21. class="custom-switch"
  22. v-model="isRemoveMode"
  23. active-color="#ff4949"
  24. inactive-color="#13ce66">
  25. </el-switch>
  26. <span v-if="isRemoveMode" class="switch-text">{{ '移除' }}</span>
  27. <span v-else class="switch-text2">{{ '添加' }}</span>
  28. </div>
  29. </div>
  30. <!-- 移库信息卡片 -->
  31. <div class="material-info-card">
  32. <div class="input-form">
  33. <div class="form-row">
  34. <div class="form-item">
  35. <label class="form-label">目标库位</label>
  36. <el-input
  37. v-model="moveForm.targetLocationId"
  38. placeholder="请扫描或输入目标库位"
  39. size="small"
  40. @keyup.enter.native="handleLocationScan"
  41. ref="locationInput">
  42. </el-input>
  43. </div>
  44. </div>
  45. </div>
  46. </div>
  47. <!-- 移库信息确认标题 -->
  48. <div class="section-title">
  49. <div class="title-left">
  50. <i class="el-icon-box"></i>
  51. <span>移库信息确认</span>
  52. </div>
  53. </div>
  54. <!-- 扫描的HandlingUnit明细列表 -->
  55. <div class="scanned-items" v-if="scannedItems.length > 0" style="margin: 2px;">
  56. <div class="label-list">
  57. <el-form label-position="top" style="margin: 3px;">
  58. <el-row :gutter="5"
  59. v-for="(label, index) in scannedItems"
  60. :key="label.id"
  61. :class="index < scannedItems.length - 1 ? 'bottom-line-row' : ''"
  62. style="border: 1px solid #e0e0e0; border-radius: 4px; margin-bottom: 8px; padding: 8px;">
  63. <el-col :span="16">
  64. <el-form-item label="HandlingUnit">
  65. <span>{{ label.unitId }}</span>
  66. </el-form-item>
  67. </el-col>
  68. <el-col :span="8">
  69. <el-form-item label="物料编码">
  70. <span>{{ label.partNo }}</span>
  71. </el-form-item>
  72. </el-col>
  73. <el-col :span="24" v-if="label.partDesc">
  74. <el-form-item label="物料描述">
  75. <span>{{ label.partDesc }}</span>
  76. </el-form-item>
  77. </el-col>
  78. <el-col :span="10" v-if="label.batchNo">
  79. <el-form-item label="批次号">
  80. <span>{{ label.batchNo }}</span>
  81. </el-form-item>
  82. </el-col>
  83. <el-col :span="10" v-if="label.locationId">
  84. <el-form-item label="当前库位">
  85. <span>{{ label.locationId }}</span>
  86. </el-form-item>
  87. </el-col>
  88. <el-col :span="4">
  89. <el-form-item label="数量">
  90. <span>{{ label.qty }} {{ label.unit || '个' }}</span>
  91. </el-form-item>
  92. </el-col>
  93. <el-col :span="12" v-if="label.expiredDate">
  94. <el-form-item label="过期日期">
  95. <span>{{ formatDate(label.expiredDate) }}</span>
  96. </el-form-item>
  97. </el-col>
  98. </el-row>
  99. </el-form>
  100. </div>
  101. </div>
  102. <!-- 空状态 -->
  103. <div v-if="scannedItems.length === 0" class="empty-labels">
  104. <div style="text-align: center; padding: 20px;">
  105. <p style="color: #999; margin: 0;">暂无扫描HandlingUnit</p>
  106. </div>
  107. </div>
  108. <!-- 底部操作按钮 -->
  109. <div class="bottom-actions" v-if="scannedItems.length > 0">
  110. <button class="action-btn primary" @click="confirmMove" :disabled="loading">
  111. <i v-if="loading" class="el-icon-loading"></i>
  112. {{ loading ? '移库中...' : '确认移库' }}
  113. </button>
  114. <button class="action-btn secondary" style="margin-left: 10px;" @click="cancelProcess" :disabled="loading">
  115. 取消
  116. </button>
  117. </div>
  118. </div>
  119. </div>
  120. </div>
  121. </template>
  122. <script>
  123. import { scanHandlingUnitLabel, confirmInventoryMove } from '@/api/po/po.js';
  124. export default {
  125. data() {
  126. return {
  127. scanCode: '',
  128. isRemoveMode: false, // 默认为添加模式
  129. moveForm: {
  130. moveReason: '',
  131. targetLocationId: ''
  132. },
  133. scannedItems: [],
  134. site: localStorage.getItem('site') || 'SITE01',
  135. loading: false
  136. };
  137. },
  138. methods: {
  139. // 处理扫描
  140. handleScan() {
  141. if (!this.scanCode.trim()) {
  142. return;
  143. }
  144. if (this.isRemoveMode) {
  145. this.removeLabelByCode(this.scanCode.trim());
  146. } else {
  147. this.validateAndAddLabel(this.scanCode.trim());
  148. }
  149. this.scanCode = '';
  150. },
  151. // 处理库位扫描
  152. handleLocationScan() {
  153. if (!this.moveForm.targetLocationId.trim()) {
  154. return;
  155. }
  156. // 失去光标
  157. if (this.$refs.locationInput) {
  158. this.$refs.locationInput.blur();
  159. }
  160. // 定位到页面最底部
  161. this.$nextTick(() => {
  162. // 方法1:尝试滚动到底部操作按钮
  163. const bottomActions = document.querySelector('.bottom-actions');
  164. if (bottomActions) {
  165. bottomActions.scrollIntoView({
  166. behavior: 'smooth',
  167. block: 'end'
  168. });
  169. return;
  170. }
  171. // 方法2:查找滚动容器并滚动
  172. const scrollContainer = document.querySelector('.pda-container > div[style*="overflow-y"]') ||
  173. document.querySelector('.pda-container > div:nth-child(2)');
  174. if (scrollContainer) {
  175. scrollContainer.scrollTo({
  176. top: scrollContainer.scrollHeight,
  177. behavior: 'smooth'
  178. });
  179. return;
  180. }
  181. // 方法3:备用方案 - 滚动整个页面
  182. setTimeout(() => {
  183. window.scrollTo({
  184. top: document.documentElement.scrollHeight,
  185. behavior: 'smooth'
  186. });
  187. }, 100);
  188. });
  189. },
  190. // 验证标签并添加到列表
  191. validateAndAddLabel(unitId) {
  192. const params = {
  193. unitId: unitId,
  194. site: this.site,
  195. };
  196. scanHandlingUnitLabel(params).then(({ data }) => {
  197. if (data && data.code === 0 && data.data) {
  198. const huInfo = data.data;
  199. this.processHandlingUnit(unitId, huInfo);
  200. } else {
  201. this.$message.error(data.msg || 'HandlingUnit不存在或查询失败');
  202. }
  203. }).catch(error => {
  204. this.$message.error('扫描失败');
  205. });
  206. },
  207. // 处理HandlingUnit数据
  208. processHandlingUnit(unitId, huInfo) {
  209. // 检查是否已经扫描过
  210. const exists = this.scannedItems.find(item => item.unitId === unitId);
  211. if (exists) {
  212. this.$message.warning('该HandlingUnit已扫描,请勿重复扫描');
  213. return;
  214. }
  215. // 校验HU是否属于当前站点
  216. if (huInfo.site && huInfo.site !== this.site) {
  217. this.$message.error('HandlingUnit站点不匹配');
  218. return;
  219. }
  220. /* // 校验HU是否在库
  221. if (huInfo.inStockFlag !== 'Y') {
  222. this.$message.error('HandlingUnit不在库,无法移库');
  223. return;
  224. }*/
  225. // 校验过期日期一致性
  226. if (this.scannedItems.length > 0) {
  227. const firstItemExpiryDate = this.scannedItems[0].expiredDate;
  228. const currentExpiryDate = huInfo.expiredDate;
  229. // 如果两个日期都存在且不相等,则不允许添加
  230. if (firstItemExpiryDate && currentExpiryDate) {
  231. const firstDate = new Date(firstItemExpiryDate).toDateString();
  232. const currentDate = new Date(currentExpiryDate).toDateString();
  233. if (firstDate !== currentDate) {
  234. this.$message.error(`HandlingUnit的过期日期不一致,无法一起移库。已扫描的过期日期:${firstDate},当前扫描的过期日期:${currentDate}`);
  235. return;
  236. }
  237. }
  238. // 如果一个有过期日期,另一个没有,也不允许
  239. else if (firstItemExpiryDate || currentExpiryDate) {
  240. this.$message.error('HandlingUnit的过期日期不一致,部分有过期日期,部分没有,无法一起移库');
  241. return;
  242. }
  243. }
  244. // 添加到列表
  245. this.scannedItems.push({
  246. id: Date.now(),
  247. unitId: unitId,
  248. partNo: huInfo.partNo,
  249. partDesc: huInfo.partDesc,
  250. qty: huInfo.qty,
  251. unit: huInfo.unit,
  252. batchNo: huInfo.batchNo,
  253. locationId: huInfo.locationId,
  254. wdr: huInfo.wdr,
  255. expiredDate: huInfo.expiredDate
  256. });
  257. this.$message.success('扫描成功');
  258. },
  259. // 通过条码移除标签
  260. removeLabelByCode(unitId) {
  261. const index = this.scannedItems.findIndex(item => item.unitId === unitId);
  262. if (index !== -1) {
  263. this.scannedItems.splice(index, 1);
  264. this.$message.success('移除成功');
  265. } else {
  266. this.$message.warning('未找到该HandlingUnit');
  267. }
  268. },
  269. // 确认移库
  270. confirmMove() {
  271. // 验证必填字段
  272. if (!this.moveForm.targetLocationId) {
  273. this.$message.error('请输入目标库位');
  274. return;
  275. }
  276. if (this.scannedItems.length === 0) {
  277. this.$message.warning('请先扫描HandlingUnit');
  278. return;
  279. }
  280. // 验证目标库位与当前库位不同
  281. const sameLocationItems = this.scannedItems.filter(item =>
  282. item.locationId === this.moveForm.targetLocationId
  283. );
  284. if (sameLocationItems.length > 0) {
  285. this.$message.error('目标库位不能与当前库位相同');
  286. return;
  287. }
  288. this.$confirm('确认要进行移库操作吗?', '提示', {
  289. confirmButtonText: '确定',
  290. cancelButtonText: '取消',
  291. type: 'warning'
  292. }).then(() => {
  293. this.submitConfirmMove();
  294. }).catch(() => {
  295. // 用户取消
  296. });
  297. },
  298. // 提交确认移库
  299. submitConfirmMove() {
  300. const handlingUnitIds = this.scannedItems.map(item => item.unitId);
  301. const params = {
  302. site: this.site,
  303. moveReason: this.moveForm.moveReason,
  304. targetLocationId: this.moveForm.targetLocationId,
  305. handlingUnitIds: handlingUnitIds,
  306. scannedItems: this.scannedItems
  307. };
  308. // 开始loading
  309. this.loading = true;
  310. // 调用后端API
  311. confirmInventoryMove(params).then(({ data }) => {
  312. this.loading = false;
  313. if (data && data.code === 0) {
  314. this.$message({
  315. message: '移库成功!处理单元: ' + handlingUnitIds.length + '个',
  316. type: 'success',
  317. duration: 3000
  318. });
  319. // 清空页面内容,留在当前页面
  320. this.clearPageContent();
  321. } else {
  322. this.$message.error(data.msg || '移库失败');
  323. }
  324. }).catch(error => {
  325. this.loading = false;
  326. console.error('移库失败:', error);
  327. this.$message.error('移库失败,请重试');
  328. });
  329. },
  330. // 清空页面内容
  331. clearPageContent() {
  332. // 清空表单数据
  333. this.moveForm = {
  334. moveReason: '',
  335. targetLocationId: ''
  336. };
  337. // 清空已扫描的物料列表
  338. this.scannedItems = [];
  339. // 清空扫描框
  340. this.scanCode = '';
  341. // 重置为添加模式
  342. this.isRemoveMode = false;
  343. // 让扫描框重新获得焦点
  344. this.$nextTick(() => {
  345. if (this.$refs.scanInput) {
  346. this.$refs.scanInput.focus();
  347. }
  348. });
  349. },
  350. // 取消处理
  351. cancelProcess() {
  352. if (this.scannedItems.length > 0) {
  353. this.$confirm('取消后将清空已扫描的HandlingUnit,确定取消吗?', '提示', {
  354. confirmButtonText: '确定',
  355. cancelButtonText: '继续操作',
  356. type: 'warning'
  357. }).then(() => {
  358. this.$router.back();
  359. }).catch(() => {
  360. // 用户选择继续操作
  361. });
  362. } else {
  363. this.$router.back();
  364. }
  365. },
  366. // 初始化表单数据
  367. initFormData() {
  368. // 可以在这里设置其他默认值
  369. },
  370. // 格式化日期显示
  371. formatDate(dateString) {
  372. if (!dateString) return '';
  373. try {
  374. const date = new Date(dateString);
  375. return date.toLocaleDateString('zh-CN', {
  376. year: 'numeric',
  377. month: '2-digit',
  378. day: '2-digit'
  379. });
  380. } catch (error) {
  381. return dateString;
  382. }
  383. }
  384. },
  385. mounted() {
  386. // 初始化表单数据
  387. this.initFormData();
  388. // 聚焦扫描框
  389. this.$nextTick(() => {
  390. if (this.$refs.scanInput) {
  391. this.$refs.scanInput.focus();
  392. }
  393. });
  394. }
  395. };
  396. </script>
  397. <style scoped>
  398. /* 复用other-inbound.vue的样式 */
  399. .pda-container {
  400. width: 100%;
  401. height: 100vh;
  402. display: flex;
  403. flex-direction: column;
  404. overflow: hidden;
  405. }
  406. .status-bar {
  407. background: #17B3A3;
  408. color: white;
  409. padding: 8px 16px;
  410. display: flex;
  411. justify-content: space-between;
  412. align-items: center;
  413. height: 40px;
  414. min-height: 40px;
  415. }
  416. .goBack {
  417. cursor: pointer;
  418. font-size: 16px;
  419. font-weight: 500;
  420. }
  421. /* 搜索容器 */
  422. .search-container {
  423. padding: 12px 16px;
  424. background: white;
  425. display: flex;
  426. align-items: center;
  427. gap: 12px;
  428. }
  429. .search-container .el-input {
  430. width: 240px;
  431. margin-right: 12px;
  432. }
  433. /* 紧凑型输入框样式 */
  434. .compact-input ::v-deep .el-input__inner {
  435. height: 36px;
  436. padding: 0 12px 0 35px;
  437. font-size: 14px;
  438. }
  439. .compact-input ::v-deep .el-input__prefix {
  440. left: 10px;
  441. }
  442. .compact-input ::v-deep .el-input__suffix {
  443. right: 30px;
  444. }
  445. /* 模式切换开关 */
  446. .mode-switch {
  447. position: relative;
  448. display: inline-block;
  449. }
  450. .custom-switch {
  451. transform: scale(1.3);
  452. }
  453. /* 中间文字 */
  454. .switch-text {
  455. position: absolute;
  456. left: 25%;
  457. transform: translateX(-50%);
  458. top: 50%;
  459. transform: translateY(-50%) translateX(-50%);
  460. font-size: 12px;
  461. font-weight: 500;
  462. color: #606266;
  463. white-space: nowrap;
  464. pointer-events: none;
  465. z-index: 1;
  466. top: 53%;
  467. transform: translate(-50%, -50%);
  468. font-size: 12px;
  469. font-weight: bold;
  470. color: white;
  471. pointer-events: none;
  472. z-index: 2;
  473. }
  474. .switch-text2 {
  475. position: absolute;
  476. left: 75%;
  477. transform: translateX(-50%);
  478. top: 50%;
  479. transform: translateY(-50%) translateX(-50%);
  480. font-size: 12px;
  481. font-weight: 500;
  482. color: #606266;
  483. white-space: nowrap;
  484. pointer-events: none;
  485. z-index: 1;
  486. top: 53%;
  487. transform: translate(-50%, -50%);
  488. font-size: 12px;
  489. font-weight: bold;
  490. color: white;
  491. pointer-events: none;
  492. z-index: 2;
  493. }
  494. /* 调整 switch 尺寸以便容纳文字 */
  495. .custom-switch ::v-deep .el-switch__core {
  496. width: 60px;
  497. height: 28px;
  498. }
  499. /* 物料信息卡片 */
  500. .material-info-card {
  501. background: white;
  502. margin: 4px 16px;
  503. padding: 6px 20px;
  504. border-radius: 8px;
  505. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  506. border: 1px solid #f0f0f0;
  507. }
  508. .card-title {
  509. margin-bottom: 8px;
  510. display: flex;
  511. align-items: center;
  512. gap: 8px;
  513. }
  514. .title-label {
  515. font-size: 12px;
  516. color: #666;
  517. font-weight: 500;
  518. }
  519. .title-value {
  520. font-size: 16px;
  521. font-weight: bold;
  522. color: #333;
  523. line-height: 1.2;
  524. }
  525. /* 表单样式 */
  526. .input-form {
  527. margin-top: 8px;
  528. }
  529. .form-row {
  530. display: flex;
  531. gap: 12px;
  532. margin-bottom: 12px;
  533. }
  534. .form-row:last-child {
  535. margin-bottom: 0;
  536. }
  537. .form-item {
  538. flex: 1;
  539. display: flex;
  540. flex-direction: column;
  541. }
  542. .form-label {
  543. font-size: 11px;
  544. color: #666;
  545. font-weight: 500;
  546. margin-bottom: 4px;
  547. display: block;
  548. }
  549. .form-item .el-input {
  550. width: 100%;
  551. }
  552. .form-item .el-date-editor {
  553. width: 100%;
  554. }
  555. /* 区域标题 */
  556. .section-title {
  557. display: flex;
  558. align-items: center;
  559. justify-content: space-between;
  560. padding: 6px 8px;
  561. background: white;
  562. margin: 0 16px;
  563. margin-top: 4px;
  564. border-radius: 8px 8px 0 0;
  565. border-bottom: 2px solid #17B3A3;
  566. }
  567. .title-left {
  568. display: flex;
  569. align-items: center;
  570. }
  571. .title-left i {
  572. color: #17B3A3;
  573. font-size: 16px;
  574. margin-right: 8px;
  575. }
  576. .title-left span {
  577. color: #17B3A3;
  578. font-size: 14px;
  579. font-weight: 500;
  580. }
  581. /* 标签列表 */
  582. .label-list {
  583. background: white;
  584. margin: 0 16px 12px;
  585. border-radius: 0 0 8px 8px;
  586. overflow: hidden;
  587. }
  588. .label-list .el-form-item {
  589. margin-bottom: 1px;
  590. }
  591. .label-list .el-form-item__label {
  592. padding-bottom: 1px;
  593. margin-bottom: 0;
  594. line-height: 1.1;
  595. font-size: 11px;
  596. }
  597. .label-list .el-form-item__content {
  598. line-height: 1.2;
  599. font-size: 12px;
  600. }
  601. .bottom-line-row {
  602. border-bottom: 1px solid #f0f0f0;
  603. margin-bottom: 8px;
  604. padding-bottom: 8px;
  605. }
  606. .empty-labels {
  607. padding: 20px;
  608. text-align: center;
  609. color: #999;
  610. background: white;
  611. margin: 0 16px;
  612. border-radius: 8px;
  613. }
  614. /* 底部操作按钮 */
  615. .bottom-actions {
  616. display: flex;
  617. padding: 16px;
  618. gap: 20px;
  619. background: white;
  620. margin-top: auto;
  621. }
  622. .action-btn {
  623. flex: 1;
  624. padding: 12px;
  625. border: 1px solid #17B3A3;
  626. background: #17B3A3;
  627. color: white;
  628. border-radius: 20px;
  629. font-size: 14px;
  630. cursor: pointer;
  631. transition: all 0.2s ease;
  632. }
  633. .action-btn.secondary {
  634. background: white;
  635. color: #17B3A3;
  636. }
  637. .action-btn:hover {
  638. background: #0d8f7f;
  639. border-color: #0d8f7f;
  640. }
  641. .action-btn.secondary:hover {
  642. background: #17B3A3;
  643. color: white;
  644. }
  645. .action-btn:active {
  646. transform: scale(0.98);
  647. }
  648. .action-btn:disabled {
  649. background: #c0c4cc !important;
  650. border-color: #c0c4cc !important;
  651. color: white !important;
  652. cursor: not-allowed !important;
  653. transform: none !important;
  654. box-shadow: none !important;
  655. }
  656. .action-btn:disabled:hover {
  657. background: #c0c4cc !important;
  658. transform: none !important;
  659. box-shadow: none !important;
  660. }
  661. .action-btn.secondary:disabled {
  662. background: #f5f7fa !important;
  663. color: #c0c4cc !important;
  664. border-color: #e4e7ed !important;
  665. }
  666. /* 响应式设计 */
  667. @media (max-width: 360px) {
  668. .status-bar {
  669. padding: 8px 12px;
  670. }
  671. .search-container {
  672. padding: 8px 12px;
  673. }
  674. .material-info-card {
  675. margin: 4px 12px;
  676. padding: 6px 16px;
  677. }
  678. .item-list {
  679. margin: 0 12px 8px;
  680. }
  681. .form-row {
  682. gap: 8px;
  683. margin-bottom: 8px;
  684. }
  685. .form-label {
  686. font-size: 10px;
  687. margin-bottom: 2px;
  688. }
  689. }
  690. </style>