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.

1340 lines
41 KiB

4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
3 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
3 months ago
3 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
4 months ago
3 months ago
4 months ago
4 months ago
3 months ago
4 months ago
3 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
  1. <template>
  2. <div>
  3. <div class="pda-container">
  4. <!-- 头部栏 -->
  5. <div class="header-bar">
  6. <div class="header-left" @click="handleBack">
  7. <i class="el-icon-arrow-left"></i>
  8. <span>组盘入库</span>
  9. </div>
  10. <div class="header-right" @click="$router.push({ path: '/' })">
  11. 首页
  12. </div>
  13. </div>
  14. <div class="main-content form-section">
  15. <!-- 第一行栈板扫描 -->
  16. <div class="input-group">
  17. <label class="input-label">栈板编码</label>
  18. <div style="display: flex; gap: 8px;">
  19. <el-input
  20. v-model="palletCode"
  21. placeholder="请扫描栈板编码"
  22. class="form-input"
  23. style="flex: 0.75;"
  24. clearable
  25. @keyup.enter.native="handlePalletScan"
  26. ref="palletInput"
  27. />
  28. <button
  29. class="action-btn secondary"
  30. style="flex: 0.25; margin: 0;"
  31. @click="handleCallPallet"
  32. >
  33. Call栈板
  34. </button>
  35. </div>
  36. </div>
  37. <!-- 第二行栈板类型和自动分拣 (扫描栈板后显示) -->
  38. <div v-if="palletScanned" class="input-group">
  39. <div style="display: flex; gap: 8px; align-items: end;">
  40. <div style="flex: 0.65;">
  41. <label class="input-label">栈板类型</label>
  42. <el-select
  43. v-model="currentPalletType"
  44. placeholder="请选择栈板类型"
  45. style="width: 100%;"
  46. :disabled="palletTypeDisabled"
  47. @change="handlePalletTypeChange"
  48. >
  49. <el-option
  50. v-for="type in palletTypeOptions"
  51. :key="type.palletType"
  52. :label="`${type.palletType} - ${type.typeDesc}`"
  53. :value="type.palletType"
  54. />
  55. </el-select>
  56. </div>
  57. <div style="flex: 0.35;">
  58. <label class="input-label">自动分拣</label>
  59. <el-select
  60. v-model="currentAutoSort"
  61. placeholder="请选择"
  62. style="width: 100%;"
  63. :disabled="autoSortDisabled"
  64. @change="handleAutoSortChange"
  65. >
  66. <el-option label="是" value="Y"></el-option>
  67. <el-option label="否" value="N"></el-option>
  68. </el-select>
  69. </div>
  70. </div>
  71. </div>
  72. <!-- 第三行筛选条件 (扫描栈板后显示) -->
  73. <div v-if="palletScanned" class="input-group">
  74. <label class="input-label">位置</label>
  75. <div style="display: flex; gap: 8px;">
  76. <el-select
  77. v-model="selectedPosition"
  78. placeholder="请选择位置"
  79. style="flex: 0.75;"
  80. >
  81. <el-option label="ALL" value=""></el-option>
  82. <el-option
  83. v-for="position in positionOptions"
  84. :key="position"
  85. :label="position"
  86. :value="position"
  87. />
  88. </el-select>
  89. <button
  90. class="action-btn secondary"
  91. style="flex: 0.25; margin: 0; white-space: nowrap;"
  92. @click="refreshTable"
  93. >
  94. 刷新
  95. </button>
  96. </div>
  97. </div>
  98. <!-- 第四行扫进/扫出选择 (扫描栈板后显示) -->
  99. <div v-if="palletScanned" class="input-group">
  100. <div style="display: flex; gap: 8px; align-items: center;">
  101. <div style="flex: 0.75;">
  102. <el-radio-group v-model="operationType" style="display: flex;">
  103. <el-radio label="in" style="margin-right: 0px; white-space: nowrap;">扫进</el-radio>
  104. <el-radio label="out" style="white-space: nowrap;">扫出</el-radio>
  105. </el-radio-group>
  106. </div>
  107. <button
  108. class="action-btn secondary"
  109. style="flex: 0.25; margin: 0; white-space: nowrap;"
  110. @click="showScanModal"
  111. >
  112. 扫描条码
  113. </button>
  114. </div>
  115. </div>
  116. </div>
  117. <!-- 栈板明细表格 (扫描栈板后显示) -->
  118. <div v-if="palletScanned" class="rma-list">
  119. <div class="list-title-row" style="display: flex; gap: 8px; align-items: center; padding: 0;">
  120. <div class="list-title" style="flex: 0.75; margin: 0;">栈板明细</div>
  121. <button class="action-btn secondary" style="flex: 0.25; margin: 0;" @click="handleTransportOrder">运输指令</button>
  122. </div>
  123. <div class="detail-table">
  124. <div class="table-header">
  125. <div class="col-position">位置</div>
  126. <div class="col-layer">层数</div>
  127. <div class="col-serial">标签号</div>
  128. </div>
  129. <div
  130. v-for="(detail, index) in detailList"
  131. :key="index"
  132. class="table-row"
  133. @click="handleRowDblClick(detail, index)"
  134. >
  135. <div class="col-position">{{ detail.position }}</div>
  136. <div class="col-layer">{{ detail.layer }}</div>
  137. <div class="col-serial">{{ detail.serialNo }}</div>
  138. </div>
  139. <!-- 暂无数据提示 -->
  140. <div v-if="detailList.length === 0" class="table-row empty-row">
  141. <div class="empty-hint">暂无栈板明细数据</div>
  142. </div>
  143. </div>
  144. </div>
  145. </div>
  146. <!-- 扫码模态框 -->
  147. <el-dialog
  148. title="扫描标签"
  149. :visible.sync="scanModalVisible"
  150. width="90%"
  151. :close-on-click-modal="false"
  152. :show-close="false"
  153. :modal="true"
  154. :modal-append-to-body="true"
  155. :append-to-body="true"
  156. >
  157. <div class="scan-modal-content">
  158. <!-- 扫进时显示位置和层数选择 -->
  159. <div v-if="operationType === 'in'" class="modal-form">
  160. <div class="input-group">
  161. <label class="input-label">位置</label>
  162. <el-select
  163. v-model="scanPosition"
  164. placeholder="请选择位置"
  165. style="width: 100%;"
  166. @change="handleScanPositionChange"
  167. >
  168. <el-option
  169. v-for="position in positionOptions"
  170. :key="position"
  171. :label="position"
  172. :value="position"
  173. />
  174. </el-select>
  175. </div>
  176. <div class="input-group">
  177. <label class="input-label">层数</label>
  178. <el-select
  179. v-model="scanLayer"
  180. placeholder="请选择层数"
  181. @change="moveFocusToScanInput"
  182. style="width: 100%;"
  183. >
  184. <el-option
  185. v-for="layer in scanLayerOptions"
  186. :key="layer"
  187. :label="`第${layer}层`"
  188. :value="layer"
  189. />
  190. </el-select>
  191. </div>
  192. </div>
  193. <!-- 标签扫描 -->
  194. <div class="input-group">
  195. <label class="input-label">标签二维码</label>
  196. <el-input
  197. v-model="scanCode"
  198. placeholder="请扫描标签二维码"
  199. class="form-input"
  200. clearable
  201. @keyup.enter.native="handleLabelScan"
  202. ref="scanInput"
  203. />
  204. </div>
  205. </div>
  206. <div slot="footer" class="dialog-footer">
  207. <button class="action-btn secondary" style="margin-left: 10px;" @click="closeScanModal">取消</button>
  208. </div>
  209. </el-dialog>
  210. <!-- 修改位置模态框 -->
  211. <el-dialog
  212. title="修改标签位置"
  213. :visible.sync="editPositionModalVisible"
  214. width="90%"
  215. :close-on-click-modal="false"
  216. :show-close="false"
  217. :modal="true"
  218. :modal-append-to-body="true"
  219. :append-to-body="true"
  220. >
  221. <div class="edit-modal-content">
  222. <!-- 标签号只读 -->
  223. <div class="input-group">
  224. <label class="input-label">标签号</label>
  225. <el-input
  226. v-model="editSerialNo"
  227. placeholder="标签号"
  228. class="form-input"
  229. readonly
  230. />
  231. </div>
  232. <!-- 位置选择 -->
  233. <div class="input-group">
  234. <label class="input-label">位置</label>
  235. <el-select
  236. v-model="editPosition"
  237. placeholder="请选择位置"
  238. style="width: 100%;"
  239. @change="handleEditPositionChange"
  240. >
  241. <el-option
  242. v-for="position in positionOptions"
  243. :key="position"
  244. :label="position"
  245. :value="position"
  246. />
  247. </el-select>
  248. </div>
  249. <!-- 层数选择 -->
  250. <div class="input-group">
  251. <label class="input-label">层数</label>
  252. <el-select
  253. v-model="editLayer"
  254. placeholder="请选择层数"
  255. style="width: 100%;"
  256. >
  257. <el-option
  258. v-for="layer in editLayerOptions"
  259. :key="layer"
  260. :label="`第${layer}层`"
  261. :value="layer"
  262. />
  263. </el-select>
  264. </div>
  265. </div>
  266. <div slot="footer" class="dialog-footer">
  267. <button class="action-btn primary" @click="confirmEditPosition" :disabled="editPositionLoading">
  268. {{ editPositionLoading ? '处理中...' : '确定' }}
  269. </button>
  270. <button class="action-btn secondary" style="margin-left: 10px;" @click="closeEditPositionModal" :disabled="editPositionLoading">取消</button>
  271. </div>
  272. </el-dialog>
  273. <!-- 运输任务模态框 -->
  274. <el-dialog
  275. title="创建运输任务"
  276. :visible.sync="transportModalVisible"
  277. width="90%"
  278. :close-on-click-modal="false"
  279. :show-close="false"
  280. :modal="true"
  281. :modal-append-to-body="true"
  282. :append-to-body="true"
  283. >
  284. <div class="transport-modal-content">
  285. <!-- 栈板号只读 -->
  286. <div class="input-group">
  287. <label class="input-label">栈板号</label>
  288. <el-input
  289. v-model="palletCode"
  290. placeholder="栈板号"
  291. class="form-input"
  292. readonly
  293. />
  294. </div>
  295. <!-- 起点站点显示只读从栈板位置自动获取 -->
  296. <div class="input-group">
  297. <label class="input-label">起点站点</label>
  298. <el-input
  299. v-model="currentPalletStation"
  300. placeholder="当前栈板位置"
  301. class="form-input"
  302. readonly
  303. />
  304. </div>
  305. <!-- 目标区域选择 -->
  306. <div class="input-group">
  307. <label class="input-label">目标区域</label>
  308. <el-select
  309. v-model="selectedTargetArea"
  310. placeholder="请选择目标区域"
  311. style="width: 100%;"
  312. >
  313. <el-option
  314. v-for="area in transportAreaOptions"
  315. :key="area.stationArea"
  316. :label="area.stationArea"
  317. :value="area.stationArea"
  318. />
  319. </el-select>
  320. </div>
  321. </div>
  322. <div slot="footer" class="dialog-footer">
  323. <button class="action-btn primary" @click="confirmTransportTask" :disabled="transportTaskLoading">
  324. {{ transportTaskLoading ? '创建中...' : '确定' }}
  325. </button>
  326. <button class="action-btn secondary" style="margin-left: 10px;" @click="closeTransportModal" :disabled="transportTaskLoading">取消</button>
  327. </div>
  328. </el-dialog>
  329. <!-- Call栈板模态框 -->
  330. <el-dialog
  331. title="调用空托盘"
  332. :visible.sync="callPalletModalVisible"
  333. width="90%"
  334. :close-on-click-modal="false"
  335. :show-close="false"
  336. :modal="true"
  337. :modal-append-to-body="true"
  338. :append-to-body="true"
  339. >
  340. <div class="call-modal-content">
  341. <!-- 起始站点选择 -->
  342. <div class="input-group">
  343. <label class="input-label">起始站点</label>
  344. <el-select
  345. v-model="selectedCallStartStation"
  346. placeholder="请选择起始站点"
  347. style="width: 100%;"
  348. @change="handleCallStartStationChange"
  349. >
  350. <el-option
  351. v-for="station in callStartStationOptions"
  352. :key="station.stationCode"
  353. :label="`${station.stationCode} - ${station.stationName}`"
  354. :value="station.stationCode"
  355. />
  356. </el-select>
  357. </div>
  358. <!-- 目标站点输入 -->
  359. <div class="input-group">
  360. <label class="input-label">目标站点</label>
  361. <el-input
  362. ref="callTargetStationInput"
  363. v-model="selectedCallTargetStation"
  364. placeholder="请扫描或输入目标站点"
  365. clearable
  366. style="width: 100%;"
  367. />
  368. </div>
  369. </div>
  370. <div slot="footer" class="dialog-footer">
  371. <button class="action-btn primary" @click="confirmCallPallet" :disabled="callPalletLoading">
  372. {{ callPalletLoading ? '调用中...' : '确定' }}
  373. </button>
  374. <button class="action-btn secondary" style="margin-left: 10px;" @click="closeCallPalletModal" :disabled="callPalletLoading">取消</button>
  375. </div>
  376. </el-dialog>
  377. </div>
  378. </template>
  379. <script>
  380. import {
  381. checkPalletExists,
  382. getPalletPositions,
  383. getPalletDetails,
  384. getLayersByPosition,
  385. validateLabel,
  386. savePalletDetail,
  387. deletePalletDetail,
  388. getLayersForEdit,
  389. updatePalletDetailPosition,
  390. getAgvStations,
  391. getAvailableAgvStations,
  392. callPalletToStation,
  393. callPalletToStationWithUpdateZuPan,
  394. getPalletInfo,
  395. getPalletTypeList,
  396. getPalletTypeAreas,
  397. updatePalletTypeAndAutoSort
  398. } from '../../../api/automatedWarehouse/palletPacking'
  399. export default {
  400. data() {
  401. return {
  402. site: localStorage.getItem('site'),
  403. palletCode: '',
  404. palletScanned: false,
  405. operationType: 'in', // 'in' 或 'out'
  406. // 栈板类型和自动分拣
  407. currentPalletFamily: '', // 当前栈板大分类(固定不可改)
  408. currentPalletType: '', // 当前栈板类型
  409. currentAutoSort: '', // 当前是否自动分拣 Y/N
  410. palletTypeOptions: [], // 托盘类型选项列表
  411. palletTypeDisabled: false, // 栈板类型下拉框是否禁用(有明细数据时禁用)
  412. autoSortDisabled: false, // 自动分拣下拉框是否禁用
  413. currentWcsAutoSort: '', // 当前托盘类型的wcsAutoSort值
  414. currentMaxLayer: 0, // 当前托盘类型的最大层数,0=无限
  415. // 筛选条件
  416. selectedPosition: '',
  417. selectedLayer: '',
  418. positionOptions: [],
  419. layerOptions: [],
  420. // 扫码模态框
  421. scanModalVisible: false,
  422. scanCode: '',
  423. scanPosition: '',
  424. scanLayer: '',
  425. scanLayerOptions: [],
  426. needRefreshOnClose: false, // 标记是否需要在关闭模态框时刷新
  427. // 栈板明细
  428. detailList: [],
  429. // 运输任务模态框
  430. transportModalVisible: false,
  431. transportAreaOptions: [], // 目标区域选项(从空闲站点中提取并去重)
  432. currentPalletStation: '',
  433. selectedTargetArea: '', // 选择的目标区域
  434. // Call栈板模态框
  435. callPalletModalVisible: false,
  436. callStartStationOptions: [], // 起始站点选项(statusDb=1,有货)
  437. callTargetStationOptions: [], // 目标站点选项(statusDb=0,空闲)
  438. selectedCallStartStation: '',
  439. selectedCallTargetStation: '',
  440. // 修改位置模态框
  441. editPositionModalVisible: false,
  442. editSerialNo: '',
  443. editPosition: '',
  444. editLayer: '',
  445. editLayerOptions: [],
  446. editOriginalPosition: '',
  447. editOriginalLayer: '',
  448. // 按钮loading状态(防止重复点击)
  449. editPositionLoading: false, // 修改位置按钮
  450. transportTaskLoading: false, // 创建运输任务按钮
  451. callPalletLoading: false, // Call栈板按钮
  452. };
  453. },
  454. methods: {
  455. handleBack() {
  456. this.$router.back();
  457. },
  458. // 重置页面到初始状态
  459. resetPage() {
  460. // 清空栈板信息
  461. this.palletCode = '';
  462. this.palletScanned = false;
  463. this.operationType = 'in';
  464. // 清空栈板类型和自动分拣
  465. this.currentPalletFamily = '';
  466. this.currentPalletType = '';
  467. this.currentAutoSort = '';
  468. this.palletTypeOptions = [];
  469. this.palletTypeDisabled = false;
  470. this.autoSortDisabled = false;
  471. this.currentWcsAutoSort = '';
  472. this.currentMaxLayer = 0;
  473. // 清空筛选条件
  474. this.selectedPosition = '';
  475. this.selectedLayer = '';
  476. this.positionOptions = [];
  477. this.layerOptions = [];
  478. // 清空栈板明细
  479. this.detailList = [];
  480. // 清空扫码模态框
  481. this.scanModalVisible = false;
  482. this.scanCode = '';
  483. this.scanPosition = '';
  484. this.scanLayer = '';
  485. this.scanLayerOptions = [];
  486. this.needRefreshOnClose = false;
  487. // 清空运输任务模态框
  488. this.transportModalVisible = false;
  489. this.transportAreaOptions = [];
  490. this.currentPalletStation = '';
  491. this.selectedTargetArea = '';
  492. // 清空修改位置模态框
  493. this.editPositionModalVisible = false;
  494. this.editSerialNo = '';
  495. this.editPosition = '';
  496. this.editLayer = '';
  497. this.editLayerOptions = [];
  498. this.editOriginalPosition = '';
  499. this.editOriginalLayer = '';
  500. // 聚焦到栈板输入框
  501. this.$nextTick(() => {
  502. if (this.$refs.palletInput) {
  503. this.$refs.palletInput.focus();
  504. }
  505. });
  506. console.log('页面已重置到初始状态');
  507. },
  508. // 扫描栈板
  509. handlePalletScan() {
  510. if (!this.palletCode.trim()) {
  511. this.$message.error('请输入栈板编码');
  512. return;
  513. }
  514. checkPalletExists({
  515. site: this.site,
  516. palletId: this.palletCode
  517. }).then(({ data }) => {
  518. if (data.code === 0) {
  519. this.palletCode = data.palletId
  520. this.$nextTick(()=> {
  521. this.palletScanned = true;
  522. this.positionOptions = data.positions || [];
  523. // 获取栈板详细信息(包括palletType和autoSort)
  524. this.loadPalletInfo( data.palletId);
  525. this.refreshTable();
  526. })
  527. } else {
  528. this.$message.error(data.msg || '栈板不存在');
  529. }
  530. }).catch(error => {
  531. console.error('验证栈板失败:', error);
  532. this.$message.error('验证栈板失败');
  533. });
  534. },
  535. // 加载栈板信息
  536. loadPalletInfo(palletId) {
  537. getPalletInfo({
  538. site: this.site,
  539. palletId: palletId
  540. }).then(({ data }) => {
  541. if (data.code === 0) {
  542. const palletInfo = data.row || {};
  543. this.currentPalletFamily = palletInfo.palletFamily || '';
  544. this.currentPalletType = palletInfo.palletType || '';
  545. this.currentAutoSort = palletInfo.autoSort || 'N';
  546. // 加载托盘类型列表(根据palletFamily过滤)
  547. this.loadPalletTypeList();
  548. } else {
  549. this.$message.error(data.msg || '获取栈板信息失败');
  550. }
  551. }).catch(error => {
  552. console.error('获取栈板信息失败:', error);
  553. this.$message.error('获取栈板信息失败');
  554. });
  555. },
  556. // 加载托盘类型列表
  557. loadPalletTypeList() {
  558. getPalletTypeList({
  559. site: this.site,
  560. palletFamily: this.currentPalletFamily,
  561. active: 'Y'
  562. }).then(({ data }) => {
  563. if (data.code === 0) {
  564. this.palletTypeOptions = data.rows || [];
  565. // 设置当前托盘类型的wcsAutoSort值和maxLayer
  566. const currentType = this.palletTypeOptions.find(t => t.palletType === this.currentPalletType);
  567. if (currentType) {
  568. this.currentWcsAutoSort = currentType.wcsAutoSort || 'N';
  569. this.currentMaxLayer = currentType.maxLayer || 0;
  570. this.updateAutoSortControl();
  571. // 加载当前托盘类型的区域列表
  572. this.loadPalletTypeAreas();
  573. }
  574. } else {
  575. this.palletTypeOptions = [];
  576. }
  577. }).catch(error => {
  578. console.error('获取托盘类型列表失败:', error);
  579. this.palletTypeOptions = [];
  580. });
  581. },
  582. // 加载托盘类型的区域列表
  583. loadPalletTypeAreas() {
  584. getPalletTypeAreas({
  585. site: this.site,
  586. palletType: this.currentPalletType
  587. }).then(({ data }) => {
  588. if (data.code === 0) {
  589. // 从pallet_type_area获取position列表
  590. const areas = data.rows || [];
  591. this.positionOptions = areas.map(area => area.position);
  592. } else {
  593. this.positionOptions = [];
  594. }
  595. }).catch(error => {
  596. console.error('获取托盘区域列表失败:', error);
  597. this.positionOptions = [];
  598. });
  599. },
  600. // 托盘类型变更事件
  601. handlePalletTypeChange() {
  602. // 查找选中的托盘类型
  603. const selectedType = this.palletTypeOptions.find(t => t.palletType === this.currentPalletType);
  604. if (selectedType) {
  605. this.currentWcsAutoSort = selectedType.wcsAutoSort || 'N';
  606. this.currentMaxLayer = selectedType.maxLayer || 0;
  607. // 默认值:从托盘类型表取
  608. this.currentAutoSort = this.currentWcsAutoSort;
  609. // 更新自动分拣控制
  610. this.updateAutoSortControl();
  611. // 重新查询pallet_type_area,更新位置下拉框
  612. this.loadPalletTypeAreas();
  613. // 保存到数据库
  614. this.savePalletTypeAndAutoSort();
  615. }
  616. },
  617. // 更新自动分拣控制逻辑
  618. updateAutoSortControl() {
  619. if (this.currentWcsAutoSort === 'N') {
  620. // 不支持自动分拣,锁定为N,禁用选择
  621. this.currentAutoSort = 'N';
  622. this.autoSortDisabled = true;
  623. } else {
  624. // 支持自动分拣,可以选择Y或N
  625. this.autoSortDisabled = false;
  626. }
  627. },
  628. // 是否自动分拣变更事件
  629. handleAutoSortChange() {
  630. // 保存到数据库
  631. this.savePalletTypeAndAutoSort();
  632. },
  633. // 保存栈板类型和自动分拣标志
  634. savePalletTypeAndAutoSort() {
  635. updatePalletTypeAndAutoSort({
  636. site: this.site,
  637. palletId: this.palletCode,
  638. palletType: this.currentPalletType,
  639. autoSort: this.currentAutoSort
  640. }).then(({ data }) => {
  641. if (data.code === 0) {
  642. this.$message.success('更新成功');
  643. } else {
  644. this.$message.error(data.msg || '更新失败');
  645. }
  646. }).catch(error => {
  647. console.error('更新失败:', error);
  648. this.$message.error('更新失败');
  649. });
  650. },
  651. // Call栈板 - 调用空托盘
  652. handleCallPallet() {
  653. this.callPalletModalVisible = true;
  654. this.selectedCallStartStation = '';
  655. this.selectedCallTargetStation = '';
  656. // 获取AGV站点列表,分别过滤起始站点和目标站点
  657. // 获取起始站点(有货的正式站点)
  658. getAvailableAgvStations({ statusDb: 1 }).then(({ data }) => {
  659. if (data && data.code === 0) {
  660. this.callStartStationOptions = data.stations || [];
  661. } else {
  662. this.$message.error(data.msg || '获取起始站点列表失败');
  663. }
  664. }).catch(error => {
  665. console.error('获取起始站点列表失败:', error);
  666. this.$message.error('获取起始站点列表失败');
  667. });
  668. // 获取目标站点(空闲的正式站点)
  669. getAvailableAgvStations({ statusDb: 0 }).then(({ data }) => {
  670. if (data && data.code === 0) {
  671. this.callTargetStationOptions = data.stations || [];
  672. } else {
  673. this.$message.error(data.msg || '获取目标站点列表失败');
  674. }
  675. }).catch(error => {
  676. console.error('获取目标站点列表失败:', error);
  677. this.$message.error('获取目标站点列表失败');
  678. });
  679. },
  680. // 起始站点选择change事件,自动聚焦到目标站点输入框
  681. handleCallStartStationChange() {
  682. this.$nextTick(() => {
  683. if (this.$refs.callTargetStationInput) {
  684. this.$refs.callTargetStationInput.focus();
  685. }
  686. });
  687. },
  688. // 位置选择变化
  689. handlePositionChange() {
  690. if (this.selectedPosition) {
  691. getLayersByPosition({
  692. site: this.site,
  693. palletId: this.palletCode,
  694. position: this.selectedPosition
  695. }).then(({ data }) => {
  696. if (data.code === 0) {
  697. this.layerOptions = data.layers || [];
  698. }
  699. }).catch(error => {
  700. console.error('获取层数失败:', error);
  701. });
  702. } else {
  703. this.layerOptions = [];
  704. }
  705. this.selectedLayer = '';
  706. },
  707. // 刷新表格
  708. refreshTable() {
  709. getPalletDetails({
  710. site: this.site,
  711. palletId: this.palletCode,
  712. position: this.selectedPosition,
  713. layer: this.selectedLayer
  714. }).then(({ data }) => {
  715. if (data.code === 0) {
  716. this.detailList = data.details || [];
  717. // 如果栈板有明细数据,禁用栈板类型和自动分拣的修改
  718. const hasDetails = this.detailList.length > 0;
  719. this.palletTypeDisabled = hasDetails;
  720. // 如果有明细数据,自动分拣也要禁用;否则根据wcsAutoSort判断
  721. if (hasDetails) {
  722. this.autoSortDisabled = true;
  723. } else {
  724. this.updateAutoSortControl();
  725. }
  726. } else {
  727. this.detailList = [];
  728. this.palletTypeDisabled = false;
  729. this.updateAutoSortControl();
  730. }
  731. }).catch(error => {
  732. console.error('获取栈板明细失败:', error);
  733. this.detailList = [];
  734. this.palletTypeDisabled = false;
  735. this.updateAutoSortControl();
  736. });
  737. },
  738. // 显示扫码模态框
  739. showScanModal() {
  740. this.scanModalVisible = true;
  741. this.scanCode = '';
  742. this.scanPosition = '';
  743. this.scanLayer = '';
  744. this.scanLayerOptions = [];
  745. this.needRefreshOnClose = false; // 重置刷新标记
  746. this.$nextTick(() => {
  747. if (this.$refs.scanInput) {
  748. this.$refs.scanInput.focus();
  749. }
  750. });
  751. },
  752. moveFocusToScanInput(){
  753. this.$nextTick(() => {
  754. if (this.$refs.scanInput) {
  755. this.$refs.scanInput.focus();
  756. }
  757. });
  758. },
  759. // 关闭扫码模态框
  760. closeScanModal() {
  761. this.scanModalVisible = false;
  762. // 如果有操作成功,则在关闭时刷新外面的列表
  763. if (this.needRefreshOnClose) {
  764. this.refreshTable();
  765. this.needRefreshOnClose = false;
  766. }
  767. },
  768. // 扫码模态框中位置变化
  769. handleScanPositionChange() {
  770. if (this.scanPosition) {
  771. // maxLayer=0表示混装托盘,只能选第1层
  772. if (this.currentMaxLayer === 0) {
  773. this.scanLayerOptions = [1];
  774. this.scanLayer = 1; // 自动选中第1层
  775. this.moveFocusToScanInput();
  776. return;
  777. }
  778. // maxLayer>0,根据已有层数和maxLayer计算可选层数
  779. getLayersByPosition({
  780. site: this.site,
  781. palletId: this.palletCode,
  782. position: this.scanPosition
  783. }).then(({ data }) => {
  784. if (data.code === 0) {
  785. const existingMaxLayer = data.layers && data.layers.length > 0
  786. ? Math.max(...data.layers)
  787. : 0;
  788. // 有maxLayer限制,取已有最大层+1和maxLayer的较小值
  789. const layerCount = Math.min(existingMaxLayer + 1, this.currentMaxLayer);
  790. this.scanLayerOptions = Array.from({ length: layerCount }, (_, i) => i + 1);
  791. }
  792. }).catch(error => {
  793. console.error('获取层数失败:', error);
  794. this.scanLayerOptions = [1];
  795. });
  796. } else {
  797. this.scanLayerOptions = [];
  798. }
  799. this.scanLayer = '';
  800. },
  801. // 处理标签扫描
  802. handleLabelScan() {
  803. if (!this.scanCode.trim()) {
  804. this.$message.error('请输入标签编码');
  805. return;
  806. }
  807. if (this.operationType === 'in') {
  808. // 扫进操作
  809. if (!this.scanPosition) {
  810. this.$message.error('请选择位置');
  811. return;
  812. }
  813. if (!this.scanLayer) {
  814. this.$message.error('请选择层数');
  815. return;
  816. }
  817. savePalletDetail({
  818. site: this.site,
  819. palletId: this.palletCode,
  820. position: this.scanPosition,
  821. layer: this.scanLayer,
  822. serialNo: this.scanCode
  823. }).then(({ data }) => {
  824. if (data.code === 0) {
  825. this.$message.success('扫进成功');
  826. this.needRefreshOnClose = true; // 标记需要在关闭时刷新
  827. this.scanCode = ''; // 清空扫描码,准备下次扫描
  828. this.handleScanPositionChange()
  829. this.$refs.scanInput.focus();
  830. } else {
  831. this.$message.error(data.msg || '扫进失败');
  832. }
  833. }).catch(error => {
  834. console.error('扫进失败:', error);
  835. this.$message.error('扫进失败');
  836. });
  837. } else {
  838. // 扫出操作
  839. deletePalletDetail({
  840. site: this.site,
  841. palletId: this.palletCode,
  842. serialNo: this.scanCode
  843. }).then(({ data }) => {
  844. if (data.code === 0) {
  845. this.$message.success('扫出成功');
  846. this.needRefreshOnClose = true; // 标记需要在关闭时刷新
  847. this.scanCode = ''; // 清空扫描码,准备下次扫描
  848. this.$refs.scanInput.focus();
  849. } else {
  850. this.$message.error(data.msg || '扫出失败');
  851. }
  852. }).catch(error => {
  853. console.error('扫出失败:', error);
  854. this.$message.error('扫出失败');
  855. });
  856. }
  857. },
  858. // 双击行事件 - 修改位置
  859. handleRowDblClick(detail, index) {
  860. this.editSerialNo = detail.serialNo;
  861. this.editPosition = detail.position;
  862. this.editLayer = detail.layer;
  863. this.editOriginalPosition = detail.position;
  864. this.editOriginalLayer = detail.layer;
  865. // 获取当前位置的层数选项(排除自己)
  866. this.handleEditPositionChange();
  867. this.editPositionModalVisible = true;
  868. },
  869. // 编辑位置选择变化
  870. handleEditPositionChange() {
  871. if (this.editPosition) {
  872. // maxLayer=0表示混装托盘,只能选第1层
  873. if (this.currentMaxLayer === 0) {
  874. this.editLayerOptions = [1];
  875. this.editLayer = 1; // 自动选中第1层
  876. return;
  877. }
  878. // maxLayer>0,根据已有层数和maxLayer计算可选层数
  879. getLayersForEdit({
  880. site: this.site,
  881. palletId: this.palletCode,
  882. position: this.editPosition,
  883. excludeSerialNo: this.editSerialNo
  884. }).then(({ data }) => {
  885. if (data.code === 0) {
  886. let layerOptions = data.layers || [];
  887. // 根据maxLayer限制层数选项
  888. if (this.currentMaxLayer > 0) {
  889. // 过滤掉超过maxLayer的层数
  890. layerOptions = layerOptions.filter(layer => layer <= this.currentMaxLayer);
  891. }
  892. this.editLayerOptions = layerOptions;
  893. // 如果当前选择的层数不在新的选项中,清空选择
  894. // if (!this.editLayerOptions.includes(this.editLayer)) {
  895. // this.editLayer = '';
  896. // }
  897. }
  898. }).catch(error => {
  899. console.error('获取层数失败:', error);
  900. this.editLayerOptions = [];
  901. });
  902. } else {
  903. this.editLayerOptions = [];
  904. this.editLayer = '';
  905. }
  906. },
  907. // 确定修改位置
  908. confirmEditPosition() {
  909. if (!this.editPosition) {
  910. this.$message.error('请选择位置');
  911. return;
  912. }
  913. if (!this.editLayer) {
  914. this.$message.error('请选择层数');
  915. return;
  916. }
  917. // 检查是否有变化
  918. if (this.editPosition === this.editOriginalPosition && this.editLayer === this.editOriginalLayer) {
  919. this.$message.warning('位置没有变化');
  920. return;
  921. }
  922. // 设置loading状态,防止重复点击
  923. this.editPositionLoading = true;
  924. updatePalletDetailPosition({
  925. site: this.site,
  926. palletId: this.palletCode,
  927. serialNo: this.editSerialNo,
  928. newPosition: this.editPosition,
  929. newLayer: this.editLayer
  930. }).then(({ data }) => {
  931. if (data.code === 0) {
  932. this.$message.success('位置修改成功');
  933. this.closeEditPositionModal();
  934. this.refreshTable();
  935. } else {
  936. this.$message.error(data.msg || '位置修改失败');
  937. }
  938. }).catch(error => {
  939. console.error('位置修改失败:', error);
  940. this.$message.error('位置修改失败');
  941. }).finally(() => {
  942. // 无论成功或失败,都要恢复按钮状态
  943. this.editPositionLoading = false;
  944. });
  945. },
  946. // 关闭修改位置模态框
  947. closeEditPositionModal() {
  948. this.editPositionModalVisible = false;
  949. this.editSerialNo = '';
  950. this.editPosition = '';
  951. this.editLayer = '';
  952. this.editLayerOptions = [];
  953. this.editOriginalPosition = '';
  954. this.editOriginalLayer = '';
  955. },
  956. // 运输指令按钮点击事件
  957. handleTransportOrder() {
  958. if (!this.palletCode) {
  959. this.$message.error('请先扫描栈板');
  960. return;
  961. }
  962. // 首先获取当前栈板信息
  963. checkPalletExists({
  964. site: this.site,
  965. palletId: this.palletCode
  966. }).then(({ data }) => {
  967. if (data.code == 0) {
  968. // 从返回数据中获取栈板位置信息
  969. this.currentPalletStation = data.locationCode || '';
  970. if (!this.currentPalletStation) {
  971. this.$message.error('无法获取当前栈板位置');
  972. return;
  973. }
  974. this.palletCode=data.palletId
  975. this.transportModalVisible = true;
  976. this.selectedTargetArea = '';
  977. this.transportAreaOptions = [];
  978. // 获取空闲的正式站点
  979. getAvailableAgvStations({ statusDb: 0 }).then(({ data }) => {
  980. if (data.code === 0) {
  981. // 后台已过滤,直接使用
  982. const freeStations = data.stations || [];
  983. console.log('空闲且为正式站点的数量:', freeStations.length);
  984. // 提取station_area并去重(只提取有stationArea的站点)
  985. const areaMap = new Map();
  986. freeStations.forEach(station => {
  987. if (station.stationArea && station.stationArea.trim() !== '') {
  988. if (!areaMap.has(station.stationArea)) {
  989. areaMap.set(station.stationArea, {
  990. stationArea: station.stationArea,
  991. areaType: station.areaType
  992. });
  993. }
  994. }
  995. });
  996. this.transportAreaOptions = Array.from(areaMap.values());
  997. console.log('可用目标区域数量:', this.transportAreaOptions.length);
  998. // 如果没有可用区域,给出友好提示
  999. if (this.transportAreaOptions.length === 0) {
  1000. this.$message.warning('当前没有可用的目标区域,所有站点可能都已被占用');
  1001. }
  1002. } else {
  1003. this.$message.error(data.msg || '获取站点列表失败');
  1004. }
  1005. }).catch(error => {
  1006. console.error('获取站点列表失败:', error);
  1007. this.$message.error('获取站点列表失败');
  1008. });
  1009. } else {
  1010. this.$message.error(data.msg || '获取栈板信息失败');
  1011. }
  1012. }).catch(error => {
  1013. console.error('获取栈板信息失败:', error);
  1014. this.$message.error('获取栈板信息失败');
  1015. });
  1016. },
  1017. // 确认创建运输任务
  1018. confirmTransportTask() {
  1019. if (!this.currentPalletStation) {
  1020. this.$message.error('无法获取当前栈板位置');
  1021. return;
  1022. }
  1023. if (!this.selectedTargetArea) {
  1024. this.$message.error('请选择目标区域');
  1025. return;
  1026. }
  1027. // 设置loading状态,防止重复点击
  1028. this.transportTaskLoading = true;
  1029. // 调用包含组盘处理的接口(后端会根据区域自动查找空闲站点)
  1030. callPalletToStationWithUpdateZuPan({
  1031. site: this.site,
  1032. startStation: this.currentPalletStation,
  1033. targetArea: this.selectedTargetArea
  1034. }).then(({ data }) => {
  1035. if (data.code === 0) {
  1036. this.$message.success('栈板运输任务创建成功');
  1037. this.closeTransportModal();
  1038. // 清空页面数据,初始化页面
  1039. this.resetPage();
  1040. } else {
  1041. this.$message.error(data.msg || '创建运输任务失败');
  1042. }
  1043. }).catch(error => {
  1044. console.error('创建运输任务失败:', error);
  1045. this.$message.error('创建运输任务失败');
  1046. }).finally(() => {
  1047. // 无论成功或失败,都要恢复按钮状态
  1048. this.transportTaskLoading = false;
  1049. });
  1050. },
  1051. // 关闭运输任务模态框
  1052. closeTransportModal() {
  1053. this.transportModalVisible = false;
  1054. this.currentPalletStation = '';
  1055. this.selectedTargetArea = '';
  1056. this.transportAreaOptions = [];
  1057. },
  1058. // 确认Call栈板
  1059. confirmCallPallet() {
  1060. if (!this.selectedCallStartStation) {
  1061. this.$message.error('请选择起始站点');
  1062. return;
  1063. }
  1064. if (!this.selectedCallTargetStation) {
  1065. this.$message.error('请选择目标站点');
  1066. return;
  1067. }
  1068. // 前端验证:两个站点不能一样
  1069. if (this.selectedCallStartStation === this.selectedCallTargetStation) {
  1070. this.$message.error('起始站点和目标站点不能相同');
  1071. return;
  1072. }
  1073. // 设置loading状态,防止重复点击
  1074. this.callPalletLoading = true;
  1075. callPalletToStation({
  1076. site: this.site,
  1077. startStation: this.selectedCallStartStation,
  1078. targetStation: this.selectedCallTargetStation
  1079. }).then(({ data }) => {
  1080. if (data.code === 0) {
  1081. this.$message.success('空托盘调用任务创建成功');
  1082. this.closeCallPalletModal();
  1083. } else {
  1084. this.$message.error(data.msg || '调用空托盘失败');
  1085. }
  1086. }).catch(error => {
  1087. console.error('调用空托盘失败:', error);
  1088. this.$message.error('异常:'+error);
  1089. }).finally(() => {
  1090. // 无论成功或失败,都要恢复按钮状态
  1091. this.callPalletLoading = false;
  1092. });
  1093. },
  1094. // 关闭Call栈板模态框
  1095. closeCallPalletModal() {
  1096. this.callPalletModalVisible = false;
  1097. this.selectedCallStartStation = '';
  1098. this.selectedCallTargetStation = '';
  1099. this.callStartStationOptions = [];
  1100. this.callTargetStationOptions = [];
  1101. },
  1102. },
  1103. mounted() {
  1104. this.$nextTick(() => {
  1105. if (this.$refs.palletInput) {
  1106. this.$refs.palletInput.focus();
  1107. }
  1108. });
  1109. }
  1110. };
  1111. </script>
  1112. <style scoped>
  1113. /* 表格样式 */
  1114. .detail-table {
  1115. background: white;
  1116. border-radius: 6px;
  1117. overflow: hidden;
  1118. border: 1px solid #e0e0e0;
  1119. }
  1120. .table-header,
  1121. .table-row {
  1122. display: flex;
  1123. align-items: center;
  1124. padding: 8px;
  1125. border-bottom: 1px solid #e0e0e0;
  1126. }
  1127. .table-header {
  1128. background: #f5f5f5;
  1129. font-weight: bold;
  1130. font-size: 14px;
  1131. }
  1132. .table-row {
  1133. font-size: 13px;
  1134. }
  1135. .table-row:last-child {
  1136. border-bottom: none;
  1137. }
  1138. .col-position {
  1139. flex: 1;
  1140. text-align: center;
  1141. }
  1142. .col-layer {
  1143. flex: 1;
  1144. text-align: center;
  1145. }
  1146. .col-serial {
  1147. flex: 4;
  1148. text-align: center;
  1149. word-break: break-all;
  1150. }
  1151. /* 空数据提示 */
  1152. .empty-hint {
  1153. text-align: center;
  1154. color: #999;
  1155. padding: 20px;
  1156. background: white;
  1157. border-radius: 6px;
  1158. margin-top: 16px;
  1159. }
  1160. /* 模态框样式 */
  1161. .scan-modal-content {
  1162. padding: 10px 0;
  1163. }
  1164. .modal-form {
  1165. margin-bottom: 16px;
  1166. }
  1167. .dialog-footer {
  1168. text-align: center;
  1169. }
  1170. /* 修复模态框层级问题 */
  1171. ::v-deep .el-dialog__wrapper {
  1172. z-index: 2000 !important;
  1173. }
  1174. ::v-deep .el-overlay {
  1175. z-index: 2000 !important;
  1176. }
  1177. /* 修复单选框样式 */
  1178. ::v-deep .el-radio {
  1179. margin-right: 8px !important;
  1180. }
  1181. ::v-deep .el-radio__label {
  1182. font-size: 14px;
  1183. }
  1184. /* 标题行样式 */
  1185. .list-title-row {
  1186. display: flex;
  1187. justify-content: space-between;
  1188. align-items: center;
  1189. margin-bottom: 8px;
  1190. }
  1191. .list-title-row .list-title {
  1192. margin: 0;
  1193. flex: 1;
  1194. }
  1195. /* 空数据行样式 */
  1196. .empty-row {
  1197. justify-content: center;
  1198. align-items: center;
  1199. padding: 20px;
  1200. border-bottom: none;
  1201. }
  1202. .empty-row .empty-hint {
  1203. text-align: center;
  1204. color: #999;
  1205. width: 100%;
  1206. }
  1207. /* 按钮禁用状态样式 */
  1208. .action-btn:disabled {
  1209. opacity: 0.6;
  1210. cursor: not-allowed;
  1211. background-color: #ccc !important;
  1212. border-color: #ccc !important;
  1213. }
  1214. </style>