Jelajahi Sumber

宝时快上-换一换逻辑调整

zhaohuanhuan 2 hari lalu
induk
melakukan
86fd780063

+ 5 - 1
src/types/haikang.ts

@@ -94,9 +94,13 @@ export interface getRecommendedLocationTypeNew {
    */
   locationUse?: string;
   /**
-   * 库位类型名称
+   * 库位类型名称,如 0.3-0.4-0.3
    */
   locationType?: string;
+  /**
+   * 换一换时是否推荐更大库位类型,true 更大 false 更小
+   */
+  largerLocationType?: boolean;
   [property: string]: any;
 }
 export interface setBindAllocateWallType {

+ 182 - 0
src/views/inbound/putaway/components/ChangeLocationPicker.vue

@@ -0,0 +1,182 @@
+<template>
+  <van-dialog
+    v-model:show="visible"
+    class="change-location-dialog-wrap"
+    :show-cancel-button="false"
+    :show-confirm-button="false"
+    :close-on-click-overlay="true"
+  >
+    <div class="change-location-picker">
+      <div class="change-location-picker__header">
+        <span class="change-location-picker__title">换一换</span>
+        <button type="button" class="change-location-picker__close" aria-label="关闭" @click="onClose">
+          <van-icon name="cross" size="22" />
+        </button>
+      </div>
+
+      <div class="change-location-picker__body">
+        <div class="change-location-picker__item change-location-picker__item--primary" @click="onSystemPick">
+          系统推荐
+        </div>
+        <div class="change-location-picker__row">
+          <div
+            class="change-location-picker__item"
+            :class="{ 'change-location-picker__item--disabled': disableLarger }"
+            @click="onLargerPick"
+          >
+            换大库位
+          </div>
+          <div
+            class="change-location-picker__item"
+            :class="{ 'change-location-picker__item--disabled': disableSmaller }"
+            @click="onSmallerPick"
+          >
+            换小库位
+          </div>
+        </div>
+      </div>
+    </div>
+  </van-dialog>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+
+const emit = defineEmits(['confirm'])
+
+const visible = ref(false)
+const disableLarger = ref(false)
+const disableSmaller = ref(false)
+
+const onClose = () => {
+  visible.value = false
+}
+
+const closeAndEmit = (payload) => {
+  visible.value = false
+  emit('confirm', payload)
+}
+
+const onSystemPick = () => {
+  closeAndEmit({ mode: 'system', label: '系统推荐' })
+}
+
+const onLargerPick = () => {
+  if (disableLarger.value) return
+  closeAndEmit({ mode: 'larger', label: '换大库位' })
+}
+
+const onSmallerPick = () => {
+  if (disableSmaller.value) return
+  closeAndEmit({ mode: 'smaller', label: '换小库位' })
+}
+
+const show = (options = {}) => {
+  disableLarger.value = !!options.disableLarger
+  disableSmaller.value = !!options.disableSmaller
+  visible.value = true
+}
+
+defineExpose({ show })
+</script>
+
+<style scoped lang="sass">
+.change-location-picker
+  position: relative
+  padding: 4px 4px 8px
+
+  &__header
+    position: relative
+    display: flex
+    align-items: center
+    justify-content: center
+    padding: 8px 40px 0
+    margin-bottom: 24px
+
+  &__title
+    font-size: 16px
+    font-weight: 600
+    color: #323233
+    text-align: center
+
+  &__close
+    position: absolute
+    top: 50%
+    right: 0
+    transform: translateY(-50%)
+    display: flex
+    align-items: center
+    justify-content: center
+    width: 40px
+    height: 40px
+    padding: 0
+    color: #646566
+    background: none
+    border: none
+    cursor: pointer
+    -webkit-tap-highlight-color: transparent
+
+    &:active
+      color: #323233
+
+  &__body
+    display: flex
+    flex-direction: column
+    gap: 20px
+    padding: 10px
+
+  &__row
+    display: flex
+    gap: 20px
+
+  &__item
+    display: flex
+    align-items: center
+    justify-content: center
+    height: 40px
+    font-size: 16px
+    color: #323233
+    background: #fff
+    border: 1px solid #ebedf0
+    border-radius: 8px
+    cursor: pointer
+    -webkit-tap-highlight-color: transparent
+
+    &:active
+      background: #f2f3f5
+
+    &--primary
+      color: #1989fa
+      font-weight: 600
+      background: #f0f7ff
+      border-color: #cce4ff
+
+      &:active
+        background: #e3f0ff
+
+    &--disabled
+      color: #c8c9cc
+      background: #f7f8fa
+      border-color: #ebedf0
+      cursor: not-allowed
+      pointer-events: none
+
+    .change-location-picker__row &
+      flex: 1
+      min-width: 0
+
+:deep(.change-location-dialog-wrap.van-dialog)
+  width: 88%
+  max-width: 320px
+  border-radius: 12px
+  overflow: hidden
+
+  .van-dialog__header
+    display: none
+
+  .van-dialog__content
+    padding: 24px 24px 20px
+
+  .van-dialog__footer
+    display: none
+</style>

+ 0 - 221
src/views/inbound/putaway/components/LocationTypePicker.vue

@@ -1,221 +0,0 @@
-<template>
-  <van-dialog
-    v-model:show="visible"
-    class="location-type-dialog-wrap"
-    :title="description"
-    :show-cancel-button="false"
-    :show-confirm-button="false"
-    :close-on-click-overlay="false"
-  >
-    <div class="location-type-dialog">
-      <div v-if="loading" class="location-type-dialog__loading">
-        <van-loading type="spinner" size="18px" color="#1989fa">加载中</van-loading>
-      </div>
-      <van-radio-group v-else v-model="selectedIndex" class="location-type-dialog__list">
-        <template v-if="fetchedList.length">
-          <label
-            v-for="(item, index) in fetchedList"
-            :key="index"
-            class="location-type-dialog__card"
-            :class="{ 'location-type-dialog__card--active': selectedIndex === index }"
-            @click="selectedIndex = index"
-          >
-            <span class="card-size">宽{{ item.width }} 深{{ item.length }} 高{{ item.height }}</span>
-            <span class="card-qty">数量{{ item.qty }}</span>
-            <van-radio class="card-radio" :name="index" icon-size="18px" />
-          </label>
-        </template>
-        <van-empty v-else class="location-type-dialog__empty" image-size="48" description="暂无数据" />
-      </van-radio-group>
-    </div>
-    <template #footer>
-      <div class="location-type-dialog__footer">
-        <van-button plain type="primary" size="small" @click="onPickAll">全部库位类型</van-button>
-        <van-button type="primary" size="small" @click="onConfirmClick">确定</van-button>
-      </div>
-    </template>
-  </van-dialog>
-</template>
-
-<script setup>
-import { ref } from 'vue'
-import { showToast } from 'vant'
-import { getLocationTypeList } from '@/api/basic/index'
-
-const props = defineProps({
-  description: { type: String, default: '选择库位类型' },
-  warehouse: { type: String, default: '' },
-})
-
-const emit = defineEmits(['confirm', 'cancel', 'loaded'])
-
-const visible = ref(false)
-const loading = ref(false)
-const fetchedList = ref([])
-const selectedIndex = ref(-1)
-const loadedWarehouse = ref('')
-
-const loadTypes = async (warehouse) => {
-  if (!warehouse) {
-    fetchedList.value = []
-    loadedWarehouse.value = ''
-    return
-  }
-  if (loadedWarehouse.value === warehouse) return
-
-  loading.value = true
-  try {
-    const res = await getLocationTypeList({ warehouse })
-    fetchedList.value = res.data || []
-    loadedWarehouse.value = warehouse
-    emit('loaded', fetchedList.value)
-  } catch (e) {
-    console.error(e)
-    fetchedList.value = []
-    loadedWarehouse.value = ''
-    showToast({ duration: 2000, message: '库位类型加载失败' })
-  } finally {
-    loading.value = false
-  }
-}
-
-const clearCache = () => {
-  fetchedList.value = []
-  loadedWarehouse.value = ''
-}
-
-const onPick = (item) => {
-  visible.value = false
-  emit('confirm', item)
-}
-
-const onPickAll = () => {
-  onPick({ value: '', label: '全部库位类型' })
-}
-
-const onConfirmClick = () => {
-  if (selectedIndex.value < 0) {
-    showToast({ duration: 2000, message: '请选择库位类型' })
-    return
-  }
-  const item = fetchedList.value[selectedIndex.value]
-  if (item) onPick({ value: item.typeName, label: item.typeName })
-}
-
-const show = async (warehouse) => {
-  selectedIndex.value = -1
-  visible.value = true
-  await loadTypes(warehouse || props.warehouse)
-}
-
-defineExpose({
-  show,
-  clearCache,
-  reload: () => {
-    loadedWarehouse.value = ''
-    return loadTypes(props.warehouse)
-  },
-})
-</script>
-
-<style scoped lang="sass">
-$active: #f5f9ff
-$border-active: #a8d4ff
-
-.location-type-dialog
-  text-align: left
-
-  &__loading
-    display: flex
-    justify-content: center
-    padding: 12px 0
-
-  &__list
-    max-height: 34vh
-    overflow-y: auto
-    padding: 4px
-    background: #f5f6f8
-    border-radius: 8px
-
-  &__card
-    display: flex
-    align-items: center
-    gap: 8px
-    width: 100%
-    height: 38px
-    margin-bottom: 6px
-    padding: 0 8px 0 12px
-    background: #fff
-    border: 1px solid #e8e8e8
-    border-radius: 6px
-    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04)
-    white-space: nowrap
-    cursor: pointer
-    box-sizing: border-box
-
-    &:last-child
-      margin-bottom: 0
-
-    &--active
-      border-color: $border-active
-      background: $active
-      box-shadow: 0 1px 3px rgba(25, 137, 250, 0.12)
-
-  .card-size
-    flex: 1
-    min-width: 0
-    font-size: 13px
-    color: #323233
-    overflow: hidden
-    text-overflow: ellipsis
-
-  .card-qty
-    flex-shrink: 0
-    font-size: 13px
-    font-weight: 600
-    color: #1989fa
-
-  .card-radio
-    flex-shrink: 0
-    margin-left: auto
-
-    :deep(.van-radio__label)
-      display: none
-
-  &__footer
-    display: flex
-    gap: 8px
-    box-sizing: border-box
-    width: 100%
-    padding: 10px 12px 12px
-
-    :deep(.van-button)
-      flex: 1
-      min-width: 0
-      height: 36px
-      margin: 0
-      padding: 0 4px
-      font-size: 13px
-      border-radius: 8px
-
-      :deep(.van-button__text)
-        overflow: hidden
-        text-overflow: ellipsis
-        white-space: nowrap
-
-:deep(.location-type-dialog-wrap.van-dialog)
-  width: 90%
-  max-width: 340px
-  border-radius: 12px
-  overflow: hidden
-
-  .van-dialog__header
-    padding: 16px 16px 10px
-    font-weight: 600
-
-  .van-dialog__content
-    padding: 0 12px
-
-  .van-dialog__footer
-    padding: 0
-</style>

