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.

554 lines
14 KiB

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