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.

1131 lines
37 KiB

4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
3 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
3 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks 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. <!-- 第一行栈板扫描 + 结束分拣按钮保持与原PDA页面一致 - rqrq -->
  13. <div class="input-group">
  14. <label class="input-label">栈板编码</label>
  15. <div style="display: flex; gap: 8px;">
  16. <el-input
  17. v-model="palletCode"
  18. placeholder="请扫描栈板编码"
  19. class="form-input"
  20. style="flex: 0.75;"
  21. clearable
  22. inputmode="none"
  23. autocomplete="off"
  24. autocorrect="off"
  25. spellcheck="false"
  26. @keyup.enter.native="handlePalletScan"
  27. ref="palletInput"
  28. />
  29. <button class="action-btn secondary" style="flex: 0.25; margin: 0;" @click="submitFinishSorting" :disabled="interfaceBusy || finishSortingLoading || detailList.length === 0">
  30. {{ finishSortingLoading ? '处理中...' : '结束分拣' }}
  31. </button>
  32. </div>
  33. </div>
  34. <!-- 第二行栈板类型和自动分拣保持与原PDA页面一致 - rqrq -->
  35. <div v-if="palletScanned" class="input-group">
  36. <div style="display: flex; gap: 8px; align-items: end;">
  37. <div style="flex: 0.65;">
  38. <label class="input-label">栈板类型</label>
  39. <el-select
  40. v-model="currentPalletType"
  41. placeholder="请选择栈板类型"
  42. style="width: 100%;"
  43. :disabled="palletTypeDisabled"
  44. @change="handlePalletTypeChange"
  45. >
  46. <el-option
  47. v-for="type in palletTypeOptions"
  48. :key="type.palletType"
  49. :label="`${type.palletType} - ${type.typeDesc}`"
  50. :value="type.palletType"
  51. />
  52. </el-select>
  53. </div>
  54. <div style="flex: 0.35;">
  55. <label class="input-label">自动分拣</label>
  56. <el-select
  57. v-model="currentAutoSort"
  58. placeholder="请选择"
  59. style="width: 100%;"
  60. :disabled="autoSortDisabled"
  61. @change="handleAutoSortChange"
  62. >
  63. <el-option label="是" value="Y"></el-option>
  64. <el-option label="否" value="N"></el-option>
  65. </el-select>
  66. </div>
  67. </div>
  68. </div>
  69. <!-- 第2.5是否强制整托出库保持与原PDA页面一致 - rqrq -->
  70. <div v-if="palletScanned" class="input-group">
  71. <el-checkbox
  72. v-model="forceFullPalletOut"
  73. :disabled="palletTypeDisabled"
  74. @change="handleForceFullPalletOutChange"
  75. style="width: 100%;"
  76. >
  77. 是否强制整托出库
  78. </el-checkbox>
  79. </div>
  80. <!-- 第三行当前站点 + 刷新保持与原PDA页面一致 - rqrq -->
  81. <div v-if="palletScanned" class="input-group">
  82. <label class="input-label">当前站点</label>
  83. <div style="display: flex; gap: 8px;">
  84. <el-input
  85. v-model="currentPalletStation"
  86. placeholder="栈板当前站点"
  87. class="form-input"
  88. readonly
  89. style="flex: 0.75;"
  90. />
  91. <button
  92. class="action-btn secondary"
  93. style="flex: 0.25; margin: 0; white-space: nowrap;"
  94. @click="refreshTable"
  95. :disabled="interfaceBusy"
  96. >
  97. 刷新
  98. </button>
  99. </div>
  100. </div>
  101. <!-- 第四行扫进/扫回 + 扫描按钮保持与原PDA页面一致 - rqrq -->
  102. <div v-if="palletScanned" class="input-group">
  103. <div style="display: flex; gap: 8px; align-items: center;">
  104. <div style="flex: 0.75;">
  105. <el-radio-group v-model="operationType" style="display: flex;">
  106. <el-radio label="in" style="margin-right: 0px; white-space: nowrap;">扫进</el-radio>
  107. <el-radio label="out" style="white-space: nowrap;">扫回</el-radio>
  108. </el-radio-group>
  109. </div>
  110. <button
  111. class="action-btn secondary"
  112. style="flex: 0.25; margin: 0; white-space: nowrap;"
  113. :disabled="interfaceBusy || currentPalletType === ''"
  114. @click="showScanModal"
  115. >
  116. 扫描条码
  117. </button>
  118. </div>
  119. </div>
  120. </div>
  121. <!-- 分拣明细区域按钮排版与原PDA页面一致 - rqrq -->
  122. <div v-if="palletScanned" class="rma-list">
  123. <div class="list-title-row" style="display: flex; gap: 8px; align-items: center; padding: 0;">
  124. <div class="list-title" style="flex: 0.75; margin: 0;">
  125. <button class="action-btn primary" style="flex: 0.25; margin: 0;" @click="showDetailModal" :disabled="interfaceBusy || submitSortingLoading">
  126. 浏览明细
  127. </button>
  128. <label style="margin-left: 5px;">条码数:{{ detailList.length }}</label>
  129. </div>
  130. <button class="action-btn primary" style="flex: 0.25; margin: 0;" @click="submitSorting" :disabled="interfaceBusy || submitSortingLoading">
  131. {{ submitSortingLoading ? '处理中...' : '提交分拣' }}
  132. </button>
  133. </div>
  134. <div class="detail-table">
  135. <div class="table-header">
  136. <div class="col-position">位置</div>
  137. <div class="col-layer">层数</div>
  138. <div class="col-serial">标签号</div>
  139. </div>
  140. <div v-for="(item, index) in detailList" :key="index" class="table-row">
  141. <div class="col-position">{{ item.position }}</div>
  142. <div class="col-layer">{{ item.layer }}</div>
  143. <div class="col-serial">{{ item.serialNo }}</div>
  144. </div>
  145. <div v-if="detailList.length === 0" class="table-row empty-row">
  146. <div class="empty-hint">暂无分拣明细数据</div>
  147. </div>
  148. </div>
  149. </div>
  150. </div>
  151. <!-- 浏览明细弹窗保持与原PDA页面一致 - rqrq -->
  152. <el-dialog
  153. :title="'已扫描数量:' + detailList.length"
  154. :visible.sync="detailModalVisible"
  155. width="90%"
  156. :close-on-click-modal="false"
  157. :show-close="false"
  158. :append-to-body="true"
  159. >
  160. <div class="table-body" style="max-height: 240px; overflow-y: auto;">
  161. <div class="detail-table">
  162. <div class="table-header">
  163. <div class="col-position">位置</div>
  164. <div class="col-layer">层数</div>
  165. <div class="col-serial">标签号</div>
  166. </div>
  167. <div v-for="(item, index) in detailList" :key="index" class="table-row">
  168. <div class="col-position">{{ item.position }}</div>
  169. <div class="col-layer">{{ item.layer }}</div>
  170. <div class="col-serial">{{ item.serialNo }}</div>
  171. </div>
  172. <div v-if="detailList.length === 0" class="table-row empty-row">
  173. <div class="empty-hint">暂无分拣明细数据</div>
  174. </div>
  175. </div>
  176. </div>
  177. <div slot="footer" class="dialog-footer">
  178. <button class="action-btn secondary" style="margin-left: 10px;" @click="detailModalVisible = false" :disabled="interfaceBusy">取消</button>
  179. </div>
  180. </el-dialog>
  181. <!-- 扫码模态框网格层数与原PDA页面保持一致 - rqrq -->
  182. <el-dialog
  183. title="扫描标签"
  184. :visible.sync="scanModalVisible"
  185. width="90%"
  186. :close-on-click-modal="false"
  187. :show-close="false"
  188. :modal="true"
  189. :modal-append-to-body="true"
  190. :append-to-body="true"
  191. >
  192. <div class="scan-modal-content">
  193. <!-- 扫进时显示层数选择和位置网格 - rqrq -->
  194. <div v-if="operationType === 'in'" class="modal-form">
  195. <!-- 层数选择混装托盘不显示 - rqrq -->
  196. <div v-if="!currentMixedMode && scanLayerOptions.length > 0" class="input-group">
  197. <label class="input-label">层数当前选择{{ scanLayer }}</label>
  198. <div class="layer-grid">
  199. <div
  200. v-for="layer in scanLayerOptions"
  201. :key="layer"
  202. class="layer-item"
  203. :class="{
  204. 'layer-selected': scanLayer === layer
  205. }"
  206. @click="handleLayerClick(layer)"
  207. >
  208. {{ layer }}
  209. </div>
  210. </div>
  211. </div>
  212. <!-- 位置网格选择器 - rqrq -->
  213. <div class="input-group">
  214. <label class="input-label">
  215. 位置{{ currentSelectedPosition ? `(当前选择:${currentSelectedPosition}` : '' }}
  216. <span v-if="positionGridLoading" style="color: #909399; font-size: 12px;">加载中...</span>
  217. </label>
  218. <div class="position-grid" :class="[
  219. {'position-grid-loading': positionGridLoading},
  220. getPositionGridClass
  221. ]">
  222. <div
  223. v-for="(item, index) in displayPositionGrid"
  224. :key="index"
  225. class="position-item"
  226. :class="{
  227. 'position-disabled': positionGridLoading || !availablePositions.includes(item.position),
  228. 'position-selected': currentSelectedPosition === item.position,
  229. 'position-loading': positionGridLoading,
  230. 'position-placeholder': item.isPlaceholder
  231. }"
  232. @click="handlePositionClick(item.position)"
  233. >
  234. <template v-if="!item.isPlaceholder">{{ item.position }}({{ item.count }})</template>
  235. </div>
  236. </div>
  237. </div>
  238. </div>
  239. <!-- 标签扫描 - rqrq -->
  240. <div class="input-group">
  241. <label class="input-label">标签二维码</label>
  242. <el-input
  243. ref="scanInput"
  244. v-model="scanCode"
  245. placeholder="请扫描标签二维码"
  246. class="form-input"
  247. clearable
  248. :disabled="operationType === 'in' && !currentSelectedPosition"
  249. @keyup.enter.native="handleLabelScan"
  250. inputmode="none"
  251. autocomplete="off"
  252. autocorrect="off"
  253. spellcheck="false"
  254. />
  255. </div>
  256. </div>
  257. <div slot="footer" class="dialog-footer">
  258. <button class="action-btn secondary" style="margin-left: 10px;" @click="closeScanModal" :disabled="interfaceBusy">取消</button>
  259. </div>
  260. </el-dialog>
  261. </div>
  262. </template>
  263. <script>
  264. import {
  265. checkPalletExists,
  266. getPalletDetails,
  267. savePalletDetail,
  268. restorePalletDetail,
  269. getLabelInfo,
  270. getPalletInfo,
  271. getPalletTypeList,
  272. updatePalletTypeAndAutoSort,
  273. getAvailablePositionsForLayer,
  274. completePalletAssemblyForFenJian,
  275. finishSorting,
  276. getSortingList
  277. } from '../../../api/haianAutoWarehouse/palletPacking'
  278. export default {
  279. name: 'haianPalletSorting',
  280. data() {
  281. return {
  282. site: this.$store.state.user.site,
  283. palletCode: '',
  284. palletScanned: false,
  285. operationType: 'in',
  286. detailModalVisible: false,
  287. currentPalletType: '',
  288. currentAutoSort: 'N',
  289. currentPalletStation: '',
  290. forceFullPalletOut: false,
  291. palletTypeDisabled: false,
  292. autoSortDisabled: false,
  293. currentWcsAutoSort: '',
  294. currentMaxLayer: 0,
  295. positionOptions: [],
  296. palletTypeOptions: [],
  297. detailList: [],
  298. sortingList: [],
  299. // 分拣扫回缓存:记录扫进前原位置信息,便于扫回恢复 - rqrq
  300. originalDetailCache: {},
  301. palletInfo: {
  302. palletType: '',
  303. autoSort: 'N',
  304. locationCode: ''
  305. },
  306. // 扫码模态框字段:与原PDA页面保持一致,按网格位置 + 层数扫入 - rqrq
  307. scanModalVisible: false,
  308. scanCode: '',
  309. scanPosition: '',
  310. scanLayer: 1,
  311. scanLayerOptions: [],
  312. needRefreshOnClose: false,
  313. positionGrid: [],
  314. availablePositions: [],
  315. currentSelectedPosition: '',
  316. currentMixedMode: false,
  317. positionGridLoading: false,
  318. submitSortingLoading: false,
  319. finishSortingLoading: false,
  320. // 接口防连点相关状态:接口处理中统一禁用按钮、回退与首页,等待返回后解锁 - rqrq
  321. palletScanLoading: false,
  322. palletConfigLoading: false,
  323. scanRequestLoading: false,
  324. }
  325. },
  326. computed: {
  327. // 根据位置数量决定网格布局,确保与原PDA扫码弹窗一致 - rqrq
  328. getPositionGridClass() {
  329. const len = this.positionGrid.length
  330. if (len === 1) return 'position-grid-1'
  331. if (len === 2) return 'position-grid-4'
  332. if (len === 4) return 'position-grid-4'
  333. return 'position-grid-9'
  334. },
  335. // 2宫格场景采用对角展示,其余按原位置顺序展示 - rqrq
  336. displayPositionGrid() {
  337. const len = this.positionGrid.length
  338. if (len === 2) {
  339. const pos1 = this.positionGrid.find((p) => p.position === '1') || { position: '1', count: 0 }
  340. const pos2 = this.positionGrid.find((p) => p.position === '2') || { position: '2', count: 0 }
  341. return [
  342. { ...pos1, isPlaceholder: false },
  343. { position: '', count: 0, isPlaceholder: true },
  344. { position: '', count: 0, isPlaceholder: true },
  345. { ...pos2, isPlaceholder: false }
  346. ]
  347. }
  348. return this.positionGrid.map((p) => ({ ...p, isPlaceholder: false }))
  349. },
  350. // 页面接口忙碌态:任一按钮触发接口未返回时视为忙碌,统一禁用关键入口 - rqrq
  351. interfaceBusy() {
  352. return this.palletScanLoading || this.palletConfigLoading || this.scanRequestLoading || this.submitSortingLoading || this.finishSortingLoading
  353. }
  354. },
  355. methods: {
  356. ensureSite() {
  357. if (!this.site) {
  358. this.$alert('当前用户缺少site,无法执行海安分拣业务', '提示', { confirmButtonText: '确定' })
  359. return false
  360. }
  361. return true
  362. },
  363. handleBack() {
  364. if (this.interfaceBusy) {
  365. return
  366. }
  367. // 返回上一页前先清空分拣页状态,避免keep-alive缓存导致二次进入残留旧数据 - rqrq
  368. this.resetPage(false)
  369. this.$router.back()
  370. },
  371. // 接口处理中禁用首页跳转,避免用户离开后重复触发或产生脏状态 - rqrq
  372. goHome() {
  373. if (this.interfaceBusy) {
  374. return
  375. }
  376. this.$router.push({ path: '/' })
  377. },
  378. // 海安分拣页统一重置方法:退出和重进都走同一逻辑,避免状态漂移 - rqrq
  379. resetPage(needFocus = true) {
  380. this.site = this.$store.state.user.site
  381. this.palletCode = ''
  382. this.palletScanned = false
  383. this.operationType = 'in'
  384. this.detailModalVisible = false
  385. this.currentPalletType = ''
  386. this.currentAutoSort = 'N'
  387. this.currentPalletStation = ''
  388. this.forceFullPalletOut = false
  389. this.palletTypeDisabled = false
  390. this.autoSortDisabled = false
  391. this.currentWcsAutoSort = ''
  392. this.currentMaxLayer = 0
  393. this.positionOptions = []
  394. this.detailList = []
  395. this.sortingList = []
  396. this.originalDetailCache = {}
  397. this.palletInfo = { palletType: '', autoSort: 'N', locationCode: '' }
  398. this.scanCode = ''
  399. this.scanPosition = ''
  400. this.scanLayer = 1
  401. this.scanModalVisible = false
  402. this.scanLayerOptions = []
  403. this.needRefreshOnClose = false
  404. this.positionGrid = []
  405. this.availablePositions = []
  406. this.currentSelectedPosition = ''
  407. this.currentMixedMode = false
  408. this.positionGridLoading = false
  409. this.submitSortingLoading = false
  410. this.finishSortingLoading = false
  411. this.palletScanLoading = false
  412. this.palletConfigLoading = false
  413. this.scanRequestLoading = false
  414. if (needFocus) {
  415. this.$nextTick(() => {
  416. if (this.$refs.palletInput) {
  417. this.$refs.palletInput.focus()
  418. }
  419. })
  420. }
  421. },
  422. handlePalletScan() {
  423. if (!this.ensureSite()) {
  424. return
  425. }
  426. if (this.interfaceBusy) {
  427. return
  428. }
  429. if (!this.palletCode || !this.palletCode.trim()) {
  430. this.$alert('请输入栈板编码', '提示', { confirmButtonText: '确定' })
  431. return
  432. }
  433. this.palletScanLoading = true
  434. checkPalletExists({
  435. site: this.site,
  436. palletId: this.palletCode.trim()
  437. }).then(({ data }) => {
  438. if (data && data.code === 0) {
  439. this.palletCode = data.palletId
  440. this.positionOptions = data.positions || []
  441. this.palletScanned = true
  442. this.scanCode = ''
  443. this.scanPosition = ''
  444. this.scanLayer = 1
  445. this.currentSelectedPosition = ''
  446. this.sortingList = []
  447. this.originalDetailCache = {}
  448. this.loadPalletInfo()
  449. this.refreshTable()
  450. } else {
  451. this.$alert((data && data.msg) || '栈板校验失败', '错误', { confirmButtonText: '确定' })
  452. }
  453. }).catch((error) => {
  454. this.$alert(error.message || '栈板校验失败', '错误', { confirmButtonText: '确定' })
  455. }).finally(() => {
  456. this.palletScanLoading = false
  457. })
  458. },
  459. loadPalletInfo() {
  460. getPalletInfo({
  461. site: this.site,
  462. palletId: this.palletCode
  463. }).then(({ data }) => {
  464. if (data && data.code === 0) {
  465. const row = data.row || {}
  466. this.currentPalletType = row.palletType || ''
  467. this.currentAutoSort = row.autoSort || 'N'
  468. this.currentPalletStation = row.locationCode || ''
  469. this.forceFullPalletOut = Number(row.soreType) === 3
  470. this.palletInfo.palletType = this.currentPalletType
  471. this.palletInfo.autoSort = this.currentAutoSort
  472. this.palletInfo.locationCode = this.currentPalletStation
  473. this.loadPalletTypeList()
  474. }
  475. })
  476. },
  477. // 读取托盘类型清单并推导自动分拣能力,保证海安页面与原PDA页面交互一致 - rqrq
  478. loadPalletTypeList() {
  479. getPalletTypeList({
  480. site: this.site,
  481. palletFamily: '',
  482. active: 'Y'
  483. }).then(({ data }) => {
  484. if (data && data.code === 0) {
  485. this.palletTypeOptions = data.rows || []
  486. const selectedType = this.palletTypeOptions.find((item) => item.palletType === this.currentPalletType)
  487. this.currentWcsAutoSort = selectedType ? (selectedType.wcsAutoSort || 'N') : ''
  488. this.currentMaxLayer = selectedType ? Number(selectedType.maxLayer || 0) : 0
  489. this.updateAutoSortControl()
  490. }
  491. })
  492. },
  493. refreshTable() {
  494. getPalletDetails({
  495. site: this.site,
  496. palletId: this.palletCode,
  497. position: '',
  498. layer: null
  499. }).then(({ data }) => {
  500. if (data && data.code === 0) {
  501. this.detailList = data.details || []
  502. } else {
  503. this.detailList = []
  504. }
  505. this.palletTypeDisabled = this.detailList.length > 0
  506. this.updateAutoSortControl()
  507. }).catch(() => {
  508. this.detailList = []
  509. this.palletTypeDisabled = false
  510. this.updateAutoSortControl()
  511. })
  512. },
  513. handlePalletTypeChange() {
  514. const selectedType = this.palletTypeOptions.find((item) => item.palletType === this.currentPalletType)
  515. this.currentWcsAutoSort = selectedType ? (selectedType.wcsAutoSort || 'N') : ''
  516. this.currentMaxLayer = selectedType ? Number(selectedType.maxLayer || 0) : 0
  517. if (this.currentPalletType === 'A0103') {
  518. this.forceFullPalletOut = true
  519. }
  520. this.updateAutoSortControl()
  521. this.savePalletTypeConfig()
  522. },
  523. handleAutoSortChange() {
  524. this.savePalletTypeConfig()
  525. },
  526. handleForceFullPalletOutChange() {
  527. this.savePalletTypeConfig()
  528. },
  529. // 统一自动分拣禁用策略:强制整托、类型不支持、已有明细三类场景锁定 - rqrq
  530. updateAutoSortControl() {
  531. if (this.forceFullPalletOut || this.currentPalletType === 'A0103') {
  532. this.currentAutoSort = 'N'
  533. this.autoSortDisabled = true
  534. return
  535. }
  536. if (this.currentWcsAutoSort === 'N') {
  537. this.currentAutoSort = 'N'
  538. this.autoSortDisabled = true
  539. return
  540. }
  541. this.autoSortDisabled = this.detailList.length > 0
  542. },
  543. savePalletTypeConfig() {
  544. if (this.interfaceBusy) {
  545. return
  546. }
  547. this.palletConfigLoading = true
  548. updatePalletTypeAndAutoSort({
  549. site: this.site,
  550. palletId: this.palletCode,
  551. palletType: this.currentPalletType,
  552. autoSort: this.currentAutoSort,
  553. forceFullPalletOut: this.forceFullPalletOut
  554. }).finally(() => {
  555. this.palletConfigLoading = false
  556. })
  557. },
  558. // 打开扫码弹窗并初始化网格数据,交互与原PDA页面一致 - rqrq
  559. showScanModal() {
  560. this.scanModalVisible = true
  561. this.scanCode = ''
  562. this.currentSelectedPosition = ''
  563. this.needRefreshOnClose = false
  564. if (this.operationType === 'in') {
  565. this.scanLayer = 1
  566. this.loadLayerOptions()
  567. } else {
  568. this.$nextTick(() => {
  569. if (this.$refs.scanInput) {
  570. this.$refs.scanInput.focus()
  571. }
  572. })
  573. }
  574. },
  575. showDetailModal() {
  576. this.detailModalVisible = true
  577. },
  578. // 计算当前可选层数并刷新位置网格,保持“按层扫描”体验一致 - rqrq
  579. loadLayerOptions() {
  580. getPalletDetails({
  581. site: this.site,
  582. palletId: this.palletCode,
  583. position: '',
  584. layer: null
  585. }).then(({ data }) => {
  586. if (data && data.code === 0) {
  587. const details = data.details || []
  588. let maxExistingLayer = 0
  589. if (details.length > 0) {
  590. maxExistingLayer = Math.max(...details.map((d) => d.layer || 0))
  591. }
  592. if (this.currentMaxLayer === 0) {
  593. this.currentMixedMode = false
  594. this.scanLayerOptions = Array.from({ length: maxExistingLayer + 1 }, (_, i) => i + 1)
  595. } else {
  596. this.currentMixedMode = false
  597. this.scanLayerOptions = Array.from({ length: this.currentMaxLayer }, (_, i) => i + 1)
  598. }
  599. this.loadAvailablePositions()
  600. } else {
  601. this.$alert((data && data.msg) || '获取栈板信息失败', '错误', { confirmButtonText: '确定' })
  602. }
  603. }).catch(() => {
  604. this.$alert('获取栈板信息失败', '错误', { confirmButtonText: '确定' })
  605. })
  606. },
  607. // 按层加载位置网格与可扫位置,确保禁用态/加载态与原页面一致 - rqrq
  608. loadAvailablePositions() {
  609. this.positionGridLoading = true
  610. this.availablePositions = []
  611. getPalletDetails({
  612. site: this.site,
  613. palletId: this.palletCode,
  614. position: '',
  615. layer: this.scanLayer
  616. }).then(({ data: detailData }) => {
  617. const positionCountMap = {}
  618. if (detailData && detailData.code === 0) {
  619. const details = detailData.details || []
  620. details.forEach((detail) => {
  621. const pos = detail.position
  622. positionCountMap[pos] = (positionCountMap[pos] || 0) + 1
  623. })
  624. }
  625. return getAvailablePositionsForLayer({
  626. site: this.site,
  627. palletId: this.palletCode,
  628. layer: this.scanLayer
  629. }).then(({ data }) => {
  630. if (data && data.code === 0) {
  631. const result = data.data || {}
  632. const positions = result.positions || []
  633. this.availablePositions = result.availablePositions || []
  634. this.positionGrid = positions.map((position) => ({
  635. position,
  636. count: positionCountMap[position] || 0
  637. }))
  638. } else {
  639. this.availablePositions = []
  640. this.positionGrid = []
  641. this.$alert((data && data.msg) || '获取位置信息失败', '错误', { confirmButtonText: '确定' })
  642. }
  643. })
  644. }).catch(() => {
  645. this.availablePositions = []
  646. this.positionGrid = []
  647. this.$alert('获取位置信息失败', '错误', { confirmButtonText: '确定' })
  648. }).finally(() => {
  649. this.positionGridLoading = false
  650. })
  651. },
  652. handleLayerClick(layer) {
  653. if (this.scanLayer === layer) {
  654. return
  655. }
  656. this.scanLayer = layer
  657. this.currentSelectedPosition = ''
  658. this.loadAvailablePositions()
  659. },
  660. handlePositionClick(position) {
  661. if (this.positionGridLoading) {
  662. this.$message.warning('位置信息加载中,请稍候')
  663. return
  664. }
  665. if (!this.availablePositions.includes(position)) {
  666. this.$message.warning('该位置不可用')
  667. return
  668. }
  669. this.currentSelectedPosition = position
  670. this.scanPosition = position
  671. this.$nextTick(() => {
  672. if (this.$refs.scanInput) {
  673. this.$refs.scanInput.focus()
  674. }
  675. })
  676. },
  677. moveFocusToScanInput() {
  678. this.$nextTick(() => {
  679. if (this.$refs.scanInput) {
  680. this.$refs.scanInput.focus()
  681. }
  682. })
  683. },
  684. closeScanModal() {
  685. this.scanModalVisible = false
  686. if (this.needRefreshOnClose) {
  687. this.refreshTable()
  688. this.needRefreshOnClose = false
  689. }
  690. },
  691. loadSortingListIfNeeded() {
  692. if (this.sortingList.length > 0) {
  693. return Promise.resolve(this.sortingList)
  694. }
  695. return getSortingList({
  696. site: this.site,
  697. palletId: this.palletCode
  698. }).then(({ data }) => {
  699. if (data && data.code === 0) {
  700. this.sortingList = data.rfidList || []
  701. return this.sortingList
  702. }
  703. throw new Error((data && data.msg) || '获取分拣清单失败')
  704. })
  705. },
  706. // 分拣扫码:扫进需命中分拣清单并缓存原位置,扫回需按缓存恢复 - rqrq
  707. handleLabelScan() {
  708. if (!this.ensureSite()) {
  709. return
  710. }
  711. if (this.interfaceBusy) {
  712. return
  713. }
  714. if (!this.scanCode || !this.scanCode.trim()) {
  715. this.scanCode = ''
  716. this.$alert('请输入标签编码', '错误', { confirmButtonText: '确定' })
  717. this.moveFocusToScanInput()
  718. return
  719. }
  720. if (this.operationType === 'in') {
  721. if (!this.scanPosition) {
  722. this.scanCode = ''
  723. this.$alert('请选择位置', '错误', { confirmButtonText: '确定' })
  724. this.moveFocusToScanInput()
  725. return
  726. }
  727. if (!this.scanLayer) {
  728. this.scanCode = ''
  729. this.$alert('请选择层数', '错误', { confirmButtonText: '确定' })
  730. this.moveFocusToScanInput()
  731. return
  732. }
  733. this.scanRequestLoading = true
  734. this.loadSortingListIfNeeded()
  735. .then((list) => {
  736. if (list.length === 0) {
  737. throw new Error('当前不存在分拣任务')
  738. }
  739. if (!list.includes(this.scanCode)) {
  740. throw new Error('该标签不在分拣清单中')
  741. }
  742. return getLabelInfo({
  743. site: this.site,
  744. serialNo: this.scanCode
  745. }).then(({ data }) => {
  746. const original = data && data.code === 0 ? data.row : null
  747. return savePalletDetail({
  748. site: this.site,
  749. palletId: this.palletCode,
  750. position: this.scanPosition,
  751. layer: this.scanLayer,
  752. serialNo: this.scanCode,
  753. sortFlag: 1
  754. }).then(({ data: saveData }) => {
  755. if (saveData && saveData.code === 0) {
  756. if (original) {
  757. this.originalDetailCache[this.scanCode] = {
  758. palletId: original.palletId || '',
  759. position: original.position || '',
  760. layer: original.layer || 1,
  761. partNo: original.partNo || '',
  762. createDate: original.createDate,
  763. createBy: original.createBy || '',
  764. // 保留原始wcsFlag语义:当原值为0时不能被“|| 1”吞掉,否则扫回会错误变成增量 - rqrq
  765. wcsFlag: original.wcsFlag === null || original.wcsFlag === undefined ? 0 : original.wcsFlag
  766. }
  767. }
  768. this.$message.success('分拣扫进成功')
  769. this.needRefreshOnClose = true
  770. this.scanCode = ''
  771. this.loadLayerOptions()
  772. this.moveFocusToScanInput()
  773. return
  774. }
  775. throw new Error((saveData && saveData.msg) || '分拣扫进失败')
  776. })
  777. })
  778. })
  779. .catch((error) => {
  780. this.$alert(error.message || '分拣扫进失败', '错误', { confirmButtonText: '确定' })
  781. this.scanCode = ''
  782. this.moveFocusToScanInput()
  783. })
  784. .finally(() => {
  785. this.scanRequestLoading = false
  786. })
  787. } else {
  788. const original = this.originalDetailCache[this.scanCode]
  789. if (!original) {
  790. this.$alert('未找到该标签原始位置缓存,无法扫回', '提示', { confirmButtonText: '确定' })
  791. this.scanCode = ''
  792. this.moveFocusToScanInput()
  793. return
  794. }
  795. this.scanRequestLoading = true
  796. restorePalletDetail({
  797. site: this.site,
  798. serialNo: this.scanCode,
  799. originalPalletId: original.palletId,
  800. originalPosition: original.position,
  801. originalLayer: original.layer,
  802. partNo: original.partNo,
  803. createDate: original.createDate,
  804. createBy: original.createBy,
  805. // 分拣扫回固定回写0:语义是“恢复到已推送基线”,不把扫回数据再次当增量 - rqrq
  806. wcsFlag: 0
  807. }).then(({ data }) => {
  808. if (data && data.code === 0) {
  809. delete this.originalDetailCache[this.scanCode]
  810. this.$message.success('分拣扫回成功')
  811. this.needRefreshOnClose = true
  812. this.scanCode = ''
  813. this.moveFocusToScanInput()
  814. } else {
  815. this.scanCode = ''
  816. this.$alert((data && data.msg) || '分拣扫回失败', '错误', { confirmButtonText: '确定' })
  817. this.moveFocusToScanInput()
  818. }
  819. }).catch((error) => {
  820. this.scanCode = ''
  821. this.$alert(error.message || '分拣扫回失败', '错误', { confirmButtonText: '确定' })
  822. this.moveFocusToScanInput()
  823. }).finally(() => {
  824. this.scanRequestLoading = false
  825. })
  826. }
  827. },
  828. submitSorting() {
  829. if (!this.ensureSite()) {
  830. return
  831. }
  832. if (this.interfaceBusy) {
  833. return
  834. }
  835. if (!this.palletScanned) {
  836. this.$alert('请先扫描栈板', '提示', { confirmButtonText: '确定' })
  837. return
  838. }
  839. this.submitSortingLoading = true
  840. completePalletAssemblyForFenJian({
  841. site: this.site,
  842. palletId: this.palletCode
  843. }).then(({ data }) => {
  844. if (data && data.code === 0) {
  845. this.sortingList = []
  846. this.$message.success('分拣提交成功')
  847. } else {
  848. this.$alert((data && data.msg) || '分拣提交失败', '错误', { confirmButtonText: '确定' })
  849. }
  850. }).catch((error) => {
  851. this.$alert(error.message || '分拣提交失败', '错误', { confirmButtonText: '确定' })
  852. }).finally(() => {
  853. this.submitSortingLoading = false
  854. })
  855. },
  856. // 结束分拣动作仅走海安后端分流,不创建WMS运输任务 - rqrq
  857. submitFinishSorting() {
  858. if (!this.ensureSite()) {
  859. return
  860. }
  861. if (this.interfaceBusy) {
  862. return
  863. }
  864. if (!this.palletScanned) {
  865. this.$alert('请先扫描栈板', '提示', { confirmButtonText: '确定' })
  866. return
  867. }
  868. this.finishSortingLoading = true
  869. finishSorting({
  870. site: this.site,
  871. palletId: this.palletCode
  872. }).then(({ data }) => {
  873. if (data && data.code === 0) {
  874. this.$message.success('结束分拣成功')
  875. this.resetPage()
  876. } else {
  877. this.$alert((data && data.msg) || '结束分拣失败', '错误', { confirmButtonText: '确定' })
  878. }
  879. }).catch((error) => {
  880. this.$alert(error.message || '结束分拣失败', '错误', { confirmButtonText: '确定' })
  881. }).finally(() => {
  882. this.finishSortingLoading = false
  883. })
  884. }
  885. },
  886. // keep-alive页面重新激活时统一重置,确保每次进入都是干净态 - rqrq
  887. activated() {
  888. this.resetPage(true)
  889. },
  890. // 离开页面时主动清空,避免返回菜单后二次进入看到上次分拣数据 - rqrq
  891. beforeRouteLeave(to, from, next) {
  892. this.resetPage(false)
  893. next()
  894. },
  895. mounted() {
  896. this.resetPage(true)
  897. }
  898. }
  899. </script>
  900. <style scoped>
  901. .header-action-disabled {
  902. opacity: 0.5;
  903. pointer-events: none;
  904. }
  905. .detail-table {
  906. background: white;
  907. border-radius: 6px;
  908. overflow: hidden;
  909. border: 1px solid #e0e0e0;
  910. }
  911. .table-header,
  912. .table-row {
  913. display: flex;
  914. align-items: center;
  915. padding: 8px;
  916. border-bottom: 1px solid #e0e0e0;
  917. }
  918. .table-header {
  919. background: #f5f5f5;
  920. font-weight: bold;
  921. font-size: 14px;
  922. }
  923. .col-position {
  924. flex: 1;
  925. text-align: center;
  926. }
  927. .col-layer {
  928. flex: 1;
  929. text-align: center;
  930. }
  931. .col-serial {
  932. flex: 4;
  933. text-align: center;
  934. word-break: break-all;
  935. }
  936. .empty-row {
  937. justify-content: center;
  938. align-items: center;
  939. padding: 20px;
  940. border-bottom: none;
  941. }
  942. .empty-hint {
  943. text-align: center;
  944. color: #999;
  945. width: 100%;
  946. }
  947. .scan-modal-content {
  948. padding: 10px 0;
  949. }
  950. .modal-form {
  951. margin-bottom: 16px;
  952. }
  953. .dialog-footer {
  954. text-align: center;
  955. }
  956. .action-btn.warning {
  957. background-color: #E6A23C;
  958. border-color: #E6A23C;
  959. color: white;
  960. }
  961. .action-btn:disabled {
  962. opacity: 0.6;
  963. cursor: not-allowed;
  964. background-color: #ccc !important;
  965. border-color: #ccc !important;
  966. }
  967. ::v-deep .el-dialog__wrapper {
  968. z-index: 2000 !important;
  969. }
  970. ::v-deep .el-overlay {
  971. z-index: 2000 !important;
  972. }
  973. ::v-deep .el-radio {
  974. margin-right: 8px !important;
  975. }
  976. ::v-deep .el-radio__label {
  977. font-size: 14px;
  978. }
  979. /* 层数网格样式,保持与原PDA页面一致 - rqrq */
  980. .layer-grid {
  981. display: flex;
  982. flex-wrap: wrap;
  983. gap: 8px;
  984. margin-top: 8px;
  985. }
  986. .layer-item {
  987. display: flex;
  988. align-items: center;
  989. justify-content: center;
  990. min-width: 50px;
  991. height: 45px;
  992. padding: 0 12px;
  993. background-color: #fff;
  994. border: 2px solid #dcdfe6;
  995. border-radius: 6px;
  996. font-size: 15px;
  997. font-weight: bold;
  998. cursor: pointer;
  999. transition: all 0.3s;
  1000. user-select: none;
  1001. }
  1002. .layer-item:hover {
  1003. border-color: #409eff;
  1004. background-color: #ecf5ff;
  1005. }
  1006. .layer-item.layer-selected {
  1007. border-color: #409eff;
  1008. background-color: #409eff;
  1009. color: #fff;
  1010. }
  1011. /* 位置网格样式,保持与原PDA页面一致 - rqrq */
  1012. .position-grid {
  1013. display: grid;
  1014. gap: 10px;
  1015. margin-top: 8px;
  1016. position: relative;
  1017. }
  1018. .position-grid.position-grid-1 {
  1019. grid-template-columns: 1fr;
  1020. grid-template-rows: 1fr;
  1021. }
  1022. .position-grid.position-grid-4 {
  1023. grid-template-columns: repeat(2, 1fr);
  1024. grid-template-rows: repeat(2, 1fr);
  1025. grid-auto-flow: column;
  1026. }
  1027. .position-grid.position-grid-9 {
  1028. grid-template-columns: repeat(3, 1fr);
  1029. grid-template-rows: repeat(3, 1fr);
  1030. grid-auto-flow: row;
  1031. }
  1032. .position-item.position-placeholder {
  1033. background-color: #f0f0f0 !important;
  1034. border: 1px dashed #d0d0d0 !important;
  1035. color: transparent !important;
  1036. cursor: not-allowed !important;
  1037. pointer-events: none !important;
  1038. }
  1039. .position-grid.position-grid-loading {
  1040. opacity: 0.7;
  1041. pointer-events: none;
  1042. }
  1043. .position-item {
  1044. display: flex;
  1045. align-items: center;
  1046. justify-content: center;
  1047. height: 60px;
  1048. background-color: #fff;
  1049. border: 2px solid #dcdfe6;
  1050. border-radius: 6px;
  1051. font-size: 16px;
  1052. font-weight: bold;
  1053. cursor: pointer;
  1054. transition: all 0.3s;
  1055. user-select: none;
  1056. }
  1057. .position-item:hover:not(.position-disabled) {
  1058. border-color: #409eff;
  1059. background-color: #ecf5ff;
  1060. }
  1061. .position-item.position-selected {
  1062. border-color: #409eff;
  1063. background-color: #409eff;
  1064. color: #fff;
  1065. }
  1066. .position-item.position-disabled {
  1067. background-color: #f5f7fa;
  1068. color: #c0c4cc;
  1069. cursor: not-allowed;
  1070. opacity: 0.6;
  1071. }
  1072. .position-item.position-loading {
  1073. background: linear-gradient(90deg, #f5f7fa 25%, #e4e7ed 50%, #f5f7fa 75%);
  1074. background-size: 200% 100%;
  1075. animation: loading 1.5s ease-in-out infinite;
  1076. }
  1077. @keyframes loading {
  1078. 0% {
  1079. background-position: 200% 0;
  1080. }
  1081. 100% {
  1082. background-position: -200% 0;
  1083. }
  1084. }
  1085. </style>