+ 194 - 70
src/views/inbound/putaway/task/index.vue

@@ -61,29 +61,28 @@
           >
           </van-search>
         </div>
-        <div class="barcode-input">
-          <van-search
-            ref="locationRef"
-            v-model="searchLocation"
-            placeholder="请扫描库位编号"
-            @search="_handlerScan(searchLocation)"
-            label="库位编号:"
-            left-icon=""
-            :class="[scanType===3?'search-input-barcode':'','van-hairline--bottom']"
-            @focus="scanType=3"
-            autocomplete="off"
-          >
-            <template #right-icon>
-              <div
-                v-if="systemForcePublishEnabled && barcodeActiveList.length > 0"
-                class="change-location-btn"
-                :class="{ 'change-location-btn--loading': changeLocationLoading }"
-                @click.stop="onChangeLocation"
-              >
-                换一换
-              </div>
-            </template>
-          </van-search>
+        <div class="barcode-input barcode-input--location">
+          <div class="location-row">
+            <van-search
+              ref="locationRef"
+              v-model="searchLocation"
+              placeholder="请扫描库位编号"
+              @search="_handlerScan(searchLocation)"
+              label="库位编号:"
+              left-icon=""
+              :class="[scanType === 3 ? 'search-input-barcode' : '', 'location-search']"
+              @focus="scanType=3"
+              autocomplete="off"
+            />
+            <div
+              v-show="systemForcePublishEnabled && barcodeActiveList.length > 0"
+              class="change-location-btn"
+              :class="{ 'change-location-btn--loading': changeLocationLoading }"
+              @click.stop="onOpenChangeLocation"
+            >
+              换一换
+            </div>
+          </div>
         </div>
         <div class="barcode-input">
           <van-search
