index.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876
  1. <template>
  2. <div class="container">
  3. <van-nav-bar title="宝时快上" left-arrow fixed placeholder @click-left="goBack" z-index="100"
  4. @click-right="refresh()">
  5. <template #left>
  6. <van-icon name="arrow-left" size="25" />
  7. <div style="color: #fff">返回</div>
  8. </template>
  9. <template #right>
  10. <div style="color: #fff">刷新<van-icon name="replay" /></div>
  11. </template>
  12. </van-nav-bar>
  13. <div class="take-delivery">
  14. <div class="take-info">
  15. <div class="take-info-no">
  16. <div class="info-no-tips">
  17. <div>货主: <span style="color: #333;font-weight: bold;" v-if="barcodeActiveList.length>0">{{ ownerMap[barcodeActiveList[0].owner] || barcodeActiveList[0].owner }}</span>
  18. <span v-else>--</span>
  19. </div>
  20. <div>待上架数:<span style="color: #0077ff;font-weight: bold;">{{ totalQuantity || 0 }}</span></div>
  21. </div>
  22. </div>
  23. <div class="take-info-number">
  24. <div class="info-number-left">
  25. <div class="number-left-box">
  26. <div>开始时间</div>
  27. <div class="left-box-title">{{ currentTime }}</div>
  28. </div>
  29. <div class="number-left-box">
  30. <div>已用时</div>
  31. <div class="left-box-title">{{ formattedTime }}</div>
  32. </div>
  33. </div>
  34. <div class="info-number-right">
  35. <div>容器号</div>
  36. <div style="display: flex;justify-content:center;align-items: center;">
  37. <div style="flex: 1;font-size: 14px;font-weight: bold;color: #0077ff;line-height: 34px">
  38. {{ containerNo || '--' }}
  39. </div>
  40. <div style="width:50px">
  41. <van-button type="primary" size="mini" plain @click="switchTask">切换容器</van-button>
  42. </div>
  43. </div>
  44. </div>
  45. </div>
  46. </div>
  47. <div class="take-barcode">
  48. <div class="barcode-input">
  49. <van-search
  50. ref="searchRef"
  51. v-model="searchBarcode"
  52. placeholder="请扫描商品条码"
  53. @search="_handlerScan(searchBarcode)"
  54. label="商品条码:"
  55. left-icon=""
  56. :class="[scanType===2?'search-input-barcode':'','van-hairline--bottom']"
  57. @focus="scanType=2"
  58. autocomplete="off"
  59. @input="onAsnCancel"
  60. @clear="reset"
  61. >
  62. </van-search>
  63. </div>
  64. <div class="barcode-input">
  65. <van-search
  66. ref="locationRef"
  67. v-model="searchLocation"
  68. placeholder="请扫描库位编号"
  69. @search="_handlerScan(searchLocation)"
  70. label="库位编号:"
  71. left-icon=""
  72. :class="[scanType===3?'search-input-barcode':'','van-hairline--bottom']"
  73. @focus="scanType=3"
  74. autocomplete="off"
  75. >
  76. <template #right-icon>
  77. <van-button
  78. v-if="forcePublishEnabled && barcodeActiveList.length > 0"
  79. type="primary"
  80. size="mini"
  81. plain
  82. :loading="changeLocationLoading"
  83. @click.stop="onChangeLocation"
  84. >
  85. 换一换
  86. </van-button>
  87. </template>
  88. </van-search>
  89. </div>
  90. <div class="barcode-input">
  91. <van-search
  92. ref="numberRef"
  93. v-model="searchCount"
  94. placeholder="请输入上架数量"
  95. type="number"
  96. label="上架数量:"
  97. left-icon=""
  98. autocomplete="off"
  99. show-action
  100. :min="1"
  101. :max="barcodeQuantity(barcodeActiveList)"
  102. @search="onConfirm"
  103. :class="[scanType===4?'search-input-barcode':'','van-hairline--bottom','search-input-number']"
  104. @focus="scanType=4"
  105. >
  106. <template #action>
  107. <div style="display: flex; align-items: center;margin-left: 20px">
  108. <div style="font-size: 12px">预计:</div>
  109. <div style="font-size: 18px;font-weight: bold;color: #ee0a25">{{ barcodeQuantity(barcodeActiveList) }}
  110. </div>
  111. </div>
  112. </template>
  113. </van-search>
  114. </div>
  115. </div>
  116. <div class="take-lot" v-if="barcodeActiveList.length>0">
  117. <van-cell-group>
  118. <div class="take-lot-title">批次信息</div>
  119. <template v-for="(value, key) in lotAttributes" :key="key">
  120. <van-cell v-if="barcodeActiveList[0][key]">
  121. <template #title>
  122. <van-icon name="warning-o" color="#ed6a0c" />
  123. <span class="custom-title">{{ value.title }}</span>
  124. </template>
  125. <template #value>
  126. <div>{{ barcodeActiveList[0][key] }}</div>
  127. </template>
  128. </van-cell>
  129. </template>
  130. </van-cell-group>
  131. </div>
  132. <div class="take-button">
  133. <div class="btn" type="primary" size="large" round style="height: 36px" @click="onConfirm">上架</div>
  134. </div>
  135. <div>
  136. <location-list :locationList="locationList" :legacy="!forcePublishEnabled" />
  137. </div>
  138. </div>
  139. </div>
  140. <!-- 条码输入组件 -->
  141. <input-barcode :back="back" @setBarcode="setBarcode" ref="inputBarcodeRef" />
  142. <!-- 单据选择-->
  143. <van-action-sheet v-model:show="lotBarcodeTrueFalseBy" cancel-text="取消" description="请选择具体单据"
  144. close-on-click-action>
  145. <van-cell-group>
  146. <van-cell v-for="item in lotBarcodeList" @click="onDetailActive(item)">
  147. <template #title>
  148. {{ item[0].barcode }}({{ item[0].lotNumber }}-{{ barcodeQuantity(item) }}件)
  149. </template>
  150. <template #label>
  151. 生产日期:{{ item[0].lotAtt01 || '--' }}-失效日期:{{ item[0].lotAtt02 || '--' }}
  152. </template>
  153. </van-cell>
  154. </van-cell-group>
  155. </van-action-sheet>
  156. <!-- 推荐库位列表-->
  157. <van-action-sheet v-model:show="locationTrueFalseBy" cancel-text="取消" description="推荐库位列表"
  158. close-on-click-action>
  159. <div style="max-height: 60vh;overflow: auto;">
  160. <location-list :locationList="locationList" :legacy="!forcePublishEnabled" />
  161. </div>
  162. </van-action-sheet>
  163. <!-- 组合商品上架数量-->
  164. <barcode-combine ref="barcodeCombineRef" @setCombine="setPutawayCombine" @cancel="onCombineCancel" :matched-sku="combineMatchedSku" />
  165. </template>
  166. <script setup>
  167. import { onMounted, onUnmounted, ref, computed } from 'vue'
  168. import { androidFocus, getHeader, goBack, scanError, scanSuccess } from '@/utils/android'
  169. import InputBarcode from '@/views/outbound/picking/components/InputBarcode.vue'
  170. import LocationList from '@/views/inbound/putaway/components/LocationList.vue'
  171. import BarcodeCombine from '@/views/inbound/putaway/components/BarcodeCombine.vue'
  172. import { openListener,closeListener,scanInit } from '@/utils/keydownListener.js'
  173. import { useRouter } from 'vue-router'
  174. import { closeLoading, showLoading } from '@/utils/loading'
  175. import { useStore } from '@/store/modules/user'
  176. import { showNotify, showToast } from 'vant'
  177. import { getCurrentTime } from '@/utils/date'
  178. import { getWaitPutawayListNew, setPutawayNew } from '@/api/putaway/index'
  179. import { getListCombineSku } from '@/api/picking'
  180. import { barcodeToUpperCase } from '@/utils/dataType.js'
  181. import { getRecommendedLocation, getRecommendedLocationNew } from '@/api/haikang/index'
  182. import { findSysParamByKey } from '@/api/basic/index'
  183. import { getOwnerList } from '@/hooks/basic/index'
  184. const router = useRouter()
  185. const store = useStore()
  186. try {
  187. getHeader()
  188. androidFocus()
  189. } catch (error) {
  190. router.push('/login')
  191. }
  192. /** 查询参数配置-精准推荐是否开启 */
  193. const forcePublishEnabled = ref(true)
  194. async function loadForcePublishParam() {
  195. try {
  196. const res = await findSysParamByKey({ paramKey: 'FORCE_PUBLISH_ENABLED' })
  197. console.log(res)
  198. if (res?.data && res.data=='true') {
  199. forcePublishEnabled.value = true
  200. }else{
  201. forcePublishEnabled.value = false
  202. }
  203. } catch (e) {
  204. console.error(e)
  205. }
  206. }
  207. // 页面初始化
  208. onMounted(async () => {
  209. openListener()
  210. scanInit(_handlerScan)
  211. await loadForcePublishParam()
  212. loadData()
  213. })
  214. const warehouse = store.warehouse
  215. //容器号
  216. const containerNo = ref('')
  217. //数据列表
  218. const dataList = ref([])
  219. //
  220. const dataMap = ref({})
  221. //库位列表
  222. const locationTrueFalseBy = ref(false)
  223. const locationList = ref([])
  224. //商品条码
  225. const searchBarcode = ref('')
  226. //库位
  227. const searchLocation = ref('')
  228. //收货数量
  229. const searchCount = ref('')
  230. //收货详情
  231. const taskInfo = ref({})
  232. //开始时间
  233. const currentTime = ref('--')
  234. const scanType = ref(2)
  235. const putweayType=ref('def')
  236. const lotAttributes = {
  237. lotAtt01: { title: '生产日期' },
  238. lotAtt02: { title: '失效日期' },
  239. lotAtt03: { title: '入库日期' },
  240. lotAtt04: { title: '生产批号' },
  241. lotAtt05: { title: '属性仓' },
  242. lotAtt08: { title: '质量状态' },
  243. }
  244. // 获取货主
  245. const { ownerMap, getOwnerData } = getOwnerList()
  246. getOwnerData()
  247. //待上架数
  248. const totalQuantity = computed(() => {
  249. return dataList.value.reduce((sum, item) => sum + Number(item.quantity), 0)
  250. })
  251. //待上架数
  252. const barcodeQuantity = (list) => {
  253. return list.reduce((sum, item) => sum + Number(item.quantity), 0)
  254. }
  255. const back = ref(true)
  256. const inputBarcodeType = ref('task')
  257. //输入框组件
  258. const inputBarcodeRef = ref(null)
  259. const oldSearchBarcode = ref('')
  260. /**
  261. * 计算时分秒
  262. */
  263. // 时器的总秒数
  264. let totalSeconds = ref(0)
  265. //时分秒
  266. const formattedTime = ref('00:00:00')
  267. let windowTimer = null // 计时器的引用
  268. const updateFormattedTime = () => {
  269. let hours = Math.floor(totalSeconds.value / 3600)
  270. let minutes = Math.floor((totalSeconds.value % 3600) / 60)
  271. let seconds = totalSeconds.value % 60
  272. formattedTime.value = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
  273. }
  274. // 启动计时器
  275. const startTimer = () => {
  276. if (!windowTimer) {
  277. windowTimer = setInterval(() => {
  278. totalSeconds.value++
  279. updateFormattedTime()
  280. }, 1000)
  281. }
  282. }
  283. // 停止计时器
  284. const stopTimer = () => {
  285. if (windowTimer) {
  286. clearInterval(windowTimer)
  287. windowTimer = null
  288. }
  289. }
  290. // 设置容器号
  291. const setBarcode = (code, type) => {
  292. showLoading()
  293. if (!type) { //切换任务时初始化计时器
  294. stopTimer()
  295. formattedTime.value = '00:00:00'
  296. totalSeconds.value = 0
  297. }
  298. const params = { warehouseId:warehouse, containerId: code }
  299. getWaitPutawayListNew(params).then(res => {
  300. reset()
  301. scanType.value=2
  302. back.value = true
  303. if (!type) {//切换任务成功重启计时器
  304. currentTime.value = getCurrentTime()
  305. startTimer()
  306. }
  307. if (res.data.asnToShelfList.length > 0 || res.data.noAsnToShelfList.length>0 ) {
  308. const asnToShelfList = res.data.asnToShelfList.map(item => {
  309. return {
  310. ...item,
  311. totalQuantity: 0,
  312. type:'asn'
  313. }
  314. })
  315. // 先映射数据
  316. const noAsnToShelfListTemp = res.data.noAsnToShelfList.map(item => {
  317. return {
  318. ...item,
  319. lotNumber: item.lotNum,
  320. lotAtt01: item.productDate,
  321. lotAtt02: item.expireDate,
  322. lotAtt04: item.productNum,
  323. lotAtt08: item.quality,
  324. quantity: item.remainShelfQty,
  325. taskLineNo: item.id,
  326. owner:item.customer,
  327. type:'noAsn'
  328. }
  329. })
  330. // 按照 lotNumber 和 businessNo 分组并累加 quantity
  331. const groupedMap = {}
  332. noAsnToShelfListTemp.forEach(item => {
  333. const key = `${item.lotNumber}_${item.businessNo || ''}`
  334. if (!groupedMap[key]) {
  335. groupedMap[key] = {
  336. items: [],
  337. totalQuantity: 0
  338. }
  339. }
  340. groupedMap[key].items.push(item)
  341. groupedMap[key].totalQuantity += Number(item.quantity) || 0
  342. })
  343. // 更新每项的 totalQuantity 为累加后的值
  344. const noAsnToShelfList = noAsnToShelfListTemp.map(item => {
  345. const key = `${item.lotNumber}_${item.businessNo || ''}`
  346. return {
  347. ...item,
  348. totalQuantity: groupedMap[key].totalQuantity
  349. }
  350. })
  351. dataList.value = [...asnToShelfList,...noAsnToShelfList]
  352. dataMap.value = groupedData(dataList.value)
  353. containerNo.value = code
  354. scanSuccess()
  355. } else {
  356. reset()
  357. dataMap.value = {}
  358. dataList.value = []
  359. inputBarcodeRef.value?.show('', '请扫描容器号', '暂无待上架信息')
  360. scanError()
  361. }
  362. }).catch(err => {
  363. reset()
  364. dataMap.value = {}
  365. dataList.value = []
  366. inputBarcodeRef.value?.show('', '请扫描容器号', err.message)
  367. scanError()
  368. }).finally(() => {
  369. closeLoading()
  370. })
  371. }
  372. //根据条码批次分组数据
  373. const groupedData = (data) => {
  374. return data.reduce((acc, item) => {
  375. const key = `{${item.barcode}、${item.barcodeAs}、${item.sku}}-${item.lotNumber}`
  376. if (acc[key]) {
  377. acc[key].push(item)
  378. } else {
  379. acc[key] = [item]
  380. }
  381. return acc
  382. }, {})
  383. }
  384. //匹配待上架列表数据
  385. const matchingBarcodeItem = (data, barcode) => {
  386. const matchingItems = []
  387. for (const key in data) {
  388. const barcodeList = key.match(/\{(.*?)\}/)[1].split('、')
  389. if (data.hasOwnProperty(key)) {
  390. if (barcodeList.some(item => barcodeToUpperCase(item) === barcodeToUpperCase(barcode))) {
  391. matchingItems.push(data[key])
  392. }
  393. }
  394. }
  395. return matchingItems.length > 0 ? matchingItems : []
  396. }
  397. // setBarcode()
  398. //切换任务
  399. const switchTask = () => {
  400. inputBarcodeType.value = 'switchTask'
  401. back.value = false
  402. if (forcePublishEnabled.value) excludedLocations.value = {}
  403. inputBarcodeRef.value?.show('', `请扫描容器号`, '')
  404. }
  405. //批次数据
  406. const lotBarcodeList = ref([])
  407. const lotBarcodeTrueFalseBy = ref(false)
  408. const barcodeActiveList = ref([])
  409. // 组合商品
  410. const barcodeCombineRef = ref(null)
  411. const putawayCombineData = ref(null)
  412. const combineMatchedSku = ref([])
  413. // 已推荐过的库位,按 lotNum 批次维度存储,用于换一换时排除
  414. const excludedLocations = ref({})
  415. // 换一换按钮 loading
  416. const changeLocationLoading = ref(false)
  417. const reset = () => {
  418. searchCount.value = ''
  419. searchBarcode.value = ''
  420. searchLocation.value = ''
  421. oldSearchBarcode.value = ''
  422. locationList.value = []
  423. barcodeActiveList.value = []
  424. putawayCombineData.value = null
  425. combineMatchedSku.value = []
  426. }
  427. // 组合商品上架数量弹框
  428. const _showPutawayCombineDialog = (batchItem) => {
  429. if (!putawayCombineData.value || !batchItem?.length) return
  430. const total = batchItem.reduce((sum, i) => sum + Number(i.quantity || 0), 0)
  431. combineMatchedSku.value = [{
  432. matchedJson: putawayCombineData.value,
  433. expectedQuantity: total,
  434. receivedQuantity: 0,
  435. }]
  436. barcodeCombineRef.value?.show()
  437. }
  438. // 组合商品确认上架数量
  439. const setPutawayCombine = ({ dataList }) => {
  440. if (dataList?.[0]?.quantity != null) {
  441. searchCount.value = String(dataList[0].quantity)
  442. }
  443. showNotify({ type: 'success', duration: 2000, message: `已填入上架数量:${searchCount.value},请扫描库位并确认上架` })
  444. }
  445. // 组合商品取消
  446. const onCombineCancel = () => {
  447. const qtyPerSet = putawayCombineData.value?.quantity ?? 1
  448. const total = barcodeQuantity(barcodeActiveList.value)
  449. searchCount.value = String(total < qtyPerSet ? 1 : qtyPerSet)
  450. }
  451. // 选择单据
  452. const onDetailActive = (item) => {
  453. barcodeActiveList.value = item
  454. lotBarcodeTrueFalseBy.value = false
  455. if (putawayCombineData.value) {
  456. _showPutawayCombineDialog(item)
  457. _getRecommendedLocation(item[0])
  458. scanType.value = 3
  459. return
  460. }
  461. searchCount.value = 1
  462. scanType.value = 3
  463. _getRecommendedLocation(item[0])
  464. scanSuccess()
  465. }
  466. const onAsnCancel = () => {
  467. if (searchBarcode.value === '' || (oldSearchBarcode.value.length != searchBarcode.value.length && oldSearchBarcode.value != '')) {
  468. barcodeActiveList.value = []
  469. searchCount.value = ''
  470. locationList.value = []
  471. }
  472. }
  473. // 商品条码不匹配时,查询组合条码
  474. const _handlePutawayCombineProduct = (code) => {
  475. showLoading()
  476. getListCombineSku({ combineSku: barcodeToUpperCase(code), workEnvironment: 'inbound' })
  477. .then((res) => {
  478. const _err = (msg) => {
  479. closeLoading()
  480. scanError()
  481. showNotify({ type: 'danger', duration: 3000, message: msg })
  482. reset()
  483. }
  484. if (!res.data?.length) return _err(`${code}-商品条码不匹配,请重新扫描`)
  485. if (res.data.length > 1) return _err('不支持多商品组合商品')
  486. const combineBarcode = res.data[0].barcode
  487. lotBarcodeList.value = matchingBarcodeItem(dataMap.value, combineBarcode)
  488. if (lotBarcodeList.value.length === 0) return _err('组合商品与待上架数据不匹配,请检查组合商品配置!')
  489. putawayCombineData.value = res.data[0]
  490. closeLoading()
  491. scanSuccess()
  492. if (lotBarcodeList.value.length === 1) {
  493. barcodeActiveList.value = lotBarcodeList.value[0]
  494. _showPutawayCombineDialog(lotBarcodeList.value[0])
  495. _getRecommendedLocation(barcodeActiveList.value[0])
  496. scanType.value = 3
  497. } else {
  498. locationList.value = []
  499. barcodeActiveList.value = []
  500. searchCount.value = ''
  501. searchLocation.value = ''
  502. lotBarcodeTrueFalseBy.value = true
  503. }
  504. })
  505. .catch(() => {
  506. closeLoading()
  507. scanError()
  508. showNotify({ type: 'danger', duration: 3000, message: `${code}-商品条码不匹配,请重新扫描` })
  509. reset()
  510. })
  511. }
  512. // 扫描条码监听
  513. const _handlerScan = (code) => {
  514. if (scanType.value == 2) {
  515. searchBarcode.value = code
  516. oldSearchBarcode.value = code
  517. lotBarcodeList.value = matchingBarcodeItem(dataMap.value, code)
  518. if (lotBarcodeList.value.length > 0) {
  519. putawayCombineData.value = null
  520. combineMatchedSku.value = []
  521. if (lotBarcodeList.value.length == 1) {
  522. barcodeActiveList.value = lotBarcodeList.value[0]
  523. _getRecommendedLocation(barcodeActiveList.value[0])
  524. scanType.value = 3
  525. scanSuccess()
  526. } else if (lotBarcodeList.value.length > 1) {
  527. locationList.value = []
  528. barcodeActiveList.value = []
  529. searchCount.value = ''
  530. searchLocation.value = ''
  531. lotBarcodeTrueFalseBy.value = true
  532. }
  533. } else {
  534. _handlePutawayCombineProduct(code)
  535. }
  536. } else if (scanType.value == 3) {
  537. const scannedLocation = barcodeToUpperCase(code)
  538. if (forcePublishEnabled.value) {
  539. const { lotAtt02 } = barcodeActiveList.value[0]
  540. if (locationList.value.length > 0 && !lotAtt02) {
  541. const recommendedLocations = locationList.value.map(item => barcodeToUpperCase(item.locationId || ''))
  542. if (!recommendedLocations.includes(scannedLocation)) {
  543. showNotify({ type: 'warning', duration: 3000, message: `扫描库位${scannedLocation}与推荐库位不一致,请确认` })
  544. searchLocation.value=''
  545. scanError()
  546. return
  547. }
  548. }
  549. }
  550. searchLocation.value = scannedLocation
  551. scanType.value = 4
  552. if (!searchCount.value) searchCount.value = 1
  553. scanSuccess()
  554. }
  555. }
  556. // 获取推荐库位(false:master GET 多行列表;true:精准推荐 POST)
  557. const _getRecommendedLocation = async (item, options = {}) => {
  558. const { fromChangeLocation = false } = options
  559. const { lotNumber, owner } = item
  560. if (!forcePublishEnabled.value) {
  561. try {
  562. const res = await getRecommendedLocation({ warehouse, lotNum: lotNumber, owner })
  563. locationList.value = res.data || []
  564. } catch (err) {
  565. console.error(err)
  566. }
  567. return
  568. }
  569. const { sku, quantity, lotAtt08 } = item
  570. const listByLot = fromChangeLocation ? (excludedLocations.value[lotNumber] || []) : []
  571. const uniqueLocationIds = listByLot.length > 0
  572. ? [...new Set(listByLot.map(loc => loc.locationId))]
  573. : undefined
  574. try {
  575. const params = { warehouse, lotNum: lotNumber, owner, sku, qty: quantity, lotAtt08 }
  576. if (fromChangeLocation && uniqueLocationIds) params.excludedLocations = uniqueLocationIds
  577. if (containerNo.value?.includes('TH-')) params.scene = 'RETURN_SHELVE'
  578. const res = await getRecommendedLocationNew(params)
  579. if (res.data) {
  580. const loc = res.data.location ?? res.data
  581. if (fromChangeLocation) {
  582. // 按批次维度存储已推荐库位,用于后续换一换排除
  583. const lotExcluded = excludedLocations.value[lotNumber] || []
  584. excludedLocations.value = {
  585. ...excludedLocations.value,
  586. [lotNumber]: [...lotExcluded, loc]
  587. }
  588. }
  589. locationList.value = [res.data]
  590. searchCount.value = 1
  591. }
  592. } catch (err) {
  593. console.error(err)
  594. }
  595. }
  596. // 换一换:请求新的推荐库位,排除当前批次已推荐过的
  597. const onChangeLocation = async () => {
  598. if (!forcePublishEnabled.value) return
  599. if (barcodeActiveList.value.length > 0) {
  600. changeLocationLoading.value = true
  601. try {
  602. const item = barcodeActiveList.value[0]
  603. const lotNumber = item.lotNumber
  604. const currentLoc = locationList.value?.[0]?.location ?? locationList.value?.[0]
  605. if (currentLoc) {
  606. const lotExcluded = excludedLocations.value[lotNumber] || []
  607. excludedLocations.value = {
  608. ...excludedLocations.value,
  609. [lotNumber]: [...lotExcluded, currentLoc]
  610. }
  611. }
  612. await _getRecommendedLocation(item, { fromChangeLocation: true })
  613. } finally {
  614. changeLocationLoading.value = false
  615. }
  616. }
  617. }
  618. const numberRef = ref(null)
  619. const locationRef = ref(null)
  620. // 完成收货校验
  621. const isCheck = () => {
  622. if (searchLocation.value == '') {
  623. locationRef.value?.focus()
  624. scanError()
  625. showToast({ duration: 3000, message: '请先扫描库位编号' })
  626. return false
  627. }
  628. if (forcePublishEnabled.value) {
  629. if(barcodeActiveList.value.length ==0) {
  630. showToast({ duration: 3000, message: '数据异常请重新扫描' })
  631. scanError()
  632. return
  633. }
  634. const { lotAtt02 } = barcodeActiveList.value[0]
  635. if (locationList.value.length > 0 && !lotAtt02) {
  636. const recommendedLocations = locationList.value.map(item => barcodeToUpperCase(item.locationId || ''))
  637. if (!recommendedLocations.includes(barcodeToUpperCase(searchLocation.value))) {
  638. locationRef.value?.focus()
  639. scanError()
  640. showToast({ duration: 3000, message: '库位与推荐库位不一致,无法上架' })
  641. return false
  642. }
  643. }
  644. }
  645. if (searchCount.value == '') {
  646. numberRef.value?.focus()
  647. scanError()
  648. showToast({ duration: 3000, message: '请先输入上架数量' })
  649. return false
  650. }
  651. const maxQuantity = barcodeQuantity(barcodeActiveList.value)
  652. if (Number(searchCount.value) > maxQuantity) {
  653. numberRef.value?.focus()
  654. scanError()
  655. showToast({ duration: 3000, message: `上架数量最大为:${maxQuantity}` })
  656. return false
  657. }
  658. return true
  659. }
  660. // 上架
  661. const onConfirm = () => {
  662. if (isCheck()) {
  663. const quantity = searchCount.value
  664. const asnDoShelfList = []
  665. const noAsnDoShelfList=[]
  666. const list = structuredClone(barcodeActiveList.value) // 深拷贝,保证原始数据不被修改
  667. // 按照 totalQuantity 从小到大排序,优先扣除最小的
  668. list.sort((a, b) => {
  669. const totalQuantityA = Number(a.totalQuantity || a.quantity || 0)
  670. const totalQuantityB = Number(b.totalQuantity || b.quantity || 0)
  671. return totalQuantityA - totalQuantityB
  672. })
  673. let remainingQuantity = quantity
  674. for (let i = 0; i < list.length && remainingQuantity > 0; i++) {
  675. const { taskNo, taskLineNo, warehouse, quantity } = list[i]
  676. const takeQuantity = Math.min(quantity, remainingQuantity)
  677. if(list[i].type=='asn'){
  678. asnDoShelfList.push({
  679. taskNo,
  680. taskLineNo,
  681. warehouse,
  682. container: '*',
  683. targetCode: searchLocation.value,
  684. quantity: takeQuantity, // 当前项扣除的数量
  685. })
  686. }else {
  687. noAsnDoShelfList.push({
  688. id:taskLineNo,
  689. location:searchLocation.value,
  690. qty:takeQuantity
  691. })
  692. }
  693. remainingQuantity -= takeQuantity
  694. }
  695. showLoading()
  696. setPutawayNew({asnDoShelfList,noAsnDoShelfList}).then(res => {
  697. if (totalQuantity.value - Number(searchCount.value) == 0) {
  698. containerNo.value = ''
  699. dataMap.value = {}
  700. dataList.value = []
  701. showNotify({ type: 'success', message: '当前任务已上架完成,请扫描下一个容器号', duration: 3000 })
  702. inputBarcodeRef.value?.show('', '请扫描容器号', '')
  703. stopTimer()
  704. } else {
  705. showNotify({ type: 'success', message: '上架成功,请继续扫描商品进行上架', duration: 3000 })
  706. setBarcode(containerNo.value, 'success')
  707. scanType.value = 2
  708. }
  709. reset()
  710. scanSuccess()
  711. }).catch(err => {
  712. scanError()
  713. }).finally(() => {
  714. closeLoading()
  715. })
  716. }
  717. }
  718. const refresh=()=>{
  719. reset()
  720. scanType.value=2
  721. loadData()
  722. }
  723. // 数据刷新
  724. const loadData = () => {
  725. if (!containerNo.value) {
  726. inputBarcodeRef.value?.show('', '请扫描容器号', '')
  727. return
  728. } else {
  729. setBarcode(containerNo.value, 'container')
  730. // currentTime.value=getCurrentTime()
  731. // startTimer()
  732. }
  733. }
  734. onUnmounted(() => {
  735. closeListener()
  736. stopTimer()
  737. })
  738. window.onRefresh = loadData
  739. </script>
  740. <style scoped lang="sass">
  741. .take-delivery
  742. .take-info
  743. padding: 6px 10px
  744. background: linear-gradient(to left, #c8e2fb, #ffffff)
  745. display: flex
  746. flex-direction: column
  747. text-align: left
  748. .take-info-no
  749. flex: 1
  750. .info-no-title
  751. font-size: 19px
  752. font-width: 500
  753. display: flex
  754. justify-content: space-between
  755. align-items: center
  756. .info-no-tips
  757. font-size: 14px
  758. color: #666666
  759. display: flex
  760. justify-content: space-between
  761. padding: 6px 0
  762. .take-info-number
  763. flex: 1
  764. border-top: 1.5px solid #efefef
  765. display: flex
  766. justify-content: space-between
  767. gap: 10px
  768. color: #666
  769. font-size: 14px
  770. padding-top: 10px
  771. .info-number-left
  772. flex: 1
  773. display: flex
  774. justify-content: space-evenly
  775. align-items: center
  776. .number-left-box
  777. flex: 1
  778. display: flex
  779. flex-direction: column
  780. align-items: center
  781. .left-box-title
  782. font-size: 14px
  783. font-weight: bold
  784. color: #000
  785. line-height: 34px
  786. .info-number-right
  787. width: 45%
  788. text-align: center
  789. .van-search
  790. padding: 0
  791. .take-barcode
  792. margin-top: 10px
  793. text-align: left
  794. background: #FFFFFF
  795. .barcode-input
  796. ::v-deep(.van-search)
  797. padding: 0
  798. height: 50px
  799. display: flex
  800. align-items: center
  801. ::v-deep(.van-search__field)
  802. border-bottom: 2px solid #ffffff
  803. display: flex
  804. align-items: center
  805. height: 50px
  806. ::v-deep(.van-search__content)
  807. background: #fff
  808. display: flex
  809. align-items: center
  810. height: 50px
  811. ::v-deep(.van-field__control)
  812. font-size: 16px
  813. font-weight: bold
  814. ::v-deep(.van-search__label)
  815. font-size: 16px
  816. font-weight: bold
  817. .search-input-barcode
  818. ::v-deep(.van-search__field)
  819. border-bottom: 2px solid #0077ff
  820. z-index: 2
  821. .search-input-number
  822. ::v-deep(.van-field__control)
  823. font-size: 16px
  824. font-weight: bold
  825. color: #ee0a25
  826. .take-lot
  827. text-align: left
  828. margin-top: 5px
  829. ::v-deep(.van-cell)
  830. padding: 5px 8px
  831. .take-lot-title
  832. font-size: 15px
  833. font-weight: bold
  834. padding: 0 5px
  835. border-left: 3px solid #1989fa
  836. color: #333
  837. margin-bottom: 3px
  838. .take-button
  839. padding: 10px 20px
  840. .btn
  841. background: #1989fa
  842. color: #fff
  843. font-size: 16px
  844. line-height: 35px
  845. border-radius: 8px
  846. </style>