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.

581 lines
13 KiB

6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 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 clearable class="compact-input" v-model="scanCode" placeholder="请扫描材料纸质标签" prefix-icon="el-icon-search"
  16. @keyup.enter.native="handleScan" ref="scanInput" />
  17. <div class="mode-switch">
  18. <el-switch class="custom-switch" v-model="isRemoveMode" active-color="#ff4949" inactive-color="#13ce66">
  19. </el-switch>
  20. <span v-if="isRemoveMode" class="switch-text">{{ '移除' }}</span>
  21. <span v-else class="switch-text2">{{ '添加' }}</span>
  22. </div>
  23. </div>
  24. <!-- 工单物料信息 -->
  25. <div class="work-order-list" v-if="workOrderNo && componentPartNo">
  26. <div class="work-order-card">
  27. <div class="card-title">
  28. <span class="title-label">工单号{{ workOrderNo }}&nbsp;&nbsp;行号{{itemNo}}</span>
  29. </div>
  30. <!-- 物料描述单独一行 -->
  31. <div class="part-desc-row">
  32. <span class="desc-text">物料编码{{ componentPartNo }}</span>
  33. </div>
  34. <div class="part-desc-row">
  35. <span class="desc-text">物料名称{{ componentPartDesc }}</span>
  36. </div>
  37. <div class="card-details">
  38. <div class="detail-item">
  39. <div class="detail-label">需求数量</div>
  40. <div class="detail-value">{{ requiredQty }}</div>
  41. </div>
  42. <div class="detail-item">
  43. <div class="detail-label">已发数量</div>
  44. <div class="detail-value">{{ issuedQty }}</div>
  45. </div>
  46. <div class="detail-item">
  47. <div class="detail-label">本次</div>
  48. <div class="detail-value">{{ totalScannedQty }}</div>
  49. </div>
  50. </div>
  51. </div>
  52. </div>
  53. <div class="section-title">
  54. <div class="title-left">
  55. <i class="el-icon-circle-check"></i>
  56. <span>出库信息确认</span>
  57. </div>
  58. </div>
  59. <!-- 标签列表 -->
  60. <div class="label-list">
  61. <div class="list-header">
  62. <div class="col-no">NO.</div>
  63. <div class="col-label">标签条码</div>
  64. <div class="col-batch">仓库</div>
  65. <div class="col-batch">批次号</div>
  66. <div class="col-qty">数量</div>
  67. </div>
  68. <div v-for="(label, index) in scannedLabels" :key="label.id" class="list-item">
  69. <div class="col-no">{{ index+1 }}</div>
  70. <div class="col-label">{{ label.labelCode }}</div>
  71. <div class="col-batch">{{label.warehouseId}}</div>
  72. <div class="col-batch">{{ label.batchNo || '-' }}</div>
  73. <div class="col-qty">{{ label.quantity }}</div>
  74. </div>
  75. <div v-if="scannedLabels.length === 0" class="empty-labels">
  76. <p>暂无扫描标签</p>
  77. </div>
  78. </div>
  79. <!-- 底部操作按钮 -->
  80. <div class="bottom-actions">
  81. <button class="action-btn secondary" @click="confirmIssue" :disabled="scannedLabels.length === 0">
  82. 确定
  83. </button>
  84. <!-- <button class="action-btn secondary" style="margin-left: 10px;">
  85. 打印
  86. </button> -->
  87. <button class="action-btn secondary" style="margin-left: 10px;" @click="clearScannedLabels">
  88. 取消
  89. </button>
  90. </div>
  91. </div>
  92. </template>
  93. <script>
  94. import {
  95. scanMaterialLabel,
  96. confirmDirectIssue,
  97. } from '@/api/production/production-issue'
  98. import moment from 'moment'
  99. export default {
  100. data() {
  101. return {
  102. scanCode: '',
  103. isRemoveMode: false,
  104. scannedLabels: [],
  105. workOrderNo: '',
  106. componentPartNo: '',
  107. componentPartDesc: '',
  108. requiredQty: 0,
  109. issuedQty: 0,
  110. itemNo: '',
  111. }
  112. },
  113. computed: {
  114. totalScannedQty() {
  115. return this.scannedLabels.reduce((sum, l) => sum + (l.quantity || 0), 0)
  116. },
  117. },
  118. methods: {
  119. formatDate(date) {
  120. return date ? moment(date).format('YYYY-MM-DD') : ''
  121. },
  122. handleScan() {
  123. if (!this.scanCode.trim()) return
  124. if (this.isRemoveMode) {
  125. this.removeLabelByCode(this.scanCode.trim())
  126. } else {
  127. this.validateAndAddLabel(this.scanCode.trim())
  128. }
  129. this.scanCode = ''
  130. },
  131. validateAndAddLabel(labelCode) {
  132. const params = {
  133. scannedLabel: labelCode,
  134. workOrderNo: this.workOrderNo,
  135. componentPartNo: this.componentPartNo,
  136. site: this.$store.state.user.site,
  137. }
  138. scanMaterialLabel(params)
  139. .then(({ data }) => {
  140. if (data && data.code === 0) {
  141. const exists = this.scannedLabels.find(
  142. (item) => item.labelCode === labelCode
  143. )
  144. if (exists) {
  145. this.$message.warning('该标签已扫描,请勿重复扫描')
  146. return
  147. }
  148. /* if (data.labelInfo.materialCode !== this.materialCode) {
  149. this.$message.error('标签物料编码与选择的材料不匹配')
  150. return
  151. } */
  152. this.scannedLabels.push({
  153. id: Date.now(),
  154. labelCode,
  155. componentPartNo: data.labelInfo.materialCode,
  156. quantity: data.labelInfo.availableQty,
  157. batchNo: data.labelInfo.batchNo,
  158. warehouseId: data.labelInfo.warehouseId,
  159. locationId: data.labelInfo.locationId,
  160. })
  161. this.$message.success('扫描成功')
  162. } else {
  163. this.$message.error(data.msg || '标签验证失败')
  164. }
  165. })
  166. .catch(() => {
  167. this.$message.error('扫描失败')
  168. })
  169. },
  170. removeLabelByCode(labelCode) {
  171. const index = this.scannedLabels.findIndex(
  172. (item) => item.labelCode === labelCode
  173. )
  174. if (index !== -1) {
  175. this.scannedLabels.splice(index, 1)
  176. this.$message.success('移除成功')
  177. } else {
  178. this.$message.warning('未找到该标签')
  179. }
  180. },
  181. clearScannedLabels() {
  182. if (this.scannedLabels.length > 0) {
  183. this.$confirm('确定清空所有已扫描的标签吗?', '提示', {
  184. confirmButtonText: '确定',
  185. cancelButtonText: '取消',
  186. type: 'warning',
  187. })
  188. .then(() => {
  189. this.scannedLabels = []
  190. this.$router.back()
  191. this.$message.success('已清空')
  192. })
  193. .catch(() => {})
  194. } else {
  195. this.$router.back()
  196. }
  197. },
  198. confirmIssue() {
  199. if (this.scannedLabels.length === 0) {
  200. this.$message.warning('请先扫描材料标签')
  201. return
  202. }
  203. console.log('1231', localStorage.getItem('userName'))
  204. const params = {
  205. site: this.$store.state.user.site,
  206. workOrderNo: this.workOrderNo,
  207. componentPartNo: this.componentPartNo,
  208. operatorName: localStorage.getItem('userName'),
  209. itemNo: this.itemNo,
  210. selectedMaterials: this.scannedLabels.map((l, i) => ({
  211. labelCode: l.labelCode,
  212. issueQty: l.quantity,
  213. batchNo: l.batchNo,
  214. warehouseId: l.warehouseId,
  215. locationId: l.locationId,
  216. materialCode: l.materialCode,
  217. })),
  218. }
  219. confirmDirectIssue(params)
  220. .then(({ data }) => {
  221. if (data && data.code === 0) {
  222. this.$message.success('发料成功')
  223. this.$router.back()
  224. } else {
  225. this.$message.error(data.msg || '发料失败')
  226. }
  227. })
  228. .catch(() => {
  229. this.$message.error('发料失败')
  230. })
  231. },
  232. initFromRoute() {
  233. this.workOrderNo = this.$route.params.workOrderNo
  234. this.componentPartNo = this.$route.params.partNo
  235. this.componentPartDesc = this.$route.params.partDesc || ''
  236. this.requiredQty = Number(this.$route.params.requiredQty || 0)
  237. this.issuedQty = Number(this.$route.params.issuedQty || 0)
  238. this.itemNo = this.$route.params.itemNo || ''
  239. /* if (!this.workOrderNo || !this.materialCode) {
  240. this.$message.error('参数错误')
  241. this.$router.back()
  242. } */
  243. },
  244. },
  245. mounted() {
  246. this.initFromRoute()
  247. this.$nextTick(() => {
  248. if (this.$refs.scanInput) {
  249. this.$refs.scanInput.focus()
  250. }
  251. })
  252. },
  253. }
  254. </script>
  255. <style scoped>
  256. .pda-container {
  257. width: 100vw;
  258. height: 100vh;
  259. display: flex;
  260. flex-direction: column;
  261. background: #f5f5f5;
  262. }
  263. .header-bar {
  264. display: flex;
  265. justify-content: space-between;
  266. align-items: center;
  267. padding: 8px 16px;
  268. background: #17b3a3;
  269. color: white;
  270. height: 40px;
  271. min-height: 40px;
  272. }
  273. .header-left {
  274. display: flex;
  275. align-items: center;
  276. cursor: pointer;
  277. font-size: 16px;
  278. font-weight: 500;
  279. }
  280. .header-left i {
  281. margin-right: 8px;
  282. font-size: 18px;
  283. }
  284. .header-right {
  285. cursor: pointer;
  286. font-size: 16px;
  287. font-weight: 500;
  288. }
  289. .search-container {
  290. padding: 12px 16px;
  291. background: white;
  292. display: flex;
  293. align-items: center;
  294. gap: 12px;
  295. }
  296. .search-container .el-input {
  297. width: 240px;
  298. margin-right: 12px;
  299. }
  300. .compact-input ::v-deep .el-input__inner {
  301. height: 36px;
  302. padding: 0 12px 0 35px;
  303. font-size: 14px;
  304. }
  305. .compact-input ::v-deep .el-input__prefix {
  306. left: 10px;
  307. }
  308. .compact-input ::v-deep .el-input__suffix {
  309. right: 30px;
  310. }
  311. .mode-switch {
  312. position: relative;
  313. display: inline-block;
  314. }
  315. .custom-switch {
  316. transform: scale(1.3);
  317. }
  318. .switch-text {
  319. position: absolute;
  320. left: 25%;
  321. transform: translateX(-50%);
  322. top: 50%;
  323. transform: translateY(-50%) translateX(-50%);
  324. font-size: 12px;
  325. font-weight: 500;
  326. color: #606266;
  327. white-space: nowrap;
  328. pointer-events: none;
  329. z-index: 1;
  330. top: 53%;
  331. transform: translate(-50%, -50%);
  332. font-size: 12px;
  333. font-weight: bold;
  334. color: white;
  335. pointer-events: none;
  336. z-index: 2;
  337. }
  338. .switch-text2 {
  339. position: absolute;
  340. left: 75%;
  341. transform: translateX(-50%);
  342. top: 50%;
  343. transform: translateY(-50%) translateX(-50%);
  344. font-size: 12px;
  345. font-weight: 500;
  346. color: #606266;
  347. white-space: nowrap;
  348. pointer-events: none;
  349. z-index: 1;
  350. top: 53%;
  351. transform: translate(-50%, -50%);
  352. font-size: 12px;
  353. font-weight: bold;
  354. color: white;
  355. pointer-events: none;
  356. z-index: 2;
  357. }
  358. /* 调整 switch 尺寸以便容纳文字 */
  359. .custom-switch ::v-deep .el-switch__core {
  360. width: 60px;
  361. height: 28px;
  362. }
  363. /* 工单列表 */
  364. .work-order-list {
  365. overflow-y: auto;
  366. padding: 12px 16px;
  367. }
  368. /* 工单卡片 */
  369. .work-order-card {
  370. background: white;
  371. border-radius: 8px;
  372. margin-bottom: 12px;
  373. padding: 16px;
  374. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  375. cursor: pointer;
  376. transition: all 0.2s ease;
  377. border: 2px solid transparent;
  378. }
  379. .work-order-card:hover {
  380. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
  381. transform: translateY(-1px);
  382. }
  383. .work-order-card.selected {
  384. border-color: #17b3a3;
  385. background: #f0fffe;
  386. }
  387. .work-order-card:active {
  388. transform: translateY(0);
  389. }
  390. /* 卡片标题 */
  391. .card-title {
  392. margin-bottom: 12px;
  393. }
  394. .title-label {
  395. font-size: 12px;
  396. color: #666;
  397. display: block;
  398. margin-bottom: 4px;
  399. }
  400. .title-value {
  401. font-size: 16px;
  402. font-weight: bold;
  403. color: #333;
  404. margin-left: 20px;
  405. }
  406. .section-title {
  407. display: flex;
  408. align-items: center;
  409. justify-content: space-between;
  410. padding: 6px 8px;
  411. background: white;
  412. margin: 0 16px;
  413. margin-top: 4px;
  414. border-radius: 8px 8px 0 0;
  415. border-bottom: 2px solid #17b3a3;
  416. }
  417. /* 物料描述行 */
  418. .part-desc-row {
  419. margin-bottom: 12px;
  420. padding: 0 4px;
  421. }
  422. .desc-text {
  423. font-size: 12px;
  424. color: #666;
  425. line-height: 1.3;
  426. word-break: break-all;
  427. }
  428. /* 卡片详情 */
  429. .card-details {
  430. display: flex;
  431. justify-content: space-between;
  432. align-items: flex-start;
  433. gap: 4px;
  434. }
  435. .detail-item {
  436. flex: 1;
  437. text-align: center;
  438. min-width: 50px;
  439. max-width: 70px;
  440. }
  441. .detail-label {
  442. font-size: 11px;
  443. color: #666;
  444. margin-bottom: 4px;
  445. line-height: 1.2;
  446. margin-left: -12px;
  447. }
  448. .detail-value {
  449. font-size: 13px;
  450. color: #333;
  451. line-height: 1.2;
  452. margin-left: -12px;
  453. }
  454. .label-list {
  455. background: white;
  456. margin: 0 16px 12px;
  457. border-radius: 8px;
  458. overflow: hidden;
  459. }
  460. .list-header {
  461. display: flex;
  462. background: #f8f9fa;
  463. padding: 12px 8px;
  464. border-bottom: 1px solid #e0e0e0;
  465. font-size: 12px;
  466. color: #666;
  467. font-weight: 500;
  468. }
  469. .list-item {
  470. display: flex;
  471. padding: 12px 8px;
  472. border-bottom: 1px solid #f0f0f0;
  473. font-size: 12px;
  474. color: #333;
  475. }
  476. .list-item:last-child {
  477. border-bottom: none;
  478. }
  479. .col-no {
  480. width: 20px;
  481. text-align: center;
  482. }
  483. .col-label {
  484. flex: 2;
  485. text-align: center;
  486. }
  487. .col-batch {
  488. flex: 1;
  489. text-align: center;
  490. }
  491. .col-qty {
  492. width: 60px;
  493. text-align: center;
  494. }
  495. .empty-labels {
  496. padding: 40px 20px;
  497. text-align: center;
  498. color: #999;
  499. }
  500. .bottom-actions {
  501. display: flex;
  502. padding: 16px;
  503. gap: 20px;
  504. background: white;
  505. margin-top: auto;
  506. }
  507. .action-btn {
  508. flex: 1;
  509. padding: 12px;
  510. border: 1px solid #17b3a3;
  511. background: white;
  512. color: #17b3a3;
  513. border-radius: 20px;
  514. font-size: 14px;
  515. cursor: pointer;
  516. transition: all 0.2s ease;
  517. }
  518. .action-btn:hover {
  519. background: #17b3a3;
  520. color: white;
  521. }
  522. .action-btn:active {
  523. transform: scale(0.98);
  524. }
  525. @media (max-width: 360px) {
  526. .header-bar {
  527. padding: 8px 12px;
  528. }
  529. .search-container {
  530. padding: 8px 12px;
  531. }
  532. .work-order-list {
  533. padding: 8px 12px;
  534. }
  535. .work-order-card {
  536. padding: 12px;
  537. }
  538. .card-details {
  539. flex-wrap: wrap;
  540. gap: 6px;
  541. }
  542. .detail-item {
  543. flex: 0 0 48%;
  544. margin-bottom: 6px;
  545. min-width: 50px;
  546. }
  547. .label-list {
  548. margin: 0 12px 8px;
  549. }
  550. }
  551. </style>