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.

1276 lines
42 KiB

2 hours ago
  1. <template>
  2. <div class="pda-container">
  3. <div class="header-bar">
  4. <div class="header-left" :class="{ 'header-action-disabled': interfaceBusy }" @click="handleBack">
  5. <i class="el-icon-arrow-left"></i>
  6. <span>直接组盘</span>
  7. </div>
  8. <div class="header-right" :class="{ 'header-action-disabled': interfaceBusy }" @click="goHome">首页</div>
  9. </div>
  10. <div class="table-body" style="max-height: 500px; overflow-y: auto;">
  11. <div class="main-content form-section">
  12. <div class="input-group">
  13. <label class="input-label">栈板编码</label>
  14. <div style="display: flex; gap: 8px;">
  15. <el-input
  16. v-model="palletCode"
  17. placeholder="请扫描栈板编码"
  18. class="form-input"
  19. style="flex: 0.75;"
  20. clearable
  21. inputmode="none"
  22. autocomplete="off"
  23. autocorrect="off"
  24. spellcheck="false"
  25. @keyup.enter.native="handlePalletScan"
  26. ref="palletInput"
  27. />
  28. <button class="action-btn secondary" style="flex: 0.25; margin: 0;" @click="handleCallPallet" :disabled="interfaceBusy">
  29. Call栈板
  30. </button>
  31. </div>
  32. </div>
  33. <div v-if="palletScanned" class="input-group">
  34. <div style="display: flex; gap: 8px; align-items: end;">
  35. <div style="flex: 0.65;">
  36. <label class="input-label">栈板类型</label>
  37. <el-select
  38. v-model="currentPalletType"
  39. placeholder="请选择栈板类型"
  40. style="width: 100%;"
  41. :disabled="palletTypeDisabled || interfaceBusy"
  42. @change="handlePalletTypeChange"
  43. >
  44. <el-option
  45. v-for="type in palletTypeOptions"
  46. :key="type.palletType"
  47. :label="`${type.palletType} - ${type.typeDesc}`"
  48. :value="type.palletType"
  49. />
  50. </el-select>
  51. </div>
  52. <div style="flex: 0.35;">
  53. <label class="input-label">自动分拣</label>
  54. <el-select
  55. v-model="currentAutoSort"
  56. placeholder="请选择"
  57. style="width: 100%;"
  58. :disabled="autoSortDisabled || interfaceBusy"
  59. @change="handleAutoSortChange"
  60. >
  61. <el-option label="是" value="Y"></el-option>
  62. <el-option label="否" value="N"></el-option>
  63. </el-select>
  64. </div>
  65. </div>
  66. </div>
  67. <div v-if="palletScanned" class="input-group">
  68. <el-checkbox
  69. v-model="forceFullPalletOut"
  70. :disabled="palletTypeDisabled || interfaceBusy"
  71. @change="handleForceFullPalletOutChange"
  72. style="width: 100%;"
  73. >
  74. 是否强制整托出库
  75. </el-checkbox>
  76. </div>
  77. <div v-if="palletScanned" class="input-group">
  78. <label class="input-label">当前站点</label>
  79. <div style="display: flex; gap: 8px;">
  80. <el-input v-model="currentPalletStation" placeholder="栈板当前站点" class="form-input" readonly style="flex: 0.75;" />
  81. <button class="action-btn secondary" style="flex: 0.25; margin: 0; white-space: nowrap;" @click="refreshTable" :disabled="interfaceBusy">
  82. 刷新
  83. </button>
  84. </div>
  85. </div>
  86. <div v-if="palletScanned" class="input-group">
  87. <div style="display: flex; gap: 8px; align-items: center;">
  88. <div style="flex: 0.75;">
  89. <el-radio-group v-model="operationType" style="display: flex;" :disabled="interfaceBusy">
  90. <el-radio label="in" style="margin-right: 0px; white-space: nowrap;">扫进</el-radio>
  91. <el-radio label="out" style="white-space: nowrap;">扫出</el-radio>
  92. </el-radio-group>
  93. </div>
  94. <button class="action-btn secondary" style="flex: 0.25; margin: 0; white-space: nowrap;" :disabled="interfaceBusy || currentPalletType === ''" @click="showScanModal">
  95. 扫描条码
  96. </button>
  97. </div>
  98. </div>
  99. </div>
  100. <div v-if="palletScanned" class="rma-list">
  101. <div class="list-title-row" style="display: flex; gap: 8px; align-items: center; padding: 0;">
  102. <div class="list-title" style="flex: 0.4; margin: 0;">
  103. <button class="action-btn primary" style="margin: 0;" @click="showDetailModal" :disabled="interfaceBusy">
  104. 明细
  105. </button>
  106. </div>
  107. <button class="action-btn warning" style="flex: 0.3; margin: 0;" @click="showScanOutModal" :disabled="interfaceBusy || scanOutList.length === 0">
  108. 扫出({{ scanOutList.length }})
  109. </button>
  110. <!-- 与海安组盘入库唯一差异此处改为推送组盘并且只允许area/location均为空的托盘触发 - rqrq -->
  111. <button class="action-btn secondary" style="flex: 0.3; margin: 0;" @click="openPushAssemblyDialog" :disabled="interfaceBusy || !canPushAssembly">
  112. {{ pushAssemblyLoading ? '处理中...' : '推送组盘' }}
  113. </button>
  114. </div>
  115. <div class="list-title" style="flex: 0.5; margin: 0;">
  116. <label style="margin-left: 5px;">条码数:{{ detailList.length }}</label>
  117. </div>
  118. <div class="detail-table">
  119. <div class="table-header">
  120. <div class="col-position">位置</div>
  121. <div class="col-layer">层数</div>
  122. <div class="col-serial">标签号</div>
  123. </div>
  124. <div v-for="(detail, index) in detailList" :key="index" class="table-row">
  125. <div class="col-position">{{ detail.position }}</div>
  126. <div class="col-layer">{{ detail.layer }}</div>
  127. <div class="col-serial">{{ detail.serialNo }}</div>
  128. </div>
  129. <div v-if="detailList.length === 0" class="table-row empty-row">
  130. <div class="empty-hint">暂无栈板明细数据</div>
  131. </div>
  132. </div>
  133. </div>
  134. </div>
  135. <el-dialog
  136. :title="'已扫描数量:' + detailList.length"
  137. :visible.sync="detailModalVisible"
  138. width="90%"
  139. :close-on-click-modal="false"
  140. :show-close="false"
  141. :append-to-body="true"
  142. >
  143. <div class="table-body" style="max-height: 240px; overflow-y: auto;">
  144. <div class="detail-table">
  145. <div class="table-header">
  146. <div class="col-position">位置</div>
  147. <div class="col-layer">层数</div>
  148. <div class="col-serial">标签号</div>
  149. </div>
  150. <div v-for="(detail, index) in detailList" :key="index" class="table-row">
  151. <div class="col-position">{{ detail.position }}</div>
  152. <div class="col-layer">{{ detail.layer }}</div>
  153. <div class="col-serial">{{ detail.serialNo }}</div>
  154. </div>
  155. <div v-if="detailList.length === 0" class="table-row empty-row">
  156. <div class="empty-hint">暂无栈板明细数据</div>
  157. </div>
  158. </div>
  159. </div>
  160. <div slot="footer" class="dialog-footer">
  161. <button class="action-btn secondary" style="margin-left: 10px;" @click="detailModalVisible = false" :disabled="interfaceBusy">取消</button>
  162. </div>
  163. </el-dialog>
  164. <el-dialog
  165. :title="'扫出记录 (共' + scanOutList.length + '条)'"
  166. :visible.sync="scanOutModalVisible"
  167. width="90%"
  168. :close-on-click-modal="false"
  169. :show-close="false"
  170. :append-to-body="true"
  171. >
  172. <div class="table-body" style="max-height: 300px; overflow-y: auto;">
  173. <div class="detail-table">
  174. <div class="table-header">
  175. <div class="col-position">位置</div>
  176. <div class="col-layer">层数</div>
  177. <div class="col-serial">标签号</div>
  178. </div>
  179. <div v-for="(detail, index) in scanOutList" :key="index" class="table-row">
  180. <div class="col-position">{{ detail.position }}</div>
  181. <div class="col-layer">{{ detail.layer }}</div>
  182. <div class="col-serial">{{ detail.serialNo }}</div>
  183. </div>
  184. <div v-if="scanOutList.length === 0" class="table-row empty-row">
  185. <div class="empty-hint">暂无扫出记录</div>
  186. </div>
  187. </div>
  188. </div>
  189. <div slot="footer" class="dialog-footer">
  190. <button class="action-btn secondary" @click="scanOutModalVisible = false" :disabled="interfaceBusy">关闭</button>
  191. </div>
  192. </el-dialog>
  193. <!-- 扫码模态框与海安组盘入库保持一致 - rqrq -->
  194. <el-dialog
  195. title="扫描标签"
  196. :visible.sync="scanModalVisible"
  197. width="90%"
  198. :close-on-click-modal="false"
  199. :show-close="false"
  200. :modal="true"
  201. :modal-append-to-body="true"
  202. :append-to-body="true"
  203. >
  204. <div class="scan-modal-content">
  205. <div v-if="operationType === 'in'" class="modal-form">
  206. <div v-if="!currentMixedMode && scanLayerOptions.length > 0" class="input-group">
  207. <label class="input-label">层数当前选择{{ scanLayer }}</label>
  208. <div class="layer-grid">
  209. <div
  210. v-for="layer in scanLayerOptions"
  211. :key="layer"
  212. class="layer-item"
  213. :class="{ 'layer-selected': scanLayer === layer }"
  214. @click="handleLayerClick(layer)"
  215. >
  216. {{ layer }}
  217. </div>
  218. </div>
  219. </div>
  220. <div class="input-group">
  221. <label class="input-label">
  222. 位置{{ currentSelectedPosition ? `(当前选择:${currentSelectedPosition}` : '' }}
  223. <span v-if="positionGridLoading" style="color: #909399; font-size: 12px;">加载中...</span>
  224. </label>
  225. <div class="position-grid" :class="[{ 'position-grid-loading': positionGridLoading }, getPositionGridClass]">
  226. <div
  227. v-for="(item, index) in displayPositionGrid"
  228. :key="index"
  229. class="position-item"
  230. :class="{
  231. 'position-disabled': positionGridLoading || !availablePositions.includes(item.position),
  232. 'position-selected': currentSelectedPosition === item.position,
  233. 'position-loading': positionGridLoading,
  234. 'position-placeholder': item.isPlaceholder
  235. }"
  236. @click="handlePositionClick(item.position)"
  237. >
  238. <template v-if="!item.isPlaceholder">{{ item.position }}({{ item.count }})</template>
  239. </div>
  240. </div>
  241. </div>
  242. </div>
  243. <div class="input-group">
  244. <label class="input-label">标签二维码</label>
  245. <el-input
  246. ref="scanInput"
  247. v-model="scanCode"
  248. placeholder="请扫描标签二维码"
  249. class="form-input"
  250. clearable
  251. :disabled="operationType === 'in' && !currentSelectedPosition"
  252. @keyup.enter.native="handleLabelScan"
  253. inputmode="none"
  254. autocomplete="off"
  255. autocorrect="off"
  256. spellcheck="false"
  257. />
  258. </div>
  259. </div>
  260. <div slot="footer" class="dialog-footer">
  261. <button class="action-btn secondary" style="margin-left: 10px;" @click="closeScanModal" :disabled="interfaceBusy">取消</button>
  262. </div>
  263. </el-dialog>
  264. <!-- 调用空托盘与海安组盘入库保持一致 - rqrq -->
  265. <el-dialog
  266. title="调用空托盘"
  267. :visible.sync="callEmptyModalVisible"
  268. width="90%"
  269. :close-on-click-modal="false"
  270. :show-close="false"
  271. :append-to-body="true"
  272. >
  273. <div class="input-group">
  274. <label class="input-label">区域</label>
  275. <el-select v-model="callEmptyForm.areaId" placeholder="请选择区域" style="width: 100%;" @change="handleAreaChangeForCall">
  276. <el-option v-for="a in areaOptions" :key="a.areaId" :label="a.areaDesc" :value="a.areaId" />
  277. </el-select>
  278. </div>
  279. <div class="input-group">
  280. <label class="input-label">站点</label>
  281. <el-select v-model="callEmptyForm.stationCode" placeholder="请选择站点" style="width: 100%;" :disabled="!callEmptyForm.areaId">
  282. <el-option v-for="s in stationOptions" :key="s.stationCode" :label="`${s.stationCode} (${s.status || '-'})`" :value="s.stationCode" />
  283. </el-select>
  284. </div>
  285. <div class="input-group">
  286. <label class="input-label">栈板类型</label>
  287. <el-select v-model="callEmptyForm.palletType" placeholder="请选择栈板类型" style="width: 100%;">
  288. <el-option v-for="t in callEmptyPalletTypeOptions" :key="t.palletType" :label="`${t.palletType} - ${t.typeDesc}`" :value="t.palletType" />
  289. </el-select>
  290. </div>
  291. <div slot="footer" class="dialog-footer">
  292. <button class="action-btn primary" @click="submitCallEmpty" :disabled="interfaceBusy || callEmptyLoading">{{ callEmptyLoading ? '处理中...' : '确认' }}</button>
  293. <button class="action-btn secondary" style="margin-left: 8px;" @click="callEmptyModalVisible = false" :disabled="interfaceBusy || callEmptyLoading">取消</button>
  294. </div>
  295. </el-dialog>
  296. <!-- 直接组盘推送弹窗必须选择入库口1/入库口2 - rqrq -->
  297. <el-dialog
  298. title="请选择入库口"
  299. :visible.sync="pushAssemblyDialogVisible"
  300. width="90%"
  301. :close-on-click-modal="false"
  302. :show-close="false"
  303. :append-to-body="true"
  304. >
  305. <div class="input-group">
  306. <label class="input-label">入库口</label>
  307. <el-select v-model="selectedInboundPort" placeholder="请选择入库口" style="width: 100%;">
  308. <el-option label="入库口1" value="INBOUND_PORT_1"></el-option>
  309. <el-option label="入库口2" value="INBOUND_PORT_2"></el-option>
  310. </el-select>
  311. </div>
  312. <div slot="footer" class="dialog-footer">
  313. <button class="action-btn primary" @click="submitPushAssembly" :disabled="interfaceBusy || !selectedInboundPort">
  314. {{ pushAssemblyLoading ? '处理中...' : '确认推送' }}
  315. </button>
  316. <button class="action-btn secondary" style="margin-left: 8px;" @click="closePushAssemblyDialog" :disabled="interfaceBusy">取消</button>
  317. </div>
  318. </el-dialog>
  319. </div>
  320. </template>
  321. <script>
  322. import {
  323. checkPalletExists,
  324. getPalletDetails,
  325. savePalletDetail,
  326. deletePalletDetail,
  327. getPalletInfo,
  328. getPalletTypeList,
  329. updatePalletTypeAndAutoSort,
  330. getAvailablePositionsForLayer,
  331. callEmptyPalletToStation,
  332. getAreaOptionsForChange,
  333. getStationsByArea,
  334. pushPalletAssemblyToInboundPort
  335. } from '../../../api/haianAutoWarehouse/palletAssembly'
  336. export default {
  337. name: 'haianPalletAssembly',
  338. data() {
  339. return {
  340. site: this.$store.state.user.site,
  341. palletCode: '',
  342. palletScanned: false,
  343. operationType: 'in',
  344. currentPalletFamily: '',
  345. currentPalletType: '',
  346. currentAutoSort: 'N',
  347. currentPalletStation: '',
  348. forceFullPalletOut: false,
  349. palletTypeDisabled: false,
  350. autoSortDisabled: false,
  351. currentWcsAutoSort: '',
  352. currentMaxLayer: 0,
  353. positionOptions: [],
  354. palletTypeOptions: [],
  355. callEmptyPalletTypeOptions: [],
  356. detailList: [],
  357. scanOutList: [],
  358. detailModalVisible: false,
  359. scanOutModalVisible: false,
  360. palletInfo: {
  361. palletType: '',
  362. autoSort: 'N',
  363. locationCode: '',
  364. area: ''
  365. },
  366. scanModalVisible: false,
  367. scanCode: '',
  368. scanPosition: '',
  369. scanLayer: 1,
  370. scanLayerOptions: [],
  371. needRefreshOnClose: false,
  372. positionGrid: [],
  373. availablePositions: [],
  374. currentSelectedPosition: '',
  375. currentMixedMode: false,
  376. positionGridLoading: false,
  377. areaOptions: [],
  378. stationOptions: [],
  379. callEmptyModalVisible: false,
  380. callEmptyLoading: false,
  381. callEmptyForm: {
  382. areaId: '',
  383. stationCode: '',
  384. palletType: ''
  385. },
  386. // 推送组盘弹窗状态 - rqrq
  387. pushAssemblyDialogVisible: false,
  388. selectedInboundPort: '',
  389. // 接口锁状态:推送组盘时要禁用页面所有按钮 - rqrq
  390. palletScanLoading: false,
  391. palletConfigLoading: false,
  392. scanRequestLoading: false,
  393. pushAssemblyLoading: false
  394. }
  395. },
  396. computed: {
  397. getPositionGridClass() {
  398. const len = this.positionGrid.length
  399. if (len === 1) return 'position-grid-1'
  400. if (len === 2) return 'position-grid-4'
  401. if (len === 4) return 'position-grid-4'
  402. return 'position-grid-9'
  403. },
  404. displayPositionGrid() {
  405. const len = this.positionGrid.length
  406. if (len === 2) {
  407. const pos1 = this.positionGrid.find((p) => p.position === '1') || { position: '1', count: 0 }
  408. const pos2 = this.positionGrid.find((p) => p.position === '2') || { position: '2', count: 0 }
  409. return [
  410. { ...pos1, isPlaceholder: false },
  411. { position: '', count: 0, isPlaceholder: true },
  412. { position: '', count: 0, isPlaceholder: true },
  413. { ...pos2, isPlaceholder: false }
  414. ]
  415. }
  416. return this.positionGrid.map((p) => ({ ...p, isPlaceholder: false }))
  417. },
  418. // 直接组盘规则:area与location_code同时为空时才允许推送 - rqrq
  419. canPushAssembly() {
  420. if (!this.palletScanned) {
  421. return false
  422. }
  423. if (!this.detailList || this.detailList.length === 0) {
  424. return false
  425. }
  426. const area = this.palletInfo && this.palletInfo.area ? String(this.palletInfo.area).trim() : ''
  427. const locationCode = this.palletInfo && this.palletInfo.locationCode ? String(this.palletInfo.locationCode).trim() : ''
  428. return area === '' && locationCode === ''
  429. },
  430. interfaceBusy() {
  431. return this.palletScanLoading || this.palletConfigLoading || this.scanRequestLoading || this.callEmptyLoading || this.pushAssemblyLoading
  432. }
  433. },
  434. methods: {
  435. ensureSite() {
  436. if (!this.site) {
  437. this.$alert('当前用户缺少site,无法执行海安组盘业务', '提示', { confirmButtonText: '确定' })
  438. return false
  439. }
  440. return true
  441. },
  442. handleBack() {
  443. if (this.interfaceBusy) {
  444. return
  445. }
  446. this.resetPage(false)
  447. this.$router.back()
  448. },
  449. goHome() {
  450. if (this.interfaceBusy) {
  451. return
  452. }
  453. this.resetPage(false)
  454. this.$router.push({ path: '/' })
  455. },
  456. resetPage(needFocus = true) {
  457. this.site = this.$store.state.user.site
  458. this.palletCode = ''
  459. this.palletScanned = false
  460. this.operationType = 'in'
  461. this.currentPalletFamily = ''
  462. this.currentPalletType = ''
  463. this.currentAutoSort = 'N'
  464. this.currentPalletStation = ''
  465. this.forceFullPalletOut = false
  466. this.palletTypeDisabled = false
  467. this.autoSortDisabled = false
  468. this.currentWcsAutoSort = ''
  469. this.currentMaxLayer = 0
  470. this.positionOptions = []
  471. this.palletTypeOptions = []
  472. this.callEmptyPalletTypeOptions = []
  473. this.detailList = []
  474. this.scanOutList = []
  475. this.detailModalVisible = false
  476. this.scanOutModalVisible = false
  477. this.palletInfo = {
  478. palletType: '',
  479. autoSort: 'N',
  480. locationCode: '',
  481. area: ''
  482. }
  483. this.scanModalVisible = false
  484. this.scanCode = ''
  485. this.scanPosition = ''
  486. this.scanLayer = 1
  487. this.scanLayerOptions = []
  488. this.needRefreshOnClose = false
  489. this.positionGrid = []
  490. this.availablePositions = []
  491. this.currentSelectedPosition = ''
  492. this.currentMixedMode = false
  493. this.positionGridLoading = false
  494. this.areaOptions = []
  495. this.stationOptions = []
  496. this.callEmptyModalVisible = false
  497. this.callEmptyLoading = false
  498. this.callEmptyForm = {
  499. areaId: '',
  500. stationCode: '',
  501. palletType: ''
  502. }
  503. this.pushAssemblyDialogVisible = false
  504. this.selectedInboundPort = ''
  505. this.palletScanLoading = false
  506. this.palletConfigLoading = false
  507. this.scanRequestLoading = false
  508. this.pushAssemblyLoading = false
  509. if (needFocus) {
  510. this.$nextTick(() => {
  511. if (this.$refs.palletInput) {
  512. this.$refs.palletInput.focus()
  513. }
  514. })
  515. }
  516. },
  517. handlePalletScan() {
  518. if (!this.ensureSite() || this.interfaceBusy) {
  519. return
  520. }
  521. if (!this.palletCode || !this.palletCode.trim()) {
  522. this.$alert('请输入栈板编码', '提示', { confirmButtonText: '确定' })
  523. return
  524. }
  525. this.palletScanLoading = true
  526. checkPalletExists({
  527. site: this.site,
  528. palletId: this.palletCode.trim()
  529. }).then(({ data }) => {
  530. if (data && data.code === 0) {
  531. this.palletCode = data.palletId
  532. this.positionOptions = data.positions || []
  533. this.palletScanned = true
  534. this.scanOutList = []
  535. this.scanCode = ''
  536. this.scanPosition = ''
  537. this.scanLayer = 1
  538. this.currentSelectedPosition = ''
  539. this.loadPalletInfo()
  540. this.refreshTable()
  541. } else {
  542. this.$alert((data && data.msg) || '栈板校验失败', '错误', { confirmButtonText: '确定' })
  543. }
  544. }).catch((error) => {
  545. this.$alert(error.message || '栈板校验失败', '错误', { confirmButtonText: '确定' })
  546. }).finally(() => {
  547. this.palletScanLoading = false
  548. })
  549. },
  550. loadPalletInfo() {
  551. getPalletInfo({
  552. site: this.site,
  553. palletId: this.palletCode
  554. }).then(({ data }) => {
  555. if (data && data.code === 0) {
  556. const row = data.row || {}
  557. this.currentPalletFamily = row.palletFamily || ''
  558. this.currentPalletType = row.palletType || ''
  559. this.currentAutoSort = row.autoSort || 'N'
  560. this.currentPalletStation = row.locationCode || ''
  561. this.forceFullPalletOut = Number(row.soreType) === 3
  562. this.palletInfo.palletType = this.currentPalletType
  563. this.palletInfo.autoSort = this.currentAutoSort
  564. this.palletInfo.locationCode = row.locationCode || ''
  565. this.palletInfo.area = row.area || ''
  566. this.loadPalletTypeList()
  567. }
  568. })
  569. },
  570. loadPalletTypeList() {
  571. getPalletTypeList({
  572. site: this.site,
  573. palletFamily: this.currentPalletFamily || '',
  574. active: 'Y'
  575. }).then(({ data }) => {
  576. if (data && data.code === 0) {
  577. this.palletTypeOptions = data.rows || []
  578. const selectedType = this.palletTypeOptions.find((item) => item.palletType === this.currentPalletType)
  579. this.currentWcsAutoSort = selectedType ? (selectedType.wcsAutoSort || 'N') : ''
  580. this.currentMaxLayer = selectedType ? Number(selectedType.maxLayer || 0) : 0
  581. this.updateAutoSortControl()
  582. }
  583. })
  584. },
  585. loadAllPalletTypeListForCallEmpty() {
  586. getPalletTypeList({
  587. site: this.site,
  588. palletFamily: '',
  589. active: 'Y'
  590. }).then(({ data }) => {
  591. if (data && data.code === 0) {
  592. this.callEmptyPalletTypeOptions = data.rows || []
  593. const hasSelectedType = this.callEmptyPalletTypeOptions.some((item) => item.palletType === this.callEmptyForm.palletType)
  594. if (!hasSelectedType) {
  595. this.callEmptyForm.palletType = ''
  596. }
  597. } else {
  598. this.callEmptyPalletTypeOptions = []
  599. this.callEmptyForm.palletType = ''
  600. }
  601. }).catch(() => {
  602. this.callEmptyPalletTypeOptions = []
  603. this.callEmptyForm.palletType = ''
  604. })
  605. },
  606. refreshTable() {
  607. getPalletDetails({
  608. site: this.site,
  609. palletId: this.palletCode,
  610. position: '',
  611. layer: null
  612. }).then(({ data }) => {
  613. if (data && data.code === 0) {
  614. this.detailList = data.details || []
  615. } else {
  616. this.detailList = []
  617. }
  618. this.palletTypeDisabled = this.detailList.length > 0
  619. this.updateAutoSortControl()
  620. }).catch(() => {
  621. this.detailList = []
  622. this.palletTypeDisabled = false
  623. this.updateAutoSortControl()
  624. })
  625. },
  626. handlePalletTypeChange() {
  627. const selectedType = this.palletTypeOptions.find((item) => item.palletType === this.currentPalletType)
  628. this.currentWcsAutoSort = selectedType ? (selectedType.wcsAutoSort || 'N') : ''
  629. this.currentMaxLayer = selectedType ? Number(selectedType.maxLayer || 0) : 0
  630. if (this.currentPalletType === 'A0103' || this.currentPalletType === 'A0206') {
  631. this.forceFullPalletOut = true
  632. }
  633. this.updateAutoSortControl()
  634. this.savePalletTypeConfig()
  635. },
  636. handleAutoSortChange() {
  637. this.savePalletTypeConfig()
  638. },
  639. handleForceFullPalletOutChange() {
  640. this.savePalletTypeConfig()
  641. },
  642. updateAutoSortControl() {
  643. if (this.forceFullPalletOut || this.currentPalletType === 'A0103') {
  644. this.currentAutoSort = 'N'
  645. this.autoSortDisabled = true
  646. return
  647. }
  648. if (this.currentWcsAutoSort === 'N') {
  649. this.currentAutoSort = 'N'
  650. this.autoSortDisabled = true
  651. return
  652. }
  653. this.autoSortDisabled = this.detailList.length > 0 || this.interfaceBusy
  654. },
  655. savePalletTypeConfig() {
  656. if (this.interfaceBusy) {
  657. return
  658. }
  659. this.palletConfigLoading = true
  660. updatePalletTypeAndAutoSort({
  661. site: this.site,
  662. palletId: this.palletCode,
  663. palletType: this.currentPalletType,
  664. autoSort: this.currentAutoSort,
  665. forceFullPalletOut: this.forceFullPalletOut
  666. }).then(({ data }) => {
  667. if (data && data.code === 0) {
  668. this.palletInfo.palletType = this.currentPalletType
  669. this.palletInfo.autoSort = this.currentAutoSort
  670. this.$message.success('托盘配置已更新')
  671. } else {
  672. this.$alert((data && data.msg) || '托盘配置更新失败', '错误', { confirmButtonText: '确定' })
  673. }
  674. }).catch((error) => {
  675. this.$alert(error.message || '托盘配置更新失败', '错误', { confirmButtonText: '确定' })
  676. }).finally(() => {
  677. this.palletConfigLoading = false
  678. })
  679. },
  680. handleCallPallet() {
  681. if (this.interfaceBusy) {
  682. return
  683. }
  684. this.openCallEmptyModal()
  685. },
  686. showScanModal() {
  687. this.scanModalVisible = true
  688. this.scanCode = ''
  689. this.currentSelectedPosition = ''
  690. this.needRefreshOnClose = false
  691. if (this.operationType === 'in') {
  692. this.scanLayer = 1
  693. this.loadLayerOptions()
  694. } else {
  695. this.$nextTick(() => {
  696. if (this.$refs.scanInput) {
  697. this.$refs.scanInput.focus()
  698. }
  699. })
  700. }
  701. },
  702. showDetailModal() {
  703. this.detailModalVisible = true
  704. },
  705. showScanOutModal() {
  706. this.scanOutModalVisible = true
  707. },
  708. loadLayerOptions() {
  709. getPalletDetails({
  710. site: this.site,
  711. palletId: this.palletCode,
  712. position: '',
  713. layer: null
  714. }).then(({ data }) => {
  715. if (data && data.code === 0) {
  716. const details = data.details || []
  717. let maxExistingLayer = 0
  718. if (details.length > 0) {
  719. maxExistingLayer = Math.max(...details.map((d) => d.layer || 0))
  720. }
  721. if (this.currentMaxLayer === 0) {
  722. this.currentMixedMode = false
  723. this.scanLayerOptions = Array.from({ length: maxExistingLayer + 1 }, (_, i) => i + 1)
  724. } else {
  725. this.currentMixedMode = false
  726. this.scanLayerOptions = Array.from({ length: this.currentMaxLayer }, (_, i) => i + 1)
  727. }
  728. this.loadAvailablePositions()
  729. } else {
  730. this.$alert((data && data.msg) || '获取栈板信息失败', '错误', { confirmButtonText: '确定' })
  731. }
  732. }).catch(() => {
  733. this.$alert('获取栈板信息失败', '错误', { confirmButtonText: '确定' })
  734. })
  735. },
  736. loadAvailablePositions() {
  737. this.positionGridLoading = true
  738. this.availablePositions = []
  739. getPalletDetails({
  740. site: this.site,
  741. palletId: this.palletCode,
  742. position: '',
  743. layer: this.scanLayer
  744. }).then(({ data: detailData }) => {
  745. const positionCountMap = {}
  746. if (detailData && detailData.code === 0) {
  747. const details = detailData.details || []
  748. details.forEach((detail) => {
  749. const pos = detail.position
  750. positionCountMap[pos] = (positionCountMap[pos] || 0) + 1
  751. })
  752. }
  753. return getAvailablePositionsForLayer({
  754. site: this.site,
  755. palletId: this.palletCode,
  756. layer: this.scanLayer
  757. }).then(({ data }) => {
  758. if (data && data.code === 0) {
  759. const result = data.data || {}
  760. const positions = result.positions || []
  761. this.availablePositions = result.availablePositions || []
  762. this.positionGrid = positions.map((position) => ({
  763. position,
  764. count: positionCountMap[position] || 0
  765. }))
  766. } else {
  767. this.availablePositions = []
  768. this.positionGrid = []
  769. this.$alert((data && data.msg) || '获取位置信息失败', '错误', { confirmButtonText: '确定' })
  770. }
  771. })
  772. }).catch(() => {
  773. this.availablePositions = []
  774. this.positionGrid = []
  775. this.$alert('获取位置信息失败', '错误', { confirmButtonText: '确定' })
  776. }).finally(() => {
  777. this.positionGridLoading = false
  778. })
  779. },
  780. handleLayerClick(layer) {
  781. if (this.scanLayer === layer) {
  782. return
  783. }
  784. this.scanLayer = layer
  785. this.currentSelectedPosition = ''
  786. this.loadAvailablePositions()
  787. },
  788. handlePositionClick(position) {
  789. if (this.positionGridLoading) {
  790. this.$message.warning('位置信息加载中,请稍候')
  791. return
  792. }
  793. if (!this.availablePositions.includes(position)) {
  794. this.$message.warning('该位置不可用')
  795. return
  796. }
  797. this.currentSelectedPosition = position
  798. this.scanPosition = position
  799. this.$nextTick(() => {
  800. if (this.$refs.scanInput) {
  801. this.$refs.scanInput.focus()
  802. }
  803. })
  804. },
  805. moveFocusToScanInput() {
  806. this.$nextTick(() => {
  807. if (this.$refs.scanInput) {
  808. this.$refs.scanInput.focus()
  809. }
  810. })
  811. },
  812. closeScanModal() {
  813. this.scanModalVisible = false
  814. if (this.needRefreshOnClose) {
  815. this.refreshTable()
  816. this.needRefreshOnClose = false
  817. }
  818. },
  819. handleLabelScan() {
  820. if (!this.ensureSite() || this.interfaceBusy) {
  821. return
  822. }
  823. if (!this.scanCode || !this.scanCode.trim()) {
  824. this.scanCode = ''
  825. this.$alert('请输入标签编码', '错误', { confirmButtonText: '确定' })
  826. this.moveFocusToScanInput()
  827. return
  828. }
  829. if (this.operationType === 'in') {
  830. if (!this.scanPosition) {
  831. this.scanCode = ''
  832. this.$alert('请选择位置', '错误', { confirmButtonText: '确定' })
  833. this.moveFocusToScanInput()
  834. return
  835. }
  836. if (!this.scanLayer) {
  837. this.scanCode = ''
  838. this.$alert('请选择层数', '错误', { confirmButtonText: '确定' })
  839. this.moveFocusToScanInput()
  840. return
  841. }
  842. this.scanRequestLoading = true
  843. savePalletDetail({
  844. site: this.site,
  845. palletId: this.palletCode,
  846. position: this.scanPosition,
  847. layer: this.scanLayer,
  848. serialNo: this.scanCode,
  849. sortFlag: 0
  850. }).then(({ data }) => {
  851. if (data && data.code === 0) {
  852. this.$message.success('扫进成功')
  853. this.needRefreshOnClose = true
  854. this.scanCode = ''
  855. this.loadLayerOptions()
  856. this.moveFocusToScanInput()
  857. } else {
  858. this.scanCode = ''
  859. this.$alert((data && data.msg) || '扫进失败', '错误', { confirmButtonText: '确定' })
  860. this.moveFocusToScanInput()
  861. }
  862. }).catch((error) => {
  863. this.scanCode = ''
  864. this.$alert(error.message || '扫进失败', '错误', { confirmButtonText: '确定' })
  865. this.moveFocusToScanInput()
  866. }).finally(() => {
  867. this.scanRequestLoading = false
  868. })
  869. } else {
  870. const scanOutItem = this.detailList.find((item) => item.serialNo === this.scanCode)
  871. this.scanRequestLoading = true
  872. deletePalletDetail({
  873. site: this.site,
  874. palletId: this.palletCode,
  875. serialNo: this.scanCode
  876. }).then(({ data }) => {
  877. if (data && data.code === 0) {
  878. if (scanOutItem) {
  879. this.scanOutList.push({
  880. position: scanOutItem.position,
  881. layer: scanOutItem.layer,
  882. serialNo: scanOutItem.serialNo
  883. })
  884. }
  885. this.$message.success('扫出成功')
  886. this.needRefreshOnClose = true
  887. this.scanCode = ''
  888. this.moveFocusToScanInput()
  889. } else {
  890. this.scanCode = ''
  891. this.$alert((data && data.msg) || '扫出失败', '错误', { confirmButtonText: '确定' })
  892. this.moveFocusToScanInput()
  893. }
  894. }).catch((error) => {
  895. this.scanCode = ''
  896. this.$alert(error.message || '扫出失败', '错误', { confirmButtonText: '确定' })
  897. this.moveFocusToScanInput()
  898. }).finally(() => {
  899. this.scanRequestLoading = false
  900. })
  901. }
  902. },
  903. loadAreaOptions() {
  904. getAreaOptionsForChange({ site: this.site }).then(({ data }) => {
  905. if (data && data.code === 0) {
  906. this.areaOptions = data.rows || []
  907. } else {
  908. this.areaOptions = []
  909. }
  910. }).catch(() => {
  911. this.areaOptions = []
  912. })
  913. },
  914. openCallEmptyModal() {
  915. if (!this.ensureSite()) {
  916. return
  917. }
  918. this.callEmptyForm = { areaId: '', stationCode: '', palletType: this.currentPalletType || '' }
  919. this.stationOptions = []
  920. this.loadAreaOptions()
  921. this.loadAllPalletTypeListForCallEmpty()
  922. this.callEmptyModalVisible = true
  923. },
  924. handleAreaChangeForCall() {
  925. this.callEmptyForm.stationCode = ''
  926. if (!this.callEmptyForm.areaId) {
  927. this.stationOptions = []
  928. return
  929. }
  930. getStationsByArea({
  931. site: this.site,
  932. areaId: this.callEmptyForm.areaId
  933. }).then(({ data }) => {
  934. if (data && data.code === 0) {
  935. this.stationOptions = data.rows || []
  936. } else {
  937. this.stationOptions = []
  938. }
  939. }).catch(() => {
  940. this.stationOptions = []
  941. })
  942. },
  943. submitCallEmpty() {
  944. if (this.interfaceBusy) {
  945. return
  946. }
  947. if (!this.callEmptyForm.areaId || !this.callEmptyForm.stationCode || !this.callEmptyForm.palletType) {
  948. this.$alert('请完整选择区域、站点和栈板类型', '提示', { confirmButtonText: '确定' })
  949. return
  950. }
  951. this.callEmptyLoading = true
  952. callEmptyPalletToStation({
  953. site: this.site,
  954. // 调用空托盘下发时,targetArea传业务区域ID,由后端映射haian_area.wcs_area_code - rqrq
  955. areaId: this.callEmptyForm.areaId,
  956. targetArea: this.callEmptyForm.areaId,
  957. stationCode: this.callEmptyForm.stationCode,
  958. palletType: this.callEmptyForm.palletType
  959. }).then(({ data }) => {
  960. if (data && data.code === 0) {
  961. this.$message.success('调用空托盘成功')
  962. this.callEmptyModalVisible = false
  963. } else {
  964. this.$alert((data && data.msg) || '调用空托盘失败', '错误', { confirmButtonText: '确定' })
  965. }
  966. }).catch((error) => {
  967. this.$alert(error.message || '调用空托盘失败', '错误', { confirmButtonText: '确定' })
  968. }).finally(() => {
  969. this.callEmptyLoading = false
  970. })
  971. },
  972. // 推送组盘前校验:area/location任何一个有值都必须阻断 - rqrq
  973. openPushAssemblyDialog() {
  974. if (!this.ensureSite() || this.interfaceBusy) {
  975. return
  976. }
  977. if (!this.palletScanned) {
  978. this.$alert('请先扫描栈板', '提示', { confirmButtonText: '确定' })
  979. return
  980. }
  981. if (!this.detailList || this.detailList.length === 0) {
  982. this.$alert('栈板明细为空,请先扫进物料', '提示', { confirmButtonText: '确定' })
  983. return
  984. }
  985. if (!this.canPushAssembly) {
  986. this.$alert('推送组盘仅允许未绑定站点的栈板,当前栈板area或location_code存在值', '提示', { confirmButtonText: '确定' })
  987. return
  988. }
  989. this.selectedInboundPort = ''
  990. this.pushAssemblyDialogVisible = true
  991. },
  992. closePushAssemblyDialog() {
  993. if (this.interfaceBusy) {
  994. return
  995. }
  996. this.pushAssemblyDialogVisible = false
  997. this.selectedInboundPort = ''
  998. },
  999. // 推送组盘:确认入库口后调用海安专用后端方法,调用期间保持全页面按钮锁定 - rqrq
  1000. submitPushAssembly() {
  1001. if (!this.selectedInboundPort) {
  1002. this.$alert('请选择入库口', '提示', { confirmButtonText: '确定' })
  1003. return
  1004. }
  1005. this.$confirm('确认推送组盘数据吗?', '提示', {
  1006. confirmButtonText: '确认',
  1007. cancelButtonText: '取消',
  1008. type: 'warning'
  1009. }).then(() => {
  1010. this.pushAssemblyLoading = true
  1011. pushPalletAssemblyToInboundPort({
  1012. site: this.site,
  1013. palletId: this.palletCode,
  1014. inboundPort: this.selectedInboundPort
  1015. }).then(({ data }) => {
  1016. if (data && data.code === 0) {
  1017. this.$message.success('推送组盘成功')
  1018. this.pushAssemblyDialogVisible = false
  1019. this.resetPage(true)
  1020. } else {
  1021. this.$alert((data && data.msg) || '推送组盘失败', '错误', { confirmButtonText: '确定' })
  1022. }
  1023. }).catch((error) => {
  1024. this.$alert(error.message || '推送组盘失败', '错误', { confirmButtonText: '确定' })
  1025. }).finally(() => {
  1026. this.pushAssemblyLoading = false
  1027. })
  1028. }).catch(() => {
  1029. // 用户取消确认时不清空数据,便于继续核对后再提交 - rqrq
  1030. })
  1031. }
  1032. },
  1033. activated() {
  1034. this.resetPage(true)
  1035. },
  1036. beforeRouteLeave(to, from, next) {
  1037. this.resetPage(false)
  1038. next()
  1039. },
  1040. mounted() {
  1041. this.resetPage(true)
  1042. }
  1043. }
  1044. </script>
  1045. <style scoped>
  1046. .header-action-disabled {
  1047. opacity: 0.5;
  1048. pointer-events: none;
  1049. }
  1050. .detail-table {
  1051. background: white;
  1052. border-radius: 6px;
  1053. overflow: hidden;
  1054. border: 1px solid #e0e0e0;
  1055. }
  1056. .table-header,
  1057. .table-row {
  1058. display: flex;
  1059. align-items: center;
  1060. padding: 8px;
  1061. border-bottom: 1px solid #e0e0e0;
  1062. }
  1063. .table-header {
  1064. background: #f5f5f5;
  1065. font-weight: bold;
  1066. font-size: 14px;
  1067. }
  1068. .col-position {
  1069. flex: 1;
  1070. text-align: center;
  1071. }
  1072. .col-layer {
  1073. flex: 1;
  1074. text-align: center;
  1075. }
  1076. .col-serial {
  1077. flex: 4;
  1078. text-align: center;
  1079. word-break: break-all;
  1080. }
  1081. .empty-row {
  1082. justify-content: center;
  1083. align-items: center;
  1084. padding: 20px;
  1085. border-bottom: none;
  1086. }
  1087. .empty-hint {
  1088. text-align: center;
  1089. color: #999;
  1090. width: 100%;
  1091. }
  1092. .scan-modal-content {
  1093. padding: 10px 0;
  1094. }
  1095. .modal-form {
  1096. margin-bottom: 16px;
  1097. }
  1098. .dialog-footer {
  1099. text-align: center;
  1100. }
  1101. .action-btn:disabled {
  1102. opacity: 0.6;
  1103. cursor: not-allowed;
  1104. background-color: #ccc !important;
  1105. border-color: #ccc !important;
  1106. }
  1107. .action-btn.warning {
  1108. background-color: #f59e0b;
  1109. border-color: #f59e0b;
  1110. color: #fff;
  1111. }
  1112. ::v-deep .el-dialog__wrapper {
  1113. z-index: 2000 !important;
  1114. }
  1115. ::v-deep .el-overlay {
  1116. z-index: 2000 !important;
  1117. }
  1118. ::v-deep .el-radio {
  1119. margin-right: 8px !important;
  1120. }
  1121. ::v-deep .el-radio__label {
  1122. font-size: 14px;
  1123. }
  1124. .layer-grid {
  1125. display: flex;
  1126. flex-wrap: wrap;
  1127. gap: 8px;
  1128. margin-top: 8px;
  1129. }
  1130. .layer-item {
  1131. display: flex;
  1132. align-items: center;
  1133. justify-content: center;
  1134. min-width: 50px;
  1135. height: 45px;
  1136. padding: 0 12px;
  1137. background-color: #fff;
  1138. border: 2px solid #dcdfe6;
  1139. border-radius: 6px;
  1140. font-size: 15px;
  1141. font-weight: bold;
  1142. cursor: pointer;
  1143. transition: all 0.3s;
  1144. user-select: none;
  1145. }
  1146. .layer-item:hover {
  1147. border-color: #409eff;
  1148. background-color: #ecf5ff;
  1149. }
  1150. .layer-item.layer-selected {
  1151. border-color: #409eff;
  1152. background-color: #409eff;
  1153. color: #fff;
  1154. }
  1155. .position-grid {
  1156. display: grid;
  1157. gap: 10px;
  1158. margin-top: 8px;
  1159. position: relative;
  1160. }
  1161. .position-grid.position-grid-1 {
  1162. grid-template-columns: 1fr;
  1163. grid-template-rows: 1fr;
  1164. }
  1165. .position-grid.position-grid-4 {
  1166. grid-template-columns: repeat(2, 1fr);
  1167. grid-template-rows: repeat(2, 1fr);
  1168. grid-auto-flow: column;
  1169. }
  1170. .position-grid.position-grid-9 {
  1171. grid-template-columns: repeat(3, 1fr);
  1172. grid-template-rows: repeat(3, 1fr);
  1173. grid-auto-flow: row;
  1174. }
  1175. .position-item.position-placeholder {
  1176. background-color: #f0f0f0 !important;
  1177. border: 1px dashed #d0d0d0 !important;
  1178. color: transparent !important;
  1179. cursor: not-allowed !important;
  1180. pointer-events: none !important;
  1181. }
  1182. .position-grid.position-grid-loading {
  1183. opacity: 0.7;
  1184. pointer-events: none;
  1185. }
  1186. .position-item {
  1187. display: flex;
  1188. align-items: center;
  1189. justify-content: center;
  1190. height: 60px;
  1191. background-color: #fff;
  1192. border: 2px solid #dcdfe6;
  1193. border-radius: 6px;
  1194. font-size: 16px;
  1195. font-weight: bold;
  1196. cursor: pointer;
  1197. transition: all 0.3s;
  1198. user-select: none;
  1199. }
  1200. .position-item:hover:not(.position-disabled) {
  1201. border-color: #409eff;
  1202. background-color: #ecf5ff;
  1203. }
  1204. .position-item.position-selected {
  1205. border-color: #409eff;
  1206. background-color: #409eff;
  1207. color: #fff;
  1208. }
  1209. .position-item.position-disabled {
  1210. background-color: #f5f7fa;
  1211. color: #c0c4cc;
  1212. cursor: not-allowed;
  1213. opacity: 0.6;
  1214. }
  1215. .position-item.position-loading {
  1216. background: linear-gradient(90deg, #f5f7fa 25%, #e4e7ed 50%, #f5f7fa 75%);
  1217. background-size: 200% 100%;
  1218. animation: loading 1.5s ease-in-out infinite;
  1219. }
  1220. @keyframes loading {
  1221. 0% {
  1222. background-position: 200% 0;
  1223. }
  1224. 100% {
  1225. background-position: -200% 0;
  1226. }
  1227. }
  1228. </style>