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.

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