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.

1333 lines
41 KiB

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
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
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
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
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
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
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
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
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
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
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
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
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
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
  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. callPalletToStation,
  392. callPalletToStationWithUpdateZuPan,
  393. getPalletInfo,
  394. getPalletTypeList,
  395. getPalletTypeAreas,
  396. updatePalletTypeAndAutoSort
  397. } from '../../../api/automatedWarehouse/palletPacking'
  398. export default {
  399. data() {
  400. return {
  401. site: localStorage.getItem('site'),
  402. palletCode: '',
  403. palletScanned: false,
  404. operationType: 'in', // 'in' 或 'out'
  405. // 栈板类型和自动分拣
  406. currentPalletFamily: '', // 当前栈板大分类(固定不可改)
  407. currentPalletType: '', // 当前栈板类型
  408. currentAutoSort: '', // 当前是否自动分拣 Y/N
  409. palletTypeOptions: [], // 托盘类型选项列表
  410. palletTypeDisabled: false, // 栈板类型下拉框是否禁用(有明细数据时禁用)
  411. autoSortDisabled: false, // 自动分拣下拉框是否禁用
  412. currentWcsAutoSort: '', // 当前托盘类型的wcsAutoSort值
  413. currentMaxLayer: 0, // 当前托盘类型的最大层数,0=无限
  414. // 筛选条件
  415. selectedPosition: '',
  416. selectedLayer: '',
  417. positionOptions: [],
  418. layerOptions: [],
  419. // 扫码模态框
  420. scanModalVisible: false,
  421. scanCode: '',
  422. scanPosition: '',
  423. scanLayer: '',
  424. scanLayerOptions: [],
  425. needRefreshOnClose: false, // 标记是否需要在关闭模态框时刷新
  426. // 栈板明细
  427. detailList: [],
  428. // 运输任务模态框
  429. transportModalVisible: false,
  430. transportAreaOptions: [], // 目标区域选项(从空闲站点中提取并去重)
  431. currentPalletStation: '',
  432. selectedTargetArea: '', // 选择的目标区域
  433. // Call栈板模态框
  434. callPalletModalVisible: false,
  435. callStartStationOptions: [], // 起始站点选项(statusDb=1,有货)
  436. callTargetStationOptions: [], // 目标站点选项(statusDb=0,空闲)
  437. selectedCallStartStation: '',
  438. selectedCallTargetStation: '',
  439. // 修改位置模态框
  440. editPositionModalVisible: false,
  441. editSerialNo: '',
  442. editPosition: '',
  443. editLayer: '',
  444. editLayerOptions: [],
  445. editOriginalPosition: '',
  446. editOriginalLayer: '',
  447. // 按钮loading状态(防止重复点击)
  448. editPositionLoading: false, // 修改位置按钮
  449. transportTaskLoading: false, // 创建运输任务按钮
  450. callPalletLoading: false, // Call栈板按钮
  451. };
  452. },
  453. methods: {
  454. handleBack() {
  455. this.$router.back();
  456. },
  457. // 重置页面到初始状态
  458. resetPage() {
  459. // 清空栈板信息
  460. this.palletCode = '';
  461. this.palletScanned = false;
  462. this.operationType = 'in';
  463. // 清空栈板类型和自动分拣
  464. this.currentPalletFamily = '';
  465. this.currentPalletType = '';
  466. this.currentAutoSort = '';
  467. this.palletTypeOptions = [];
  468. this.palletTypeDisabled = false;
  469. this.autoSortDisabled = false;
  470. this.currentWcsAutoSort = '';
  471. this.currentMaxLayer = 0;
  472. // 清空筛选条件
  473. this.selectedPosition = '';
  474. this.selectedLayer = '';
  475. this.positionOptions = [];
  476. this.layerOptions = [];
  477. // 清空栈板明细
  478. this.detailList = [];
  479. // 清空扫码模态框
  480. this.scanModalVisible = false;
  481. this.scanCode = '';
  482. this.scanPosition = '';
  483. this.scanLayer = '';
  484. this.scanLayerOptions = [];
  485. this.needRefreshOnClose = false;
  486. // 清空运输任务模态框
  487. this.transportModalVisible = false;
  488. this.transportAreaOptions = [];
  489. this.currentPalletStation = '';
  490. this.selectedTargetArea = '';
  491. // 清空修改位置模态框
  492. this.editPositionModalVisible = false;
  493. this.editSerialNo = '';
  494. this.editPosition = '';
  495. this.editLayer = '';
  496. this.editLayerOptions = [];
  497. this.editOriginalPosition = '';
  498. this.editOriginalLayer = '';
  499. // 聚焦到栈板输入框
  500. this.$nextTick(() => {
  501. if (this.$refs.palletInput) {
  502. this.$refs.palletInput.focus();
  503. }
  504. });
  505. console.log('页面已重置到初始状态');
  506. },
  507. // 扫描栈板
  508. handlePalletScan() {
  509. if (!this.palletCode.trim()) {
  510. this.$message.error('请输入栈板编码');
  511. return;
  512. }
  513. checkPalletExists({
  514. site: this.site,
  515. palletId: this.palletCode
  516. }).then(({ data }) => {
  517. if (data.code === 0) {
  518. this.palletCode=data.palletId
  519. this.palletScanned = true;
  520. this.positionOptions = data.positions || [];
  521. // 获取栈板详细信息(包括palletType和autoSort)
  522. this.loadPalletInfo();
  523. this.refreshTable();
  524. } else {
  525. this.$message.error(data.msg || '栈板不存在');
  526. }
  527. }).catch(error => {
  528. console.error('验证栈板失败:', error);
  529. this.$message.error('验证栈板失败');
  530. });
  531. },
  532. // 加载栈板信息
  533. loadPalletInfo() {
  534. getPalletInfo({
  535. site: this.site,
  536. palletId: this.palletCode
  537. }).then(({ data }) => {
  538. if (data.code === 0) {
  539. const palletInfo = data.row || {};
  540. this.currentPalletFamily = palletInfo.palletFamily || '';
  541. this.currentPalletType = palletInfo.palletType || '';
  542. this.currentAutoSort = palletInfo.autoSort || 'N';
  543. // 加载托盘类型列表(根据palletFamily过滤)
  544. this.loadPalletTypeList();
  545. } else {
  546. this.$message.error(data.msg || '获取栈板信息失败');
  547. }
  548. }).catch(error => {
  549. console.error('获取栈板信息失败:', error);
  550. this.$message.error('获取栈板信息失败');
  551. });
  552. },
  553. // 加载托盘类型列表
  554. loadPalletTypeList() {
  555. getPalletTypeList({
  556. site: this.site,
  557. palletFamily: this.currentPalletFamily,
  558. active: 'Y'
  559. }).then(({ data }) => {
  560. if (data.code === 0) {
  561. this.palletTypeOptions = data.rows || [];
  562. // 设置当前托盘类型的wcsAutoSort值和maxLayer
  563. const currentType = this.palletTypeOptions.find(t => t.palletType === this.currentPalletType);
  564. if (currentType) {
  565. this.currentWcsAutoSort = currentType.wcsAutoSort || 'N';
  566. this.currentMaxLayer = currentType.maxLayer || 0;
  567. this.updateAutoSortControl();
  568. // 加载当前托盘类型的区域列表
  569. this.loadPalletTypeAreas();
  570. }
  571. } else {
  572. this.palletTypeOptions = [];
  573. }
  574. }).catch(error => {
  575. console.error('获取托盘类型列表失败:', error);
  576. this.palletTypeOptions = [];
  577. });
  578. },
  579. // 加载托盘类型的区域列表
  580. loadPalletTypeAreas() {
  581. getPalletTypeAreas({
  582. site: this.site,
  583. palletType: this.currentPalletType
  584. }).then(({ data }) => {
  585. if (data.code === 0) {
  586. // 从pallet_type_area获取position列表
  587. const areas = data.rows || [];
  588. this.positionOptions = areas.map(area => area.position);
  589. } else {
  590. this.positionOptions = [];
  591. }
  592. }).catch(error => {
  593. console.error('获取托盘区域列表失败:', error);
  594. this.positionOptions = [];
  595. });
  596. },
  597. // 托盘类型变更事件
  598. handlePalletTypeChange() {
  599. // 查找选中的托盘类型
  600. const selectedType = this.palletTypeOptions.find(t => t.palletType === this.currentPalletType);
  601. if (selectedType) {
  602. this.currentWcsAutoSort = selectedType.wcsAutoSort || 'N';
  603. this.currentMaxLayer = selectedType.maxLayer || 0;
  604. // 默认值:从托盘类型表取
  605. this.currentAutoSort = this.currentWcsAutoSort;
  606. // 更新自动分拣控制
  607. this.updateAutoSortControl();
  608. // 重新查询pallet_type_area,更新位置下拉框
  609. this.loadPalletTypeAreas();
  610. // 保存到数据库
  611. this.savePalletTypeAndAutoSort();
  612. }
  613. },
  614. // 更新自动分拣控制逻辑
  615. updateAutoSortControl() {
  616. if (this.currentWcsAutoSort === 'N') {
  617. // 不支持自动分拣,锁定为N,禁用选择
  618. this.currentAutoSort = 'N';
  619. this.autoSortDisabled = true;
  620. } else {
  621. // 支持自动分拣,可以选择Y或N
  622. this.autoSortDisabled = false;
  623. }
  624. },
  625. // 是否自动分拣变更事件
  626. handleAutoSortChange() {
  627. // 保存到数据库
  628. this.savePalletTypeAndAutoSort();
  629. },
  630. // 保存栈板类型和自动分拣标志
  631. savePalletTypeAndAutoSort() {
  632. updatePalletTypeAndAutoSort({
  633. site: this.site,
  634. palletId: this.palletCode,
  635. palletType: this.currentPalletType,
  636. autoSort: this.currentAutoSort
  637. }).then(({ data }) => {
  638. if (data.code === 0) {
  639. this.$message.success('更新成功');
  640. } else {
  641. this.$message.error(data.msg || '更新失败');
  642. }
  643. }).catch(error => {
  644. console.error('更新失败:', error);
  645. this.$message.error('更新失败');
  646. });
  647. },
  648. // Call栈板 - 调用空托盘
  649. handleCallPallet() {
  650. this.callPalletModalVisible = true;
  651. this.selectedCallStartStation = '';
  652. this.selectedCallTargetStation = '';
  653. // 获取AGV站点列表,分别过滤起始站点和目标站点
  654. getAgvStations({}).then(({ data }) => {
  655. if (data.code === 0) {
  656. const allStations = data.stations || [];
  657. // 起始站点:过滤出statusDb为1(有货)且为正式站点的站点
  658. this.callStartStationOptions = allStations.filter(station =>
  659. station.statusDb === 1 && station.stationType === '正式站点'
  660. );
  661. // 目标站点:过滤出statusDb为0(空闲)且为正式站点的站点
  662. this.callTargetStationOptions = allStations.filter(station =>
  663. station.statusDb === 0 && station.stationType === '正式站点'
  664. );
  665. } else {
  666. this.$message.error(data.msg || '获取站点列表失败');
  667. }
  668. }).catch(error => {
  669. console.error('获取站点列表失败:', error);
  670. this.$message.error('获取站点列表失败');
  671. });
  672. },
  673. // 起始站点选择change事件,自动聚焦到目标站点输入框
  674. handleCallStartStationChange() {
  675. this.$nextTick(() => {
  676. if (this.$refs.callTargetStationInput) {
  677. this.$refs.callTargetStationInput.focus();
  678. }
  679. });
  680. },
  681. // 位置选择变化
  682. handlePositionChange() {
  683. if (this.selectedPosition) {
  684. getLayersByPosition({
  685. site: this.site,
  686. palletId: this.palletCode,
  687. position: this.selectedPosition
  688. }).then(({ data }) => {
  689. if (data.code === 0) {
  690. this.layerOptions = data.layers || [];
  691. }
  692. }).catch(error => {
  693. console.error('获取层数失败:', error);
  694. });
  695. } else {
  696. this.layerOptions = [];
  697. }
  698. this.selectedLayer = '';
  699. },
  700. // 刷新表格
  701. refreshTable() {
  702. getPalletDetails({
  703. site: this.site,
  704. palletId: this.palletCode,
  705. position: this.selectedPosition,
  706. layer: this.selectedLayer
  707. }).then(({ data }) => {
  708. if (data.code === 0) {
  709. this.detailList = data.details || [];
  710. // 如果栈板有明细数据,禁用栈板类型和自动分拣的修改
  711. const hasDetails = this.detailList.length > 0;
  712. this.palletTypeDisabled = hasDetails;
  713. // 如果有明细数据,自动分拣也要禁用;否则根据wcsAutoSort判断
  714. if (hasDetails) {
  715. this.autoSortDisabled = true;
  716. } else {
  717. this.updateAutoSortControl();
  718. }
  719. } else {
  720. this.detailList = [];
  721. this.palletTypeDisabled = false;
  722. this.updateAutoSortControl();
  723. }
  724. }).catch(error => {
  725. console.error('获取栈板明细失败:', error);
  726. this.detailList = [];
  727. this.palletTypeDisabled = false;
  728. this.updateAutoSortControl();
  729. });
  730. },
  731. // 显示扫码模态框
  732. showScanModal() {
  733. this.scanModalVisible = true;
  734. this.scanCode = '';
  735. this.scanPosition = '';
  736. this.scanLayer = '';
  737. this.scanLayerOptions = [];
  738. this.needRefreshOnClose = false; // 重置刷新标记
  739. this.$nextTick(() => {
  740. if (this.$refs.scanInput) {
  741. this.$refs.scanInput.focus();
  742. }
  743. });
  744. },
  745. moveFocusToScanInput(){
  746. this.$nextTick(() => {
  747. if (this.$refs.scanInput) {
  748. this.$refs.scanInput.focus();
  749. }
  750. });
  751. },
  752. // 关闭扫码模态框
  753. closeScanModal() {
  754. this.scanModalVisible = false;
  755. // 如果有操作成功,则在关闭时刷新外面的列表
  756. if (this.needRefreshOnClose) {
  757. this.refreshTable();
  758. this.needRefreshOnClose = false;
  759. }
  760. },
  761. // 扫码模态框中位置变化
  762. handleScanPositionChange() {
  763. if (this.scanPosition) {
  764. // maxLayer=0表示混装托盘,只能选第1层
  765. if (this.currentMaxLayer === 0) {
  766. this.scanLayerOptions = [1];
  767. this.scanLayer = 1; // 自动选中第1层
  768. this.moveFocusToScanInput();
  769. return;
  770. }
  771. // maxLayer>0,根据已有层数和maxLayer计算可选层数
  772. getLayersByPosition({
  773. site: this.site,
  774. palletId: this.palletCode,
  775. position: this.scanPosition
  776. }).then(({ data }) => {
  777. if (data.code === 0) {
  778. const existingMaxLayer = data.layers && data.layers.length > 0
  779. ? Math.max(...data.layers)
  780. : 0;
  781. // 有maxLayer限制,取已有最大层+1和maxLayer的较小值
  782. const layerCount = Math.min(existingMaxLayer + 1, this.currentMaxLayer);
  783. this.scanLayerOptions = Array.from({ length: layerCount }, (_, i) => i + 1);
  784. }
  785. }).catch(error => {
  786. console.error('获取层数失败:', error);
  787. this.scanLayerOptions = [1];
  788. });
  789. } else {
  790. this.scanLayerOptions = [];
  791. }
  792. this.scanLayer = '';
  793. },
  794. // 处理标签扫描
  795. handleLabelScan() {
  796. if (!this.scanCode.trim()) {
  797. this.$message.error('请输入标签编码');
  798. return;
  799. }
  800. if (this.operationType === 'in') {
  801. // 扫进操作
  802. if (!this.scanPosition) {
  803. this.$message.error('请选择位置');
  804. return;
  805. }
  806. if (!this.scanLayer) {
  807. this.$message.error('请选择层数');
  808. return;
  809. }
  810. savePalletDetail({
  811. site: this.site,
  812. palletId: this.palletCode,
  813. position: this.scanPosition,
  814. layer: this.scanLayer,
  815. serialNo: this.scanCode
  816. }).then(({ data }) => {
  817. if (data.code === 0) {
  818. this.$message.success('扫进成功');
  819. this.needRefreshOnClose = true; // 标记需要在关闭时刷新
  820. this.scanCode = ''; // 清空扫描码,准备下次扫描
  821. this.$refs.scanInput.focus();
  822. } else {
  823. this.$message.error(data.msg || '扫进失败');
  824. }
  825. }).catch(error => {
  826. console.error('扫进失败:', error);
  827. this.$message.error('扫进失败');
  828. });
  829. } else {
  830. // 扫出操作
  831. deletePalletDetail({
  832. site: this.site,
  833. palletId: this.palletCode,
  834. serialNo: this.scanCode
  835. }).then(({ data }) => {
  836. if (data.code === 0) {
  837. this.$message.success('扫出成功');
  838. this.needRefreshOnClose = true; // 标记需要在关闭时刷新
  839. this.scanCode = ''; // 清空扫描码,准备下次扫描
  840. this.$refs.scanInput.focus();
  841. } else {
  842. this.$message.error(data.msg || '扫出失败');
  843. }
  844. }).catch(error => {
  845. console.error('扫出失败:', error);
  846. this.$message.error('扫出失败');
  847. });
  848. }
  849. },
  850. // 双击行事件 - 修改位置
  851. handleRowDblClick(detail, index) {
  852. this.editSerialNo = detail.serialNo;
  853. this.editPosition = detail.position;
  854. this.editLayer = detail.layer;
  855. this.editOriginalPosition = detail.position;
  856. this.editOriginalLayer = detail.layer;
  857. // 获取当前位置的层数选项(排除自己)
  858. this.handleEditPositionChange();
  859. this.editPositionModalVisible = true;
  860. },
  861. // 编辑位置选择变化
  862. handleEditPositionChange() {
  863. if (this.editPosition) {
  864. // maxLayer=0表示混装托盘,只能选第1层
  865. if (this.currentMaxLayer === 0) {
  866. this.editLayerOptions = [1];
  867. this.editLayer = 1; // 自动选中第1层
  868. return;
  869. }
  870. // maxLayer>0,根据已有层数和maxLayer计算可选层数
  871. getLayersForEdit({
  872. site: this.site,
  873. palletId: this.palletCode,
  874. position: this.editPosition,
  875. excludeSerialNo: this.editSerialNo
  876. }).then(({ data }) => {
  877. if (data.code === 0) {
  878. let layerOptions = data.layers || [];
  879. // 根据maxLayer限制层数选项
  880. if (this.currentMaxLayer > 0) {
  881. // 过滤掉超过maxLayer的层数
  882. layerOptions = layerOptions.filter(layer => layer <= this.currentMaxLayer);
  883. }
  884. this.editLayerOptions = layerOptions;
  885. // 如果当前选择的层数不在新的选项中,清空选择
  886. // if (!this.editLayerOptions.includes(this.editLayer)) {
  887. // this.editLayer = '';
  888. // }
  889. }
  890. }).catch(error => {
  891. console.error('获取层数失败:', error);
  892. this.editLayerOptions = [];
  893. });
  894. } else {
  895. this.editLayerOptions = [];
  896. this.editLayer = '';
  897. }
  898. },
  899. // 确定修改位置
  900. confirmEditPosition() {
  901. if (!this.editPosition) {
  902. this.$message.error('请选择位置');
  903. return;
  904. }
  905. if (!this.editLayer) {
  906. this.$message.error('请选择层数');
  907. return;
  908. }
  909. // 检查是否有变化
  910. if (this.editPosition === this.editOriginalPosition && this.editLayer === this.editOriginalLayer) {
  911. this.$message.warning('位置没有变化');
  912. return;
  913. }
  914. // 设置loading状态,防止重复点击
  915. this.editPositionLoading = true;
  916. updatePalletDetailPosition({
  917. site: this.site,
  918. palletId: this.palletCode,
  919. serialNo: this.editSerialNo,
  920. newPosition: this.editPosition,
  921. newLayer: this.editLayer
  922. }).then(({ data }) => {
  923. if (data.code === 0) {
  924. this.$message.success('位置修改成功');
  925. this.closeEditPositionModal();
  926. this.refreshTable();
  927. } else {
  928. this.$message.error(data.msg || '位置修改失败');
  929. }
  930. }).catch(error => {
  931. console.error('位置修改失败:', error);
  932. this.$message.error('位置修改失败');
  933. }).finally(() => {
  934. // 无论成功或失败,都要恢复按钮状态
  935. this.editPositionLoading = false;
  936. });
  937. },
  938. // 关闭修改位置模态框
  939. closeEditPositionModal() {
  940. this.editPositionModalVisible = false;
  941. this.editSerialNo = '';
  942. this.editPosition = '';
  943. this.editLayer = '';
  944. this.editLayerOptions = [];
  945. this.editOriginalPosition = '';
  946. this.editOriginalLayer = '';
  947. },
  948. // 运输指令按钮点击事件
  949. handleTransportOrder() {
  950. if (!this.palletCode) {
  951. this.$message.error('请先扫描栈板');
  952. return;
  953. }
  954. // 首先获取当前栈板信息
  955. checkPalletExists({
  956. site: this.site,
  957. palletId: this.palletCode
  958. }).then(({ data }) => {
  959. if (data.code == 0) {
  960. // 从返回数据中获取栈板位置信息
  961. this.currentPalletStation = data.locationCode || '';
  962. if (!this.currentPalletStation) {
  963. this.$message.error('无法获取当前栈板位置');
  964. return;
  965. }
  966. this.palletCode=data.palletId
  967. this.transportModalVisible = true;
  968. this.selectedTargetArea = '';
  969. this.transportAreaOptions = [];
  970. // 获取所有AGV站点列表,只显示空闲站点(statusDb = 0)且站点类型为正式站点的区域
  971. getAgvStations({}).then(({ data }) => {
  972. if (data.code === 0) {
  973. // 过滤出statusDb为0(空闲)且stationType为'正式站点'的站点
  974. const freeStations = (data.stations || []).filter(station =>
  975. station.statusDb === 0 && station.stationType === '正式站点'
  976. );
  977. console.log('空闲且为正式站点的数量:', freeStations.length);
  978. // 提取station_area并去重(只提取有stationArea的站点)
  979. const areaMap = new Map();
  980. freeStations.forEach(station => {
  981. if (station.stationArea && station.stationArea.trim() !== '') {
  982. if (!areaMap.has(station.stationArea)) {
  983. areaMap.set(station.stationArea, {
  984. stationArea: station.stationArea,
  985. areaType: station.areaType
  986. });
  987. }
  988. }
  989. });
  990. this.transportAreaOptions = Array.from(areaMap.values());
  991. console.log('可用目标区域数量:', this.transportAreaOptions.length);
  992. // 如果没有可用区域,给出友好提示
  993. if (this.transportAreaOptions.length === 0) {
  994. this.$message.warning('当前没有可用的目标区域,所有站点可能都已被占用');
  995. }
  996. } else {
  997. this.$message.error(data.msg || '获取站点列表失败');
  998. }
  999. }).catch(error => {
  1000. console.error('获取站点列表失败:', error);
  1001. this.$message.error('获取站点列表失败');
  1002. });
  1003. } else {
  1004. this.$message.error(data.msg || '获取栈板信息失败');
  1005. }
  1006. }).catch(error => {
  1007. console.error('获取栈板信息失败:', error);
  1008. this.$message.error('获取栈板信息失败');
  1009. });
  1010. },
  1011. // 确认创建运输任务
  1012. confirmTransportTask() {
  1013. if (!this.currentPalletStation) {
  1014. this.$message.error('无法获取当前栈板位置');
  1015. return;
  1016. }
  1017. if (!this.selectedTargetArea) {
  1018. this.$message.error('请选择目标区域');
  1019. return;
  1020. }
  1021. // 设置loading状态,防止重复点击
  1022. this.transportTaskLoading = true;
  1023. // 调用包含组盘处理的接口(后端会根据区域自动查找空闲站点)
  1024. callPalletToStationWithUpdateZuPan({
  1025. site: this.site,
  1026. startStation: this.currentPalletStation,
  1027. targetArea: this.selectedTargetArea
  1028. }).then(({ data }) => {
  1029. if (data.code === 0) {
  1030. this.$message.success('栈板运输任务创建成功');
  1031. this.closeTransportModal();
  1032. // 清空页面数据,初始化页面
  1033. this.resetPage();
  1034. } else {
  1035. this.$message.error(data.msg || '创建运输任务失败');
  1036. }
  1037. }).catch(error => {
  1038. console.error('创建运输任务失败:', error);
  1039. this.$message.error('创建运输任务失败');
  1040. }).finally(() => {
  1041. // 无论成功或失败,都要恢复按钮状态
  1042. this.transportTaskLoading = false;
  1043. });
  1044. },
  1045. // 关闭运输任务模态框
  1046. closeTransportModal() {
  1047. this.transportModalVisible = false;
  1048. this.currentPalletStation = '';
  1049. this.selectedTargetArea = '';
  1050. this.transportAreaOptions = [];
  1051. },
  1052. // 确认Call栈板
  1053. confirmCallPallet() {
  1054. if (!this.selectedCallStartStation) {
  1055. this.$message.error('请选择起始站点');
  1056. return;
  1057. }
  1058. if (!this.selectedCallTargetStation) {
  1059. this.$message.error('请选择目标站点');
  1060. return;
  1061. }
  1062. // 前端验证:两个站点不能一样
  1063. if (this.selectedCallStartStation === this.selectedCallTargetStation) {
  1064. this.$message.error('起始站点和目标站点不能相同');
  1065. return;
  1066. }
  1067. // 设置loading状态,防止重复点击
  1068. this.callPalletLoading = true;
  1069. callPalletToStation({
  1070. site: this.site,
  1071. startStation: this.selectedCallStartStation,
  1072. targetStation: this.selectedCallTargetStation
  1073. }).then(({ data }) => {
  1074. if (data.code === 0) {
  1075. this.$message.success('空托盘调用任务创建成功');
  1076. this.closeCallPalletModal();
  1077. } else {
  1078. this.$message.error(data.msg || '调用空托盘失败');
  1079. }
  1080. }).catch(error => {
  1081. console.error('调用空托盘失败:', error);
  1082. this.$message.error('异常:'+error);
  1083. }).finally(() => {
  1084. // 无论成功或失败,都要恢复按钮状态
  1085. this.callPalletLoading = false;
  1086. });
  1087. },
  1088. // 关闭Call栈板模态框
  1089. closeCallPalletModal() {
  1090. this.callPalletModalVisible = false;
  1091. this.selectedCallStartStation = '';
  1092. this.selectedCallTargetStation = '';
  1093. this.callStartStationOptions = [];
  1094. this.callTargetStationOptions = [];
  1095. },
  1096. },
  1097. mounted() {
  1098. this.$nextTick(() => {
  1099. if (this.$refs.palletInput) {
  1100. this.$refs.palletInput.focus();
  1101. }
  1102. });
  1103. }
  1104. };
  1105. </script>
  1106. <style scoped>
  1107. /* 表格样式 */
  1108. .detail-table {
  1109. background: white;
  1110. border-radius: 6px;
  1111. overflow: hidden;
  1112. border: 1px solid #e0e0e0;
  1113. }
  1114. .table-header,
  1115. .table-row {
  1116. display: flex;
  1117. align-items: center;
  1118. padding: 8px;
  1119. border-bottom: 1px solid #e0e0e0;
  1120. }
  1121. .table-header {
  1122. background: #f5f5f5;
  1123. font-weight: bold;
  1124. font-size: 14px;
  1125. }
  1126. .table-row {
  1127. font-size: 13px;
  1128. }
  1129. .table-row:last-child {
  1130. border-bottom: none;
  1131. }
  1132. .col-position {
  1133. flex: 1;
  1134. text-align: center;
  1135. }
  1136. .col-layer {
  1137. flex: 1;
  1138. text-align: center;
  1139. }
  1140. .col-serial {
  1141. flex: 4;
  1142. text-align: center;
  1143. word-break: break-all;
  1144. }
  1145. /* 空数据提示 */
  1146. .empty-hint {
  1147. text-align: center;
  1148. color: #999;
  1149. padding: 20px;
  1150. background: white;
  1151. border-radius: 6px;
  1152. margin-top: 16px;
  1153. }
  1154. /* 模态框样式 */
  1155. .scan-modal-content {
  1156. padding: 10px 0;
  1157. }
  1158. .modal-form {
  1159. margin-bottom: 16px;
  1160. }
  1161. .dialog-footer {
  1162. text-align: center;
  1163. }
  1164. /* 修复模态框层级问题 */
  1165. ::v-deep .el-dialog__wrapper {
  1166. z-index: 2000 !important;
  1167. }
  1168. ::v-deep .el-overlay {
  1169. z-index: 2000 !important;
  1170. }
  1171. /* 修复单选框样式 */
  1172. ::v-deep .el-radio {
  1173. margin-right: 8px !important;
  1174. }
  1175. ::v-deep .el-radio__label {
  1176. font-size: 14px;
  1177. }
  1178. /* 标题行样式 */
  1179. .list-title-row {
  1180. display: flex;
  1181. justify-content: space-between;
  1182. align-items: center;
  1183. margin-bottom: 8px;
  1184. }
  1185. .list-title-row .list-title {
  1186. margin: 0;
  1187. flex: 1;
  1188. }
  1189. /* 空数据行样式 */
  1190. .empty-row {
  1191. justify-content: center;
  1192. align-items: center;
  1193. padding: 20px;
  1194. border-bottom: none;
  1195. }
  1196. .empty-row .empty-hint {
  1197. text-align: center;
  1198. color: #999;
  1199. width: 100%;
  1200. }
  1201. /* 按钮禁用状态样式 */
  1202. .action-btn:disabled {
  1203. opacity: 0.6;
  1204. cursor: not-allowed;
  1205. background-color: #ccc !important;
  1206. border-color: #ccc !important;
  1207. }
  1208. </style>