@@ -167,24 +166,19 @@
       <van-cell title="叠装区" @click="onSelectLocationType('DZ')" />
     </van-cell-group>
   </van-action-sheet>
-  <!--  库位类型-->
-  <location-type-picker
-    ref="changeLocationTypePickerRef"
-    :warehouse="warehouse"
-    description="选择库位类型"
-    @confirm="doChangeLocationWithType"
-  />
+  <!--  换一换-->
+  <change-location-picker ref="changeLocationPickerRef" @confirm="doChangeLocation" />
   <!--  组合商品上架数量-->
   <barcode-combine ref="barcodeCombineRef" @setCombine="setPutawayCombine" @cancel="onCombineCancel" :matched-sku="combineMatchedSku" />
 </template>
 
 <script setup>
-import { onMounted, onUnmounted, ref, computed } from 'vue'
+import { onMounted, onUnmounted, ref, computed, nextTick } from 'vue'
 import { androidFocus, getHeader, goBack, scanError, scanSuccess } from '@/utils/android'
 import InputBarcode from '@/views/outbound/picking/components/InputBarcode.vue'
 import LocationList from '@/views/inbound/putaway/components/LocationList.vue'
 import BarcodeCombine from '@/views/inbound/putaway/components/BarcodeCombine.vue'
-import LocationTypePicker from '@/views/inbound/putaway/components/LocationTypePicker.vue'
+import ChangeLocationPicker from '@/views/inbound/putaway/components/ChangeLocationPicker.vue'
 import { openListener,closeListener,scanInit } from '@/utils/keydownListener.js'
 import { useRouter } from 'vue-router'
 import { closeLoading, showLoading } from '@/utils/loading'
@@ -273,6 +267,55 @@ const isLocationInRecommendedList = (locationCode) => {
   return isScannedLocationInRecommendedList(locationCode)
 }
 
+// 库位类型体积(如 0.3-0.4-0.3,三数相乘区分大小)
+const calcLocationTypeVolume = (typeStr) => {
+  if (!typeStr) return 0
+  const parts = String(typeStr).split('-').map(Number)
+  if (parts.length !== 3 || parts.some(n => Number.isNaN(n))) return 0
+  return parts[0] * parts[1] * parts[2]
+}
+
+const getItemLocationTypeName = (item) => item?.locationType || item?.typeName || ''
+
+// 从强制/可选库位中取最大或最小库位类型
+const pickLocationTypeFromList = (larger) => {
+  const types = [...new Set(
+    locationList.value
+      .filter(item => item.displayType === 'FORCE' || item.displayType === 'OPTIONAL')
+      .map(getItemLocationTypeName)
+      .filter(Boolean),
+  )]
+  if (!types.length) return ''
+  types.sort((a, b) => calcLocationTypeVolume(a) - calcLocationTypeVolume(b))
+  return larger ? types[types.length - 1] : types[0]
+}
+
+// 上次换一换方向:true 换大的,false 换小的
+const lastChangeLocationDirection = ref(null)
+// 上次换一换使用的库位类型(如 0.3-0.4-0.3)
+const lastChangeLocationType = ref('')
+//同步上次换一换使用的库位类型
+const syncLastChangeLocationTypeFromList = () => {
+  const type = pickLocationTypeFromList(true) || pickLocationTypeFromList(false)
+  if (type) lastChangeLocationType.value = type
+}
+//换一换使用的库位类型(换大的或换小的)
+const resolveChangeLocationType = (larger) => {
+  const fromList = pickLocationTypeFromList(larger)
+  if (fromList) return fromList
+  if (larger && lastChangeLocationType.value) return lastChangeLocationType.value
+  return ''
+}
+//换一换使用的库位类型(换大的或换小的)
+const resolveOverflowLocationType = () => {
+  return pickLocationTypeFromList(true) || pickLocationTypeFromList(false) || lastChangeLocationType.value || ''
+}
+
+const resetChangeLocationDirection = () => {
+  lastChangeLocationDirection.value = null
+  lastChangeLocationType.value = ''
+}
+
 // 从已排除库位记录中提取 locationId 列表(换一换时使用,INVENTORY 类型不排除)
 const getExcludedLocationIds = (list) => {
   const ids = [...new Set(list.map(loc => {
@@ -395,7 +438,6 @@ const setBarcode = (code, type) => {
     formattedTime.value = '00:00:00'
     totalSeconds.value = 0
     locationType.value = ''
-    changeLocationTypePickerRef.value?.clearCache()
   }
   const params = { warehouseId:warehouse, containerId: code }
   getWaitPutawayListNew(params).then(res => {
@@ -502,8 +544,8 @@ const switchTask = () => {
   inputBarcodeType.value = 'switchTask'
   back.value = false
   if (systemForcePublishEnabled.value) excludedLocations.value = []
+  resetChangeLocationDirection()
   locationType.value = ''
-  changeLocationTypePickerRef.value?.clearCache()
   inputBarcodeRef.value?.show('', `请扫描容器号`, '')
 }
 
@@ -522,7 +564,6 @@ const changeLocationLoading = ref(false)
 // 上架区域,默认全部
 const locationType = ref('')
 const locationTypeSheetShow = ref(false)
-const changeLocationTypePickerRef = ref(null)
 const locationTypeLabel = computed(() => {
   const map = { PICKING: '拣货位', STORAGE: '存储位', RETURN: '退货区', HANGING: '挂装区',ZZ: '周转区', DZ: '叠装区'
 }
@@ -534,6 +575,7 @@ const onSelectLocationType = async (value = '') => {
   locationTypeSheetShow.value = false
   if (value !== prev && barcodeActiveList.value.length > 0) {
     excludedLocations.value = []
+    resetChangeLocationDirection()
     await _getRecommendedLocation(barcodeActiveList.value[0])
   }
 }
@@ -547,6 +589,7 @@ const reset = () => {
   putawayCombineData.value = null
   combineMatchedSku.value = []
   excludedLocations.value = []
+  resetChangeLocationDirection()
   forbidForcePutaway.value = true // 重置为默认不强制,待下次推荐接口返回后再更新
   locationType.value = ''
 }
@@ -630,6 +673,7 @@ const _handlePutawayCombineProduct = (code) => {
 const _handlerScan = (code) => {
   if (scanType.value === SCAN_TYPE.BARCODE) {
     excludedLocations.value = []
+    resetChangeLocationDirection()
     searchBarcode.value = code
     oldSearchBarcode.value = code
     lotBarcodeList.value = matchingBarcodeItem(dataMap.value, code)
@@ -648,7 +692,7 @@ const _handlerScan = (code) => {
     _handleLocationScan(code)
   }
 }
-
+//库位扫描
 const _handleLocationScan = async (code) => {
   const scannedLocation = barcodeToUpperCase(code)
   if (!isLocationInRecommendedList(scannedLocation)) {
@@ -657,6 +701,7 @@ const _handleLocationScan = async (code) => {
     scanError()
     return
   }
+  //库位校验
   if (!isScannedLocationInRecommendedList(scannedLocation)) {
     showLoading()
     const valid = await validateCustomPutawayLocation(scannedLocation)
@@ -690,19 +735,27 @@ const _getRecommendedLocation = async (item, options = {}) => {
   const uniqueLocationIds = fromChangeLocation ? getExcludedLocationIds(excludedLocations.value) : undefined
   try {
     const total = barcodeQuantity(barcodeActiveList.value)
-    const statisticsKey=barcodeActiveList.value[0].taskNo || barcodeActiveList.value[0].businessNo +'_'+barcodeActiveList.value[0].lotNumber
     const params = { warehouse, lotNum: lotNumber, owner, sku, qty: total, lotAtt08}
     if (locationType.value) params.locationUse = locationType.value
-    if (options.changeTypeName) params.locationType = options.changeTypeName
-    if (fromChangeLocation && uniqueLocationIds) {
-      params.excludedLocations = uniqueLocationIds
-      params.statisticsKey=statisticsKey
+    //换一换参数
+    if (fromChangeLocation) {
+      const { taskNo, businessNo, lotNum } = item
+      params.statisticsKey = `${taskNo || businessNo || ''}_${lotNumber || lotNum || ''}`
+      if (options.changeLocationType) params.locationType = options.changeLocationType
+      if (options.largerLocationType !== undefined && options.largerLocationType !== null) {
+        params.largerLocationType = options.largerLocationType
+      }
+      //换一换排除库位
+      if (uniqueLocationIds) {
+        params.excludedLocations = uniqueLocationIds
+      }
     }
     const res = await getRecommendedLocationNew(params)
     if (res.data) {
       forbidForcePutaway.value = res.data.forbidForcePutaway // 货主策略:是否禁止强制上架
       const loc = res.data.locationList
       locationList.value = loc
+      syncLastChangeLocationTypeFromList()
       searchCount.value = 1
     }
   } catch (err) {
@@ -713,21 +766,67 @@ const _getRecommendedLocation = async (item, options = {}) => {
   }
 }
 
-// 换一换:先选库位类型,再请求新的推荐库位
-const onChangeLocation = () => {
-  if (barcodeActiveList.value.length === 0) return
-  changeLocationTypePickerRef.value?.show(warehouse)
+// 换一换
+const changeLocationPickerRef = ref(null)
+//换一换禁用状态
+const getChangeLocationDisabledState = () => {
+  const noLargerType = !pickLocationTypeFromList(true)
+  const noSmallerType = !pickLocationTypeFromList(false)
+  return {
+    disableLarger: noLargerType && lastChangeLocationDirection.value === true,
+    disableSmaller: noSmallerType && lastChangeLocationDirection.value === false,
+  }
+}
+//换一换打开
+const onOpenChangeLocation = () => {
+  if (barcodeActiveList.value.length === 0 || changeLocationLoading.value) return
+  changeLocationPickerRef.value?.show(getChangeLocationDisabledState())
 }
-const doChangeLocationWithType = async (item) => {
+//换一换确认
+const doChangeLocation = async ({ mode }) => {
   if (barcodeActiveList.value.length === 0) return
+
+  const reqOpts = { fromChangeLocation: true }
+
+  if (mode === 'larger') {
+    if (getChangeLocationDisabledState().disableLarger) return
+    const changeLocationType = resolveChangeLocationType(true)
+    if (!changeLocationType) {
+      showToast({ duration: 2000, message: '当前无可用库位类型' })
+      return
+    }
+    reqOpts.largerLocationType = true
+    reqOpts.changeLocationType = changeLocationType
+  } else if (mode === 'smaller') {
+    if (getChangeLocationDisabledState().disableSmaller) return
+    const changeLocationType = resolveChangeLocationType(false)
+    if (!changeLocationType) {
+      showToast({ duration: 2000, message: '当前无可用库位类型' })
+      return
+    }
+    reqOpts.largerLocationType = false
+    reqOpts.changeLocationType = changeLocationType
+  } else if (mode === 'overflow') {
+    const changeLocationType = resolveOverflowLocationType()
+    if (!changeLocationType) {
+      showToast({ duration: 2000, message: '当前无可用库位类型' })
+      return
+    }
+    reqOpts.changeLocationType = changeLocationType
+  }
+
   changeLocationLoading.value = true
   try {
+    //换一换获取推荐库位
     const batchItem = barcodeActiveList.value[0]
     if (locationList.value?.length) {
       excludedLocations.value = [...excludedLocations.value, ...locationList.value]
     }
-    const reqOpts = { fromChangeLocation: true }
-    if (item?.value) reqOpts.changeTypeName = item.value
+    if (reqOpts.changeLocationType) {
+      lastChangeLocationType.value = reqOpts.changeLocationType
+    }
+    if (mode === 'larger') lastChangeLocationDirection.value = true
+    else if (mode === 'smaller') lastChangeLocationDirection.value = false
     await _getRecommendedLocation(batchItem, reqOpts)
   } finally {
     changeLocationLoading.value = false
@@ -735,10 +834,20 @@ const doChangeLocationWithType = async (item) => {
 }
 const numberRef = ref(null)
 const locationRef = ref(null)
+
+const focusField = async (fieldRef) => {
+  await nextTick()
+  try {
+    fieldRef.value?.focus?.()
+  } catch (e) {
+    console.error(e)
+  }
+}
+
 // 完成收货校验
 const isCheck = async () => {
   if (searchLocation.value == '') {
-    locationRef.value?.focus()
+    await focusField(locationRef)
     scanError()
     showToast({ duration: 3000, message: '请先扫描库位编号' })
     return false
@@ -750,7 +859,7 @@ const isCheck = async () => {
   }
   // 扫描库位与提交上架共用同一套强制匹配规则
   if (!isLocationInRecommendedList(searchLocation.value)) {
-    locationRef.value?.focus()
+    await focusField(locationRef)
     scanError()
     showToast({ duration: 3000, message: '库位与推荐库位不一致,无法上架' })
     return false
@@ -761,20 +870,20 @@ const isCheck = async () => {
     closeLoading()
     if (!valid) {
       searchLocation.value = ''
-      locationRef.value?.focus()
+      await focusField(locationRef)
       scanError()
       return false
     }
   }
   if (searchCount.value == '') {
-    numberRef.value?.focus()
+    await focusField(numberRef)
     scanError()
     showToast({ duration: 3000, message: '请先输入上架数量' })
     return false
   }
   const maxQuantity = barcodeQuantity(barcodeActiveList.value)
   if (Number(searchCount.value) > maxQuantity) {
-    numberRef.value?.focus()
+    await focusField(numberRef)
     scanError()
     showToast({ duration: 3000, message: `上架数量最大为:${maxQuantity}` })
     return false
@@ -936,6 +1045,38 @@ window.onRefresh = loadData
         display: flex
         align-items: center
 
+      &--location
+        background: #fff
+
+        .location-row
+          display: flex
+          align-items: center
+          height: 50px
+
+          .location-search
+            flex: 1
+            min-width: 0
+
+        .change-location-btn
+          display: inline-flex
+          align-items: center
+          justify-content: center
+          flex-shrink: 0
+          height: 22px
+          margin-right: 8px
+          padding: 0 8px
+          font-size: 12px
+          color: #1989fa
+          border: 1px solid #1989fa
+          border-radius: 999px
+          white-space: nowrap
+          cursor: pointer
+          -webkit-tap-highlight-color: transparent
+
+          &--loading
+            opacity: 0.6
+            pointer-events: none
+
       ::v-deep(.van-search__field)
         border-bottom: 2px solid #ffffff
         display: flex
@@ -967,23 +1108,6 @@ window.onRefresh = loadData
           font-weight: bold
           color: #ee0a25
 
-      .change-location-btn
-        display: inline-flex
-        align-items: center
-        justify-content: center
-        height: 22px
-        padding: 0 8px
-        font-size: 12px
-        color: #1989fa
-        border: 1px solid #1989fa
-        border-radius: 999px
-        white-space: nowrap
-        flex-shrink: 0
-
-        &--loading
-          opacity: 0.6
-          pointer-events: none
-
   .take-lot
     text-align: left
     margin-top: 5px