Browse Source

Merge branch 'zhaohuanhuan_returnTask' into zhaohuanhuan_asrs-v4

# Conflicts:
#	src/views/outbound/picking/list/index.vue
zhaohuanhuan 2 months ago
parent
commit
652dd40382

+ 1 - 0
package.json

@@ -13,6 +13,7 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "@ericblade/quagga2": "^1.12.1",
     "axios": "^1.7.9",
     "lib-flexible": "^0.3.2",
     "pinia": "^2.3.0",

+ 5 - 1
src/api/inbound/index.ts

@@ -5,11 +5,15 @@ import request from '@/utils/request'
  * 面单识别OCR - 上传图片进行识别
  * @param file 图片文件
  * @param warehouse 仓库编码
+ * @param barcode 识别到的条码(可选)
  */
-export function uploadOCRImage(file: File, warehouse: string) {
+export function uploadOCRImage(file: File, warehouse: string, barcode?: string) {
   const formData = new FormData()
   formData.append('file', file)
   formData.append('warehouse', warehouse)
+  if (barcode) {
+    formData.append('barcode', barcode)
+  }
   
   return request({
     url: '/api/entryOrder/OCR/upload',

+ 89 - 22
src/api/location/merge.ts

@@ -3,25 +3,27 @@ import request from '@/utils/request'
 
 // 库位合并详情
 export interface LocationMergeDetails {
-  id: number
-  code: string
-  warehouse: string
-  sourceLocation: string
-  targetLocation: string
-  sourceBox: string
-  targetBox: string
-  status: string
-  quantity: number
-  moveQty: number
-  sku: string
-  barcode: string
-  productName: string
-  lotNumber: string
-  owner: string
-  qualityStatus: string
-  warehouseType: string
-  productionDate: string
-  expiryDate: string
+  id?: number
+  code?: string
+  warehouse?: string
+  sourceLocation?: string
+  targetLocation?: string
+  sourceBox?: string
+  targetBox?: string
+  status?: string
+  quantity?: number
+  moveQty?: number
+  sku?: string
+  barcode?: string
+  productName?: string
+  lotNumber?: string
+  lotNum?: string
+  owner?: string
+  qualityStatus?: string
+  warehouseType?: string
+  productionDate?: string
+  expiryDate?: string
+  involveBox?: string[]
 }
 
 // 料箱相关的并库详情VO
@@ -58,15 +60,42 @@ export function getBoxSplitCode(warehouse: string, locations: string[]) {
   })
 }
 
+/**
+ * 料箱回库结果
+ */
+export interface BoxInboundResult {
+  t1: string // 料箱编码
+  t2: string // 执行结果消息
+}
+
 /**
  * 执行料箱回库(呼唤机器人)
  * @param warehouse 仓库
+ * @param boxCodes 可选的料箱编码列表,如果不传则回库所有料箱
  */
-export function boxInbound(warehouse: string) {
-  return request<[string, string][]>({
+export function boxInbound(warehouse: string, boxCodes?: string[]): Promise<BoxInboundResult[]> {
+  const params: any = { warehouse }
+  if (boxCodes && boxCodes.length > 0) {
+    params.boxCodes = boxCodes
+  }
+
+  return request<BoxInboundResult[]>({
     url: '/api/wms/location/merge/boxInbound',
     method: 'post',
-    params: { warehouse }
+    params,
+    paramsSerializer: (params: any) => {
+      const searchParams = new URLSearchParams()
+      Object.keys(params).forEach(key => {
+        const value = params[key]
+        if (Array.isArray(value)) {
+          // Spring @RequestParam List 期望格式: key=v1&key=v2
+          value.forEach(item => searchParams.append(key, item))
+        } else {
+          searchParams.append(key, value)
+        }
+      })
+      return searchParams.toString()
+    }
   })
 }
 
@@ -109,3 +138,41 @@ export function getBoxStatus(warehouse: string, boxCodeList: string[]) {
     }
   })
 }
+
+/**
+ * 获取正在作业中的并库任务详情
+ * @param warehouse 仓库
+ */
+export function getWorkingMergeTasks(warehouse: string) {
+  return request<LocationMergeDetails[]>({
+    url: '/api/wms/location/merge/workingTasks',
+    method: 'get',
+    params: { warehouse }
+  })
+}
+
+/**
+ * 强制完成料箱任务
+ * @param warehouse 仓库
+ * @param stationCode 站点编码
+ * @param boxCode 料箱编码
+ */
+export function forceCompleteBoxTask(warehouse: string, stationCode: string, boxCode: string | null) {
+  return request<boolean>({
+    url: '/api/wms/location/merge/forceCompleteBoxTask',
+    method: 'get',
+    params: { warehouse, stationCode, boxCode }
+  })
+}
+
+/**
+ * 海康-站点解绑
+ * @param data
+ */
+export function boxAndStationUnbindTask(params:any) {
+  return request({
+    url: '/api/wms/location/merge/boxAndStationUnbindTask',
+    method: 'post',
+    params
+  })
+}

+ 74 - 0
src/api/returnTask/index.ts

@@ -0,0 +1,74 @@
+// @ts-ignore
+import request from '@/utils/request'
+// @ts-ignore
+import {createReturnFirstSeedType, createReturnTaskType, getReturnTaskBinListType, getReturnTaskPageType, returnTaskFirstStepCompleteType, updateReturnTaskUsedQtyType} from '@/types/returnTask'
+
+//分页查询还库任务列表
+export function getReturnTaskPage(params:getReturnTaskPageType) {
+  return request({
+    url: '/api/wms/return/task/page',
+    method: 'get',
+    params
+  })
+}
+
+//分页查询还库任务列表
+export function createReturnTask(data:createReturnTaskType) {
+  return request({
+    url: '/api/wms/return/task/create',
+    method: 'post',
+    data
+  })
+}
+
+
+//还库分配格口
+export function createReturnFirstSeed(data:createReturnFirstSeedType) {
+  return request({
+    url: '/api/wms/return/task/firstSeed',
+    method: 'post',
+    data
+  })
+}
+
+
+
+//查询还库分配格口数据
+export function getReturnTaskBinList(params:getReturnTaskBinListType) {
+  return request({
+    url: '/api/wms/return/task/item/list',
+    method: 'get',
+    params
+  })
+}
+
+
+
+//放弃任务
+export function returnTaskAbandon(data:getReturnTaskBinListType) {
+  return request({
+    url: '/api/wms/return/task/abandon',
+    method: 'post',
+    data
+  })
+}
+
+//完成分配格口和呼叫校车
+export function returnTaskFirstStepComplete(data:returnTaskFirstStepCompleteType) {
+  return request({
+    url: '/api/wms/return/task/firstStepComplete',
+    method: 'post',
+    data
+  })
+}
+
+//更新还库任务已使用数量
+export function updateReturnTaskUsedQty(data:updateReturnTaskUsedQtyType) {
+  return request({
+    url: '/api/wms/return/task/item/updateUsedQty',
+    method: 'post',
+    data
+  })
+}
+
+

+ 5 - 0
src/hooks/basic/menu.js

@@ -79,6 +79,11 @@ export default function() {
               icon: 'newspaper-o',
               path: 'inventory-transfer',
             },
+            {
+              title: '还库任务',
+              icon: 'newspaper-o',
+              path: 'return-list',
+            },
           ],
         },
         {

+ 18 - 0
src/router/index.ts

@@ -176,6 +176,24 @@ const routes: RouteRecordRaw[] = [
     meta:{title:'面单识别'},
     component: () => import('@/views/inbound/photoOCR/index.vue')
   },
+  {
+    path: '/return-list',
+    name: 'ReturnList',
+    meta:{title:'还库列表'},
+    component: () => import('@/views/inventory/returnTask/list/index.vue')
+  },
+  {
+    path: '/return-bin',
+    name: 'ReturnBin',
+    meta:{title:'还库任务-分格口'},
+    component: () => import('@/views/inventory/returnTask/bin/index.vue')
+  },
+  {
+    path: '/return-task',
+    name: 'ReturnTask',
+    meta:{title:'还库任务'},
+    component: () => import('@/views/inventory/returnTask/task/index.vue')
+  },
 ];
 
 // 创建路由实例

+ 97 - 0
src/types/returnTask.ts

@@ -0,0 +1,97 @@
+
+/**
+ * 分页查询还库任务列表
+ */
+export interface getReturnTaskPageType {
+  page: number;
+  size: number;
+  warehouse: string;
+  /**
+   * 状态
+   */
+  status?: string;
+  /**
+   * 任务号
+   */
+  taskNo?: string;
+  [property: string]: any;
+}
+
+/**
+ * 创建还库任务
+ */
+export interface createReturnTaskType {
+  /**
+   * 容器号
+   */
+  containerCode: string;
+  /**
+   * 仓库编码
+   */
+  warehouse: string;
+  [property: string]: any;
+}
+
+/**
+ * 创建还库任务
+ */
+export interface createReturnFirstSeedType {
+  /**
+   * 商品条码
+   */
+  barcode: string;
+  /**
+   * 库位容器号
+   */
+  containerCode: string;
+  /**
+   * 商品批次号
+   */
+  lotNum: string;
+  /**
+   * 任务号
+   */
+  taskNo: string;
+  [property: string]: any;
+}
+
+//查询还库分配格口数据
+export interface getReturnTaskBinListType {
+  taskNo: string;
+  [property: string]: any;
+}
+
+//完成分配格口和呼叫校车
+export interface returnTaskFirstStepCompleteType {
+  /**
+   * 是否调用海康快上
+   */
+  callHikQuickIn: boolean;
+  /**
+   * 任务号
+   */
+  taskNo: string;
+  [property: string]: any;
+}
+
+/**
+ * 更新还库任务已使用数量
+ */
+export interface updateReturnTaskUsedQtyType {
+  /**
+   * 批次号
+   */
+  lotNum: string;
+  /**
+   * 本次移库数量
+   */
+  moveQty: number;
+  /**
+   * 任务号
+   */
+  taskNo: string;
+  [property: string]: any;
+}
+
+
+

+ 171 - 7
src/views/inbound/photoOCR/index.vue

@@ -30,6 +30,22 @@
             <van-cell title="当前仓库" :value="warehouse" />
           </div>
 
+          <!-- 快递单号输入框 -->
+          <div class="express-input-box">
+            <div class="express-input-text">
+              <div>快递单号</div>
+            </div>
+            <van-field 
+              class="express-input" 
+              ref="expressNoRef"
+              :style="expressNo!==''?'border: 2px solid #07c160':''" 
+              clearable 
+              v-model="expressNo"
+              placeholder="识别完成后自动填充"
+              readonly
+            />
+          </div>
+
           <!-- 拍照上传区域 -->
           <div class="upload-section">
             <div class="upload-tips">
@@ -75,9 +91,10 @@
 import { ref, onMounted } from 'vue'
 import { showNotify, showFailToast, showLoadingToast, closeToast } from 'vant'
 import { uploadOCRImage } from '@/api/inbound/index'
-import { getHeader, goBack } from '@/utils/android'
+import { getHeader, goBack, scanSuccess, scanError } from '@/utils/android'
 import { compressImage } from '@/utils/imageCompression'
 import { useStore } from '@/store/modules/user'
+import Quagga from '@ericblade/quagga2'
 
 try {
   getHeader()
@@ -90,9 +107,12 @@ const warehouse = store.warehouse
 
 const uploadImages = ref([])
 const uploading = ref(false)
+const expressNo = ref('') // 快递单号
+const expressNoRef = ref(null)
+const recognizing = ref(false) // 条码识别中
 
 // 自动上传图片
-const autoUploadImage = async (file) => {
+const autoUploadImage = async (file, barcode = null) => {
   if (!warehouse) {
     showFailToast('未获取到仓库信息')
     return
@@ -108,13 +128,13 @@ const autoUploadImage = async (file) => {
   const toast = showLoadingToast('自动上传识别中...')
 
   try {
-    const response = await uploadOCRImage(file, warehouse)
+    const response = await uploadOCRImage(file, warehouse, barcode)
     
     closeToast()
     
     if (response.code === 200) {
       showNotify({ type: 'success', message: '面单上传成功' })
-      // 上传成功后重置表单
+      // 上传成功后重置表单(但保留快递单号)
       uploadImages.value = []
     } else {
       showNotify({ type: 'danger', message: response.message || '上传失败' })
@@ -149,6 +169,112 @@ const beforeReadImage = (file) => {
   return true
 }
 
+// 使用 Quagga2 识别图片中的条码(Code128)
+// 注意:decodeSingle 只能识别一个条码,如果图片中有多个条码,会识别到第一个找到的
+const recognizeBarcode = async (file) => {
+  return new Promise((resolve) => {
+    try {
+      recognizing.value = true
+      const imageFile = file
+      
+      // 创建图片对象
+      const img = new Image()
+      const url = URL.createObjectURL(imageFile)
+      
+      img.onload = () => {
+        try {
+          // 创建canvas
+          const canvas = document.createElement('canvas')
+          const ctx = canvas.getContext('2d')
+          canvas.width = img.width
+          canvas.height = img.height
+          ctx.drawImage(img, 0, 0)
+          
+          // 使用 Quagga2 识别条码(优先识别 Code128 格式)
+          Quagga.decodeSingle(
+            {
+              decoder: {
+                readers: ['code_128_reader'] // 优先识别 Code128 格式(快递单号通常是 Code128)
+              },
+              locate: true,
+              src: canvas.toDataURL(),
+              numOfWorkers: 0 // 不使用 Web Workers,避免兼容性问题
+            },
+            (result) => {
+              if (result && result.codeResult && result.codeResult.code) {
+                const barcodeText = result.codeResult.code
+                URL.revokeObjectURL(url)
+                recognizing.value = false
+                console.log('识别到条码(Code128):', barcodeText)
+                scanSuccess()
+                resolve(barcodeText)
+              } else {
+                // 如果 Code128 失败,尝试所有格式
+                Quagga.decodeSingle(
+                  {
+                    decoder: {
+                      readers: [
+                        'code_128_reader',
+                        'ean_reader',
+                        'ean_8_reader',
+                        'code_39_reader',
+                        'code_39_vin_reader',
+                        'codabar_reader',
+                        'upc_reader',
+                        'upc_e_reader',
+                        'i2of5_reader'
+                      ]
+                    },
+                    locate: true,
+                    src: canvas.toDataURL(),
+                    numOfWorkers: 0
+                  },
+                  (result) => {
+                    URL.revokeObjectURL(url)
+                    recognizing.value = false
+                    
+                    if (result && result.codeResult && result.codeResult.code) {
+                      const barcodeText = result.codeResult.code
+                      console.log('识别到条码(其他格式):', barcodeText, '格式:', result.codeResult.format)
+                      scanSuccess()
+                      resolve(barcodeText)
+                    } else {
+                      console.log('条码识别失败')
+                      scanError()
+                      resolve(null)
+                    }
+                  }
+                )
+              }
+            }
+          )
+        } catch (error) {
+          URL.revokeObjectURL(url)
+          recognizing.value = false
+          console.error('条码识别过程出错:', error)
+          scanError()
+          resolve(null)
+        }
+      }
+      
+      img.onerror = () => {
+        URL.revokeObjectURL(url)
+        recognizing.value = false
+        console.error('图片加载失败')
+        scanError()
+        resolve(null)
+      }
+      
+      img.src = url
+    } catch (error) {
+      recognizing.value = false
+      console.error('条码识别初始化失败:', error)
+      scanError()
+      resolve(null)
+    }
+  })
+}
+
 // 图片读取完成后处理
 const afterReadImage = async (file) => {
   try {
@@ -162,6 +288,26 @@ const afterReadImage = async (file) => {
       if (originalFile) {
         let finalFile = originalFile
         
+        // 先尝试识别条码
+        showNotify({ 
+          type: 'primary', 
+          message: '正在识别条码...' 
+        })
+        const barcodeResult = await recognizeBarcode(originalFile)
+        if (barcodeResult) {
+          expressNo.value = barcodeResult
+          showNotify({ 
+            type: 'success', 
+            message: `识别到快递单号: ${barcodeResult}` 
+          })
+        } else {
+          expressNo.value = ''
+          showNotify({ 
+            type: 'warning', 
+            message: '未识别到条码,请确保图片清晰' 
+          })
+        }
+        
         // 检查文件大小,如果大于1MB则进行压缩
         if (originalFile.size > 1 * 1024 * 1024) {
           showNotify({ 
@@ -196,8 +342,8 @@ const afterReadImage = async (file) => {
           console.log(`图片大小符合要求,无需压缩: ${(originalFile.size / 1024).toFixed(2)}KB`)
         }
         
-        // 压缩完成后自动上传
-        await autoUploadImage(finalFile)
+        // 压缩完成后自动上传(带上识别到的条码)
+        await autoUploadImage(finalFile, expressNo.value || null)
       }
     }
   } catch (error) {
@@ -236,7 +382,7 @@ const submitOCR = async () => {
     formData.append('file', image.file)
     formData.append('warehouse', warehouse)
     
-    const response = await uploadOCRImage(image.file, warehouse)
+    const response = await uploadOCRImage(image.file, warehouse, expressNo.value || undefined)
     
     closeToast()
     
@@ -290,6 +436,24 @@ onMounted(() => {
   border-radius: 8px
   overflow: hidden
 
+.express-input-box
+  margin-bottom: 20px
+  padding: 10px 5px
+  .express-input-text
+    display: flex
+    justify-content: space-between
+    align-items: center
+    font-size: 18px
+    font-weight: bold
+    margin: 10px 0
+    padding: 5px 0
+  .express-input
+    background: #eff0f2
+    padding: 10px 20px
+    font-size: 20px
+    border: 2px solid #0077ff
+    font-weight: 500
+
 .upload-section
   background: #fff
   padding: 20px

+ 2 - 0
src/views/inbound/takeDelivery/task/index.vue

@@ -684,6 +684,7 @@ const onConfirm = () => {
   if(isCheck()){
     const lotMap = toMap(lotData.value, 'field', 'mapping')
     const { asnLineNo, asnNo, warehouse,customerId } = asnInfo.value
+    const {taskNo: taskCode } = taskInfo.value
     const data = {
       asnLineNo,
       asnNo,
@@ -693,6 +694,7 @@ const onConfirm = () => {
       customerId,
       serialNos: uniqueCodeList.value.length > 0 ? uniqueCodeList.value : undefined,
       ...lotMap,
+      taskNo:taskCode
     }
     showLoading()
     inputBarcodeType.value='task'

+ 1 - 0
src/views/inventory/moveTask/putaway/index.vue

@@ -318,6 +318,7 @@ const _moveTaskDown=()=>{
 }
 const setBarcode=(item)=>{
   activeBarcode.value=item
+  searchBarcode.value=item.barcode
   lotBarcodeTrueFalseBy.value=false;
   _getLocation(item)
 }

+ 546 - 0
src/views/inventory/returnTask/bin/index.vue

@@ -0,0 +1,546 @@
+<template>
+  <div class="container">
+    <van-nav-bar
+        title="还库任务-分格口"
+        left-arrow
+        fixed
+        placeholder
+        @click-left="onClickLeft"
+    >
+      <template #left>
+        <van-icon name="arrow-left" size="25"/>
+        <div style="color: #fff">返回</div>
+      </template>
+      <template #right>
+        <div style="color: #fff;line-height: 46px " @click="onComplete">完成</div>
+        <div style="padding:14px 0 12px 6px" @click="onClickRightIcon">
+          <van-icon name="list-switch" size="25"/>
+        </div>
+      </template>
+    </van-nav-bar>
+    <div class="move-stock">
+      <div class="code">
+        <div class="code-title">
+          <div>{{ containerNo || '' }}</div>
+          <div class="code-tips">
+            <van-notice-bar :background="'none'" :speed="50" :text="tips"/>
+          </div>
+        </div>
+        <div class="code-input">
+          <van-search
+              ref="searchRef"
+              v-model="searchBarcode"
+              placeholder="请扫描商品条码"
+              @search="_handlerScan(searchBarcode)"
+              label="商品条码:"
+              left-icon=""
+              :class="[scanType===2?'search-input-barcode':'','van-hairline--bottom']"
+              @focus="scanType=2"
+              autocomplete="off"
+          >
+          </van-search>
+        </div>
+      </div>
+      <div class="move-stock-content">
+        <table class="compact-table">
+          <colgroup>
+            <col v-for="i in 10" style="width: 10%;">
+          </colgroup>
+          <tbody>
+          <tr>
+            <td colspan="3">格口</td>
+            <td colspan="7">{{ bin }}</td>
+          </tr>
+          </tbody>
+        </table>
+      </div>
+      <div class="move-stock-list">
+        <table class="task-table">
+          <thead>
+          <tr>
+            <th>商品条码</th>
+            <th>商品名称</th>
+            <th>格口号</th>
+            <th>数量</th>
+          </tr>
+          </thead>
+          <tbody>
+          <tr v-for="(item, index) in binList" :key="index" v-if="binList.length>0">
+            <td>{{ item.barcode }}</td>
+            <td>{{ item.skuName }}</td>
+            <td>{{ item.slot }}</td>
+            <td>{{ item.qty }}</td>
+          </tr>
+          <tr v-else>
+            <td colspan="4">
+              <van-empty :image="nodataUrl" image-size="120"/>
+            </td>
+          </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+    <!-- 弹出框 -->
+    <van-action-sheet
+        v-model:show="modeTrueFalseBy"
+        :actions="actions"
+        cancel-text="取消"
+        close-on-click-action
+        @select="onSelectMode"
+    />
+    <!--  单据选择-->
+    <van-action-sheet v-model:show="lotBarcodeTrueFalseBy" cancel-text="取消" description="请选择商品批次"
+                      close-on-click-action>
+      <van-cell-group>
+        <van-cell v-for="item in lotBarcodeList"
+                  @click="_getRecommendedLocation(item.lotNum,item.customerId);barcodeActive=item;lotBarcodeTrueFalseBy=false">
+          <template #title>
+            {{ item.sku }}({{ item.availableQty }}件)
+          </template>
+          <template #label>
+            生产日期:{{ item.productionDate || '--' }}-失效日期:{{ item.expirationDate || '--' }}
+          </template>
+        </van-cell>
+      </van-cell-group>
+    </van-action-sheet>
+  </div>
+</template>
+
+<script setup>
+import {onMounted, onUnmounted, ref} from 'vue'
+import {useRouter, useRoute} from 'vue-router'
+import {useStore} from '@/store/modules/user'
+import {androidFocus, getHeader, goBack, scanError, scanSuccess} from '@/utils/android'
+import {closeListener, openListener, scanInit} from '@/utils/keydownListener'
+import {getInventoryList} from '@/api/check/index'
+import {closeLoading, showLoading} from '@/utils/loading'
+import nodataUrl from '@/assets/nodata.png'
+import {showConfirmDialog, showDialog, showNotify, showToast} from 'vant'
+import {getRecommendedLocation} from "@/api/haikang/index";
+import {
+  createReturnFirstSeed,
+  getReturnTaskBinList,
+  returnTaskAbandon,
+  returnTaskFirstStepComplete
+} from "@/api/returnTask/index";
+
+const router = useRouter()
+const route = useRoute()
+const store = useStore()
+try {
+  getHeader()
+  androidFocus()
+} catch (error) {
+  router.push('/login')
+}
+const warehouse = store.warehouse
+
+const pattern = /^[1-9]\d*$/
+// 容器号和扫描类型的状态
+const taskNo = ref(route.query.code)
+const containerNo = ref(route.query.container)
+//扫描类型
+const scanType = ref(2)
+const searchRef = ref(null)
+//扫描条码
+const searchBarcode = ref('')
+//扫描库位
+const location = ref('')
+//批次数据
+const lotBarcodeList = ref([])
+const lotBarcodeTrueFalseBy = ref(false)
+const barcodeActive = ref({})
+//格口
+const bin = ref('-')
+const binList = ref([])
+const model = ref({})
+const countRef = ref(null)
+const back = ref(true)
+// 页面初始化
+onMounted(() => {
+  openListener()
+  scanInit(_handlerScan)
+  loadData()
+})
+//切换模式
+const actions = [
+  {name: '放弃', key: 'abandon'}
+]
+const modeTrueFalseBy = ref(false)
+
+// 扫描条码监听
+const _handlerScan = (code) => {
+  if (scanType.value === 2) {
+    setTimeout(() => {
+      _getInventoryList(code)
+    }, 200)
+  }
+}
+// 获取库存数据
+const _getInventoryList = async (barcode) => {
+  const data = {warehouse, location: containerNo.value, barcode}
+  try {
+    showLoading()
+    const res = await getInventoryList(data)
+    closeLoading()
+    if (res.data.length === 0) {
+      scanError()
+      searchBarcode.value = ''
+      showNotify({duration: 5000, message: `条码:${barcode},未找到可还库库存,请检查条码!`})
+      tips.value = `条码:${barcode},未找到可还库库存,请检查条码!`
+      return
+    }
+    barcodeActive.value = {}
+    lotBarcodeList.value = res.data
+    searchBarcode.value = res.data[0].sku
+    if (lotBarcodeList.value.length == 1) {
+      barcodeActive.value = lotBarcodeList.value[0]
+      await _getRecommendedLocation(lotBarcodeList.value[0].lotNum, lotBarcodeList.value[0].customerId)
+    } else if (lotBarcodeList.value.length > 1) {
+      lotBarcodeTrueFalseBy.value = true
+    }
+
+  } catch (err) {
+    closeLoading()
+    console.error(err)
+  }
+}
+// 获取库存数据
+const _getRecommendedLocation = async (lotNum, owner) => {
+  try {
+    const params = {warehouse, lotNum, owner, zoneGroup: 'WH01-01'}
+    const res = await getRecommendedLocation(params)
+    //  'EA'数据
+    const eaItems = res.data.filter(item => item.type === 'EA')
+    // 获取 quantity < 300 的
+    let result = eaItems.find(item => item.quantity !== null && item.quantity < 1000)
+    // 获取 quantity 为 null 的
+    if (!result) {
+      result = eaItems.find(item => item.quantity === null)
+    }
+    if (result) {
+      if (result.quantity === null) {
+        scanError()
+        tips.value = `${searchBarcode.value}:库区内无商品库存,请调空料箱进行入库`
+        showNotify({
+          type: 'danger',
+          duration: 3000,
+          message: `${searchBarcode.value}:库区内无商品库存,请调空料箱进行入库`
+        })
+        searchBarcode.value = ''
+        scanType.value = 2
+        return
+      }
+      setLocation(result)
+    }
+  } catch (err) {
+    console.error(err)
+  }
+}
+
+// 设置库位
+const setLocation = (item) => {
+  const data = {
+    taskNo: taskNo.value,
+    barcode: searchBarcode.value,
+    lotNum: barcodeActive.value.lotNum,
+    containerCode: item.location
+  }
+  createReturnFirstSeed(data).then(res => {
+    scanSuccess()
+    searchBarcode.value = ''
+    bin.value = res.data
+    tips.value = `请扫描商品条码`
+    getTaskBinList()
+  }).catch(err => {
+    scanError()
+    barcodeActive.value = {}
+    searchBarcode.value = ''
+    bin.value = '-'
+    tips.value = `${err.message}`
+  })
+}
+
+// 提示文本根据扫描类型返回
+const tips = ref('请扫描商品条码')
+const onSelectMode = async (value) => {
+  if (value.key == 'abandon') {
+    showDialog({
+      title: '温馨提示',
+      message: '您正在进行放弃任务操作,是否继续?',
+    }).then(() => {
+      showLoading()
+      returnTaskAbandon({taskNo: taskNo.value}).then(res => {
+        router.push({name: 'ReturnList'})
+        showToast({duration: 3000, message: '当前任务已放弃正在跳转到任务页面,请稍后~'})
+      }).catch((err) => {
+        tips.value = `${err.message}`
+      }).finally(_ => {
+        closeLoading()
+      })
+    });
+
+  }
+}
+
+const getTaskBinList = () => {
+  getReturnTaskBinList({taskNo: taskNo.value}).then(res => {
+    binList.value = res.data.sort((a, b) => {
+      if (a.slot < b.slot) {
+        return 1;
+      }
+      if (a.slot > b.slot) {
+        return -1;
+      }
+      return 0
+    });
+  }).catch(err => {
+    scanError()
+    tips.value = `${err.message}`
+    showNotify({type: 'danger', duration: 3000, message: `${err.message}`})
+  })
+}
+const onClickRightIcon = () => {
+  modeTrueFalseBy.value = true
+}
+// 完成任务
+const onComplete = () => {
+  showConfirmDialog({
+    title: '温馨提示',
+    message:
+        '您正在进行完成操作,是否立即呼叫小车?',
+    confirmButtonText: '立即呼叫',
+    cancelButtonText: '不呼叫',
+  })
+    .then(() => {
+      _returnTaskFirstStepComplete(true)
+    })
+    .catch(() => {
+      _returnTaskFirstStepComplete(false)
+    });
+}
+const _returnTaskFirstStepComplete=(callHikQuickIn)=>{
+  showLoading()
+  returnTaskFirstStepComplete({taskNo:taskNo.value,callHikQuickIn}).then(res => {
+    if(callHikQuickIn){
+      router.replace({ name: 'ReturnTask', query: { code:taskNo.value,container:containerNo.value } });
+    }else {
+      router.push({name: 'ReturnList'})
+    }
+  }).finally(_ => {
+    closeLoading()
+  })
+}
+// 数据刷新
+const loadData = () => {
+  scanType.value = 2
+  getTaskBinList()
+  setTimeout(() => {
+    if (binList.value.length > 0) {
+      bin.value = binList.value[0].slot
+    }
+  }, 400)
+}
+
+const onClickLeft = () => {
+  router.push({name:'ReturnList'})
+};
+
+onUnmounted(() => {
+  closeListener()
+})
+
+window.onRefresh = loadData
+</script>
+
+<style scoped lang="scss">
+.container {
+  .move-stock {
+    .code {
+      background: #e9f4ff;
+      box-sizing: border-box;
+      padding: 8px 0;
+    }
+
+    .code-title {
+      display: flex;
+      justify-content: space-between;
+      padding: 0 15px 8px 15px;
+    }
+
+    .code-input {
+      ::v-deep(.van-search) {
+        padding: 0;
+        font-size: 16px;
+      }
+
+      ::v-deep(.van-search__field) {
+        border-bottom: 2px solid #ffffff;
+        height: 50px;
+        display: flex;
+        align-items: center;
+      }
+
+      ::v-deep(.van-search__content) {
+        background: #fff;
+        height: 50px;
+        display: flex;
+        align-items: center;
+      }
+
+      ::v-deep(.van-field__control) {
+        font-size: 16px;
+        height: 50px;
+        line-height: 50px;
+      }
+
+      .search-input-barcode {
+        ::v-deep(.van-search__field) {
+          border-bottom: 2px solid #0077ff;
+          z-index: 2;
+        }
+      }
+    }
+
+    .code-tips {
+      color: #ed6a0c;
+      flex: 1;
+    }
+
+    .code-count {
+      font-size: 16px;
+      font-weight: bold;
+
+      span {
+        color: #0077ff;
+      }
+    }
+
+    .nav-right {
+      padding: 14px 0 12px 5px;
+    }
+
+    .move-stock-list {
+      width: 100%;
+      overflow-y: auto;
+      max-height: 280px;
+      min-height: 100px;
+
+      .move-button {
+        background: #1989fa;
+        color: #fff;
+        width: 100%;
+        height: 30px;
+        font-size: 14px;
+        line-height: 30px;
+        font-weight: bold;
+      }
+
+      .task-table,
+      .task-table-bin,
+      .task-table-box {
+        width: 100%;
+        table-layout: fixed;
+        border-collapse: collapse;
+        font-size: 13px;
+      }
+
+      .task-table th,
+      .task-table-bin th,
+      .task-table td,
+      .task-table-bin td,
+      .task-table-box th,
+      .task-table-box td {
+        text-align: center;
+        border: 1px solid #ccc;
+        word-wrap: break-word;
+        word-break: break-all;
+      }
+
+      .task-table thead,
+      .task-table-bin thead,
+      .task-table-box thead {
+        background-color: #3f8dff;
+        position: sticky;
+        top: 0;
+        color: white;
+        font-size: 13px;
+      }
+
+      .task-table-bin thead {
+        background-color: #3f8dff;
+      }
+
+      .task-table-bin tbody {
+        background: #cde7ff;
+      }
+
+      .task-table tbody tr.pricking-active {
+        background-color: #d6f9e7;
+      }
+
+      .task-table tbody tr.stock-active {
+        background-color: #fffadd;
+      }
+
+      .task-table tbody tr.virtual-active {
+        background-color: #cacaca;
+      }
+    }
+
+    .move-stock-content {
+      margin-bottom: 10px;
+
+      .compact-table {
+        width: 100%;
+        border-collapse: collapse;
+        margin: 0;
+        font-size: 14px;
+        line-height: 1.2;
+        border: 1px solid #ccc;
+
+        td {
+          padding: 8px 12px;
+          border: 1px solid #ccc;
+          background: transparent;
+          text-align: center;
+        }
+
+        td:first-child {
+          color: #333;
+          font-size: 25px;
+        }
+
+        td:last-child {
+          color: #e63535;
+          font-size: 50px;
+
+        }
+      }
+    }
+  }
+}
+
+.count-input {
+  ::v-deep(.van-field__value) {
+    border-bottom: 2px solid #0077ff;
+    font-size: 16px;
+  }
+}
+
+.completion {
+  text-align: right;
+  font-size: 14px;
+  line-height: 35px;
+  color: #0077ff;
+  padding: 0 10px;
+  cursor: pointer;
+  text-decoration: underline;
+}
+
+.tips {
+  font-size: 13px;
+  text-align: left;
+  padding: 5px 15px;
+}
+</style>

+ 216 - 0
src/views/inventory/returnTask/list/index.vue

@@ -0,0 +1,216 @@
+<template>
+  <div class="container">
+    <div class="task">
+      <div class="top">
+        <div class="nav-bar">
+          <van-nav-bar title="还库任务" left-arrow  @click-left="goBack" @click-right="onClickRight" >
+            <template #left>
+              <van-icon name="arrow-left" size="25"   />
+              <div style="color: #fff;height: 46px;padding-right:20px;line-height: 46px" >返回</div>
+            </template>
+            <template #right>
+              <div style="color: #fff;line-height: 46px " >创建任务</div>
+            </template>
+          </van-nav-bar>
+        </div>
+        <div class="content">
+          <van-pull-refresh v-model="loading" @refresh="onRefresh" :style="{ 'max-height': computedMaxHeight,'min-height':computedMaxHeight ,'overflow':'auto'}" >
+            <van-list
+              v-model="loadingMore"
+              :finished="!hasMore"
+              finished-text="没有更多了"
+              @load="loadMore"
+              :style="{ 'max-height': computedMaxHeight,'min-height':computedMaxHeight ,'overflow':'auto'}"
+            >
+              <table border="1" style="width: 100%;border-collapse: collapse;text-align: center;table-layout: fixed;" >
+                <thead>
+                <tr>
+                  <th>任务号</th>
+                  <th style="width: 50px">状态</th>
+                  <th style="width: 75px">创建日期</th>
+                  <th style="width: 55px">剩余数量</th>
+                  <th style="width: 70px">操作</th>
+                </tr>
+                </thead>
+                <tbody>
+                <tr v-for="(row, rowIndex) in taskList" :key="rowIndex">
+                  <td style="word-wrap: break-word" >{{ row.containerCode }}</td>
+                  <td style="word-wrap: break-word" >{{ row.statusDesc }}</td>
+                  <td style="word-wrap: break-word;font-size:11px">{{ time(row.createTime) }}</td>
+                  <td style="word-wrap: break-word;">{{ row.remainingQty }}</td>
+                  <td>
+                    <van-button type="primary" size="mini" plain  @click="linkTask(row)">作业</van-button>
+                    <van-button type="primary" size="mini" plain @click="linkTask(row)">查看</van-button>
+                  </td>
+                </tr>
+                <tr v-if="taskList.length==0 && !loadingMore">
+                  <td colspan="5">
+                    <van-empty :image="nodataUrl" image-size="140" >
+                      <van-button round type="primary" class="bottom-button" size="small" @click="loadData(true)">刷新</van-button>
+                    </van-empty>
+                  </td>
+                </tr>
+                </tbody>
+              </table>
+            </van-list>
+            <van-back-top right="80vw" bottom="10vh" />
+          </van-pull-refresh>
+        </div>
+      </div>
+    </div>
+    <input-barcode ref="inputBarcodeRef"  @setBarcode="setBarcode"  />
+  </div>
+</template>
+
+<script setup>
+import { computed, onMounted, ref } from 'vue'
+import {getHeader, goBack, scanError, scanSuccess} from '@/utils/android'
+import { useStore } from '@/store/modules/user'
+import { showToast } from 'vant'
+import nodataUrl from '@/assets/nodata.png'
+import { closeLoading, showLoading } from '@/utils/loading'
+import { useRouter } from 'vue-router'
+import InputBarcode from '@/views/outbound/picking/components/InputBarcode.vue'
+import {createReturnTask, getReturnTaskPage} from "@/api/returnTask/index.ts";
+const router = useRouter()
+try {
+  getHeader()
+}catch (error) {
+  router.push('/login')
+}
+const topHeight=ref(46)
+const computedMaxHeight = computed(() => {
+  return `calc(100vh - ${topHeight.value}px)`;
+});
+const storeUser = useStore()
+const warehouse = storeUser.warehouse
+//任务列表
+const taskList=ref([])
+const statusMap=ref({
+  'CREATED':'创建',
+  'IN_PROGRESS':'进行中',
+  'FINISHED':'完成'
+})
+
+// 分页相关状态
+const page = ref(1)
+const size = ref(20)
+const hasMore = ref(true)
+const loadingMore = ref(false)
+
+const loadData = async (isLoadMore = false) => {
+  if (!isLoadMore) {
+    showLoading()
+    page.value = 1
+    taskList.value = []
+  } else {
+    loadingMore.value = true
+  }
+
+  try {
+    const params = {
+      warehouse: warehouse,
+      page: page.value,
+      size: size.value
+    }
+    const res = await getReturnTaskPage(params)
+    if (isLoadMore) {
+      taskList.value = [...taskList.value, ...res.data.records]
+    } else {
+      taskList.value = res.data.records
+    }
+
+    // 判断是否还有更多数据
+    hasMore.value = res.data.records && res.data.records.length === size.value
+  } finally {
+    if (!isLoadMore) {
+      closeLoading()
+    } else {
+      loadingMore.value = false
+    }
+  }
+}
+ const time=(createTime)=>{
+   // 创建一个日期对象
+   let date = new Date(createTime);
+// 获取年月日时分的部分
+   let year = date.getFullYear().toString().slice(2); // 获取年份的后两位
+   let month = String(date.getMonth() + 1).padStart(2, '0'); // 获取月份,保证两位
+   let day = String(date.getDate()).padStart(2, '0'); // 获取日期,保证两位
+   let hours = String(date.getHours()).padStart(2, '0'); // 获取小时,保证两位
+   let minutes = String(date.getMinutes()).padStart(2, '0'); // 获取分钟,保证两位
+// 组装成目标格式
+   let formattedDate = `${year}${month}${day}${hours}:${minutes}`;
+   return formattedDate
+ }
+// 上拉加载更多
+const loadMore = () => {
+  if (hasMore.value && !loadingMore.value) {
+    page.value++
+    loadData(true)
+  }
+}
+loadData()
+
+const inputBarcodeRef=ref(null)
+const onClickRight=()=>{
+  inputBarcodeRef.value?.show(undefined,'请扫描反拣容器','')
+}
+const setBarcode=(code)=>{
+  createReturnTask({warehouse,containerCode:code}).then(res=>{
+    console.log(res,"res")
+    scanSuccess()
+    router.push({ name:'ReturnTask', query: { code: res.data,container:code,status:'CREATING' } });
+  }).catch(err=>{
+    scanError()
+    setTimeout(()=>{
+      inputBarcodeRef.value?.show('', '请扫描反拣容器', err.message)
+    },300)
+  })
+}
+
+// 进入任务
+const linkTask = (item) => {
+  let name='ReturnBin'
+  if(item.status=='WORKING'){
+    name='ReturnTask'
+  }
+  router.push({ name, query: { code:item.taskNo,container:item.containerCode,status:item.status } });
+};
+
+/**
+ * 下拉刷新
+ */
+const loading = ref(false)
+const onRefresh = () => {
+  setTimeout(() => {
+    loadData(false)
+    showToast('刷新成功')
+    loading.value = false
+  }, 1000)
+}
+
+window.onRefresh = loadData
+</script>
+
+<style scoped lang="sass">
+.container
+  width: 100%
+
+  .task
+    display: flex
+    flex-direction: column
+    height: 100vh
+
+    .top
+      flex: 1
+      display: flex
+      flex-direction: column
+
+      .nav-bar
+        height: 46px
+
+      .content
+        flex: 1
+        font-size: 13px
+</style>

+ 853 - 0
src/views/inventory/returnTask/task/index.vue

@@ -0,0 +1,853 @@
+<template>
+  <div class="container">
+    <van-nav-bar
+        title="还库任务"
+        left-arrow
+        fixed
+        placeholder
+        @click-left="onClickLeft"
+    >
+      <template #left>
+        <van-icon name="arrow-left" size="25"/>
+        <div style="color: #fff">返回</div>
+      </template>
+      <template #right>
+        <div style="color: #fff;line-height: 46px " @click="onComplete">料箱回库</div>
+      </template>
+    </van-nav-bar>
+    <div class="move-stock">
+      <div class="code">
+        <div class="code-title">
+          <div>{{ containerNo || '' }}</div>
+          <div class="code-tips">
+            <van-notice-bar :background="'none'" :speed="50" :text="tips"/>
+          </div>
+        </div>
+        <div class="code-input">
+          <van-field v-model="workBinNo" placeholder="请扫描料箱号" label="料箱编号:" left-icon="" :class="[scanType===1?'search-input-barcode':'']"
+                     autocomplete="off" readonly @click="setBarcodeInput(workBinNo,1)"></van-field>
+          <van-field v-model="searchBarcode" placeholder="请扫描商品条码" label="商品条码:" left-icon="" readonly :class="[scanType===2?'search-input-barcode':'']"
+                     autocomplete="off" @click="setBarcodeInput(searchBarcode,2)"></van-field>
+          <van-field ref="countRef" @click="scanType=3" v-model="count" type="number" placeholder="还库数量" label="还库数量:" left-icon="" autocomplete="off"
+                     :class="{'search-input-barcode': scanType === 3,'input-count': barcodeActive.availableQty}"  @keydown.enter="setPutaway(2)"
+                     :max="barcodeActive.availableQty?barcodeActive.availableQty:10000"
+          >
+            <template #button>
+              <div v-if="barcodeActive.qty" style="color:#333;font-size: 14px;font-weight: bold">预计{{ barcodeActive.qty ||0 }}</div>
+            </template>
+          </van-field>
+          <van-field v-model="searchLocation" placeholder="请扫描库位" label="还库库位:" left-icon="" readonly :class="[scanType===4?'search-input-barcode':'']"
+                     autocomplete="off" @click="setBarcodeInput(searchLocation,4)">
+            <template #button>
+              <div v-if="recommendedLocation" style="color:#666;font-size: 12px"><span style="font-size: 10px">推荐:</span>{{ recommendedLocation }}</div>
+            </template>
+          </van-field>
+        </div>
+        <div class="code-button">
+          <van-button type="primary" size="small" block :disabled="searchLocation!=recommendedLocation || searchLocation=='' "   @click="setPutaway(2)" >提交还库</van-button>
+        </div>
+      </div>
+      <div class="move-stock-list">
+        <table class="task-table">
+          <thead>
+          <tr>
+            <th>商品条码</th>
+            <th>商品名称</th>
+            <th>格口号</th>
+            <th>数量</th>
+          </tr>
+          </thead>
+          <tbody>
+          <template v-if="binList.length>0">
+            <tr v-for="(item, index) in binList" :key="index"
+                :class="{
+                  'container-highlight': isContainerHighlighted(item.containerCode),
+                  'barcode-highlight': isBarcodeHighlighted(item.containerCode, item.lotNum)
+                }">
+              <td>{{ item.barcode }}</td>
+              <td>{{ item.skuName }}</td>
+              <td>{{ item.slot }}</td>
+              <td>{{item.availableQty}}</td>
+            </tr>
+          </template>
+          <tr v-else>
+            <td colspan="4">
+              <van-empty :image="nodataUrl" image-size="120"/>
+            </td>
+          </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+    <!--  单据选择-->
+    <van-action-sheet :show="lotBarcodeTrueFalseBy" cancel-text="取消" description="请选择商品批次"
+                      close-on-click-action
+                      @update:show="lotBarcodeTrueFalseBy = $event">
+      <van-cell-group>
+        <van-cell v-for="(item, idx) in lotBarcodeList" :key="idx"
+                  @click="onSelectLotBarcode(item)">
+          <template #title>
+            {{item.barcode }}({{item.availableQty}}件)
+          </template>
+          <template #label>
+            生产日期:{{ item.productionDate || '--' }}-失效日期:{{ item.expirationDate || '--' }}
+          </template>
+        </van-cell>
+      </van-cell-group>
+    </van-action-sheet>
+    <!-- 条码输入组件 -->
+    <input-barcode ref="inputBarcodeRef" @setBarcode="_handlerScan" />
+    <!-- 料箱回库组件 -->
+    <go-box-back ref="goBackRef" :scanType="scanType" @setGoBack="setGoBack" />
+  </div>
+</template>
+
+<script setup>
+import {onMounted, onUnmounted, ref} from 'vue'
+import {useRouter, useRoute} from 'vue-router'
+import {useStore} from '@/store/modules/user'
+import {androidFocus, getHeader, goBack, scanError, scanSuccess} from '@/utils/android'
+import {closeListener, openListener, scanInit} from '@/utils/keydownListener'
+import {closeLoading, showLoading} from '@/utils/loading'
+import nodataUrl from '@/assets/nodata.png'
+import {showConfirmDialog, showDialog, showNotify, showToast} from 'vant'
+import { barcodeToUpperCase } from '@/utils/dataType'
+import {
+  getReturnTaskBinList,
+  updateReturnTaskUsedQty
+} from "@/api/returnTask/index";
+import { movementReturn } from '@/api/check/index'
+import { boxReturn } from '@/api/haikang'
+import InputBarcode from '@/views/outbound/picking/components/InputBarcode.vue'
+import GoBoxBack from '@/views/haikang/putaway/components/GoBoxBack.vue'
+
+const router = useRouter()
+const route = useRoute()
+const store = useStore()
+try {
+  getHeader()
+  androidFocus()
+} catch (error) {
+  router.push('/login')
+}
+const warehouse = store.warehouse
+
+const pattern = /^[1-9]\d*$/
+// 容器号和扫描类型的状态
+const taskNo = ref(route.query.code)
+const containerNo = ref(route.query.container)
+//扫描类型
+const scanType = ref(2)
+const searchRef = ref(null)
+//料箱编号
+const workBinNo = ref('')
+//扫描条码
+const searchBarcode = ref('')
+//原始扫描的条码(用于匹配binList)
+const originalBarcode = ref('')
+//还库数量
+const count = ref('')
+//扫描库位
+const location = ref('')
+//还库库位
+const searchLocation = ref('')
+//推荐库位
+const recommendedLocation = ref('')
+//提示信息
+const tips = ref('请扫描料箱编号')
+//批次数据
+const lotBarcodeList = ref([])
+const lotBarcodeTrueFalseBy = ref(false)
+const barcodeActive = ref({})
+//格口
+const bin = ref('-')
+const binList = ref([])
+const model = ref({})
+const countRef = ref(null)
+const back = ref(true)
+const goBackRef = ref(null)
+// 高亮状态:橘黄色高亮(containerCode匹配)
+const orangeHighlightSet = ref(new Set())
+// 高亮状态:绿色高亮(条码匹配)
+const greenHighlightSet = ref(new Set())
+
+// 检查是否应该高亮
+const isContainerHighlighted = (containerCode) => orangeHighlightSet.value?.has(containerCode)
+const isBarcodeHighlighted = (containerCode, lotNum) => greenHighlightSet.value?.has(`${containerCode}-${lotNum}`)
+
+// 更新高亮状态:从橘黄色变为绿色
+const updateHighlight = (barcode, lotNum) => {
+  const barcodeUpper = barcodeToUpperCase(barcode)
+  binList.value
+      .filter(item => {
+        // 匹配条码
+        const barcodeMatch = barcodeToUpperCase(item.barcode) === barcodeUpper ||
+            barcodeToUpperCase(item.alternateSku1) === barcodeUpper ||
+            barcodeToUpperCase(item.alternateSku2) === barcodeUpper
+        return orangeHighlightSet.value.has(item.containerCode) &&
+            barcodeMatch &&
+            item.lotNum === lotNum
+      })
+      .forEach(item => {
+        orangeHighlightSet.value.delete(item.containerCode)
+        greenHighlightSet.value.add(`${item.containerCode}-${item.lotNum}`)
+      })
+}
+
+// 页面初始化
+onMounted(() => {
+  openListener()
+  scanInit(_handlerScan)
+  loadData()
+})
+
+//设置条码
+const inputBarcodeRef = ref(null)
+const setBarcodeInput = (code, type) => {
+  const typeMap = { 1: '请扫描料箱编号', 2: '请扫描商品条码', 3: '请输入还库数量', 4: '请扫描还库库位' }
+  scanType.value = type
+  inputBarcodeRef.value?.show(code, typeMap[type])
+}
+
+// 扫描条码监听
+const _handlerScan = (code) => {
+  if (scanType.value === 1) {
+    workBinNo.value = code
+    // 先清空推荐库位、预计数量和匹配的条码
+    recommendedLocation.value = ''
+    barcodeActive.value = {}
+    searchBarcode.value = ''
+    originalBarcode.value = ''
+    count.value = ''
+    searchLocation.value = ''
+    // 高亮所有匹配的containerCode
+    orangeHighlightSet.value.clear()
+    greenHighlightSet.value.clear()
+    // 在binList中查找匹配的containerCode并高亮
+    const matched = binList.value.filter(item =>
+        item.containerCode && (item.containerCode === code || item.containerCode.startsWith(`${code}-`))
+    )
+    if (matched.length === 0) {
+      scanError()
+      tips.value = `料箱编号:${code},未找到匹配的数据`
+      showNotify({ type: 'danger', duration: 3000, message: `料箱编号:${code},未找到匹配的数据` })
+      // 清空页面
+      workBinNo.value = ''
+      searchBarcode.value = ''
+      originalBarcode.value = ''
+      searchLocation.value = ''
+      recommendedLocation.value = ''
+      count.value = ''
+      barcodeActive.value = {}
+      orangeHighlightSet.value.clear()
+      greenHighlightSet.value.clear()
+      scanType.value = 1
+      tips.value = '请扫描料箱编号'
+      return
+    }
+
+    matched.forEach(item => orangeHighlightSet.value.add(item.containerCode))
+    sortBinList()
+
+    scanType.value = 2
+    tips.value = '请扫描商品条码'
+    scanSuccess()
+  } else if (scanType.value === 2) {
+    // 商品条码扫描
+    setTimeout(() => {
+      _getInventoryList(code)
+    }, 200)
+  } else if (scanType.value === 4) {
+    const codeUpper = barcodeToUpperCase(code)
+    const recommendedUpper = barcodeToUpperCase(recommendedLocation.value)
+    if (codeUpper !== recommendedUpper) {
+      tips.value = `${code}-扫描库位与推荐库位不一致,请重新扫描`
+      showNotify({ type: 'danger', duration: 3000, message: `${code}-扫描库位与推荐库位不一致,请重新扫描` })
+      scanError()
+      return
+    }
+    searchLocation.value = barcodeToUpperCase(code)
+    scanType.value = 3
+    count.value = barcodeActive.value.availableQty || 1
+    tips.value = '请输入还库数量'
+    scanSuccess()
+  }
+}
+
+
+// 从binList中获取匹配到的推荐库位(containerCode)
+const getRecommendedLocationFromBin = (barcode, lotNum) => {
+  const barcodeUpper = barcodeToUpperCase(barcode)
+  // 获取匹配到的绿色高亮项(最终匹配到的批次)
+  const matchedItems = binList.value.filter(item => {
+    // 匹配 barcode、alternateSku1、alternateSku2 三个字段,使用 barcodeToUpperCase 进行大小写不敏感匹配
+    const barcodeMatch = barcodeToUpperCase(item.barcode) === barcodeUpper ||
+        barcodeToUpperCase(item.alternateSku1) === barcodeUpper ||
+        barcodeToUpperCase(item.alternateSku2) === barcodeUpper
+    const match = barcodeMatch && item.lotNum === lotNum
+    if (!match) return false
+    return greenHighlightSet.value?.has(`${item.containerCode}-${item.lotNum}`)
+  })
+
+  if (matchedItems.length > 0) {
+    // 返回第一个匹配项的containerCode作为推荐库位
+    return matchedItems[0].containerCode || ''
+  }
+  return ''
+}
+
+// 选择批次后的处理
+const onSelectLotBarcode = (item) => {
+  // 使用批次对象中保存的实际 barcode(支持 alternateSku1、alternateSku2 匹配)
+  const actualBarcode = item.barcode || originalBarcode.value
+  updateHighlight(actualBarcode, item.lotNum)
+  // 基于最终匹配到的批次计算数量(使用可用数量 availableQty)
+  barcodeActive.value = item
+  // 从binList中获取推荐库位(containerCode)
+  recommendedLocation.value = getRecommendedLocationFromBin(actualBarcode, item.lotNum)
+  lotBarcodeTrueFalseBy.value = false
+  scanType.value = 4
+  tips.value = '请扫描还库库位'
+  scanSuccess()
+}
+
+const _getInventoryList = (barcode) => {
+  try {
+    // 检查是否有当前料箱号
+    if (!workBinNo.value) {
+      scanError()
+      searchBarcode.value = ''
+      originalBarcode.value = ''
+      const msg = '请先扫描料箱编号'
+      tips.value = msg
+      showNotify({duration: 5000, message: msg})
+      return
+    }
+
+    // 保存原始条码用于匹配binList
+    originalBarcode.value = barcode
+
+    // 在当前料箱内查找匹配的条码数据
+    const scanBarcodeUpper = barcodeToUpperCase(barcode)
+    const matchedItems = binList.value.filter(binItem => {
+      const containerMatch = binItem.containerCode === workBinNo.value ||
+          binItem.containerCode?.startsWith(`${workBinNo.value}-`)
+      if (!containerMatch) return false
+      return barcodeToUpperCase(binItem.barcode) === scanBarcodeUpper ||
+          barcodeToUpperCase(binItem.alternateSku1) === scanBarcodeUpper ||
+          barcodeToUpperCase(binItem.alternateSku2) === scanBarcodeUpper
+    })
+
+    // 如果没有任何匹配的数据,提示错误
+    if (matchedItems.length === 0) {
+      scanError()
+      searchBarcode.value = ''
+      originalBarcode.value = ''
+      const msg = `扫描条码不在当前料箱内,当前料箱号:${workBinNo.value}`
+      tips.value = msg
+      showNotify({duration: 5000, message: msg})
+      return
+    }
+
+    // 根据批次(lotNum)分组,生成批次列表
+    const lotMap = new Map()
+    matchedItems.forEach(item => {
+      const lotNum = item.lotNum || ''
+      if (!lotMap.has(lotNum)) {
+        // 获取实际匹配的条码
+        const matchedBarcode = barcodeToUpperCase(item.barcode) === scanBarcodeUpper ? item.barcode :
+            barcodeToUpperCase(item.alternateSku1) === scanBarcodeUpper ? item.barcode :
+                barcodeToUpperCase(item.alternateSku2) === scanBarcodeUpper ? item.barcode : barcode
+
+        // 计算可用数量
+        lotMap.set(lotNum, {
+          customerId:item.customerId,
+          lotNum: lotNum,
+          usedQty:item.usedQty,
+          qty:item.qty,
+          availableQty: item.availableQty, // 添加可用数量
+          productionDate: item.productionDate ,
+          expirationDate: item.expirationDate,
+          barcode: item.barcode // 使用实际匹配的条码
+        })
+      }
+    })
+
+    // 转换为数组
+    const matchedInBin = Array.from(lotMap.values())
+
+    // 如果没有任何批次,提示错误
+    if (matchedInBin.length === 0) {
+      scanError()
+      searchBarcode.value = ''
+      originalBarcode.value = ''
+      const msg = `扫描条码不在当前料箱内,当前料箱号:${workBinNo.value}`
+      tips.value = msg
+      showNotify({duration: 5000, message: msg})
+      return
+    }
+
+    // 只保留在当前料箱内的批次
+    lotBarcodeList.value = matchedInBin
+    searchBarcode.value = matchedInBin[0].barcode
+
+    if (matchedInBin.length === 1) {
+      // 只有一个批次,直接选择
+      const item = matchedInBin[0]
+      // 更新高亮状态(从橘黄色变为绿色)
+      updateHighlight(item.barcode, item.lotNum)
+      barcodeActive.value = item
+      // 从binList中获取推荐库位(containerCode)
+      recommendedLocation.value = getRecommendedLocationFromBin(item.barcode, item.lotNum)
+      scanType.value = 4
+      tips.value = '请扫描还库库位'
+      scanSuccess()
+    } else {
+      // 多个批次,弹出批次选择弹窗
+      lotBarcodeTrueFalseBy.value = true
+    }
+  } catch (err) {
+    console.error(err)
+    scanError()
+    tips.value = err.message || '匹配失败'
+    showNotify({type: 'danger', duration: 3000, message: err.message || '匹配失败'})
+  }
+}
+
+// 提交还库
+const setPutaway = async (type) => {
+  if (type !== 2) return
+
+  const validations = [
+    { check: !searchBarcode.value, type: 2, msg: '请扫描商品条码' },
+    { check: !searchLocation.value, type: 4, msg: '请扫描还库库位' },
+    { check: barcodeToUpperCase(searchLocation.value) !== barcodeToUpperCase(recommendedLocation.value), type: 4, msg: '扫描库位与推荐库位不一致,请重新扫描' },
+    { check: !count.value || Number(count.value) <= 0, type: 3, msg: '请输入还库数量' },
+    { check: barcodeActive.value.availableQty && Number(count.value) > Number(barcodeActive.value.availableQty), type: 3, msg: '还库数量不能大于预计还库数量' }
+  ]
+
+  const error = validations.find(v => v.check)
+  if (error) {
+    scanType.value = error.type
+    tips.value = error.msg
+    showNotify({ type: 'danger', duration: 3000, message: error.msg })
+    scanError()
+    return
+  }
+  const {customerId,lotNum,barcode:sku}=barcodeActive.value
+  const data = {
+    warehouse: warehouse,
+    owner:customerId,
+    lotNum,
+    sku,
+    quantity: count.value,
+    fmLocation:containerNo.value ,
+    toLocation: searchLocation.value
+  }
+  try {
+    showLoading()
+    const res = await movementReturn(data)
+    if(res){
+      // 还库成功后,更新还库任务已使用数量
+      try {
+        await updateReturnTaskUsedQty({
+          taskNo: taskNo.value,
+          lotNum: barcodeActive.value.lotNum,
+          moveQty: Number(count.value)
+        })
+      } catch (updateErr) {
+        console.error('更新已使用数量失败:', updateErr)
+        // 不阻断主流程,只记录错误
+      }
+    }
+    closeLoading()
+    scanSuccess()
+    // 保存当前还库数量,用于料箱回库弹窗显示
+    const lastCount = count.value
+    const lastBarcodeActive = { ...barcodeActive.value }
+    // 重置(保留料箱号,以便继续扫描同一料箱的其他商品)
+    searchBarcode.value = ''
+    originalBarcode.value = ''
+    searchLocation.value = ''
+    recommendedLocation.value = ''
+    count.value = ''
+    barcodeActive.value = {}
+    // 刷新binList
+    getTaskBinList()
+
+    // 检查当前料箱是否已完成还库
+    setTimeout(() => {
+      if (checkBinComplete()) {
+        // 所有商品都已还库完成,弹出料箱回库提示
+        goBackRef.value?.show(workBinNo.value, '1', lastCount, lastBarcodeActive)
+      } else {
+        // 还有未还库的商品,继续扫描
+        scanType.value = 2
+        tips.value = '请扫描商品条码'
+        showNotify({ type: 'success', duration: 3000, message: '还库成功,请继续扫描商品条码' })
+      }
+    }, 300)
+  } catch (err) {
+    closeLoading()
+    scanError()
+    tips.value = err.message || '还库失败'
+    showNotify({ type: 'danger', duration: 3000, message: err.message || '还库失败' })
+  }
+}
+
+// 排序:匹配到的料箱排在最前面
+const sortBinList = () => {
+  binList.value.sort((a, b) => {
+    const aHighlighted = orangeHighlightSet.value.has(a.containerCode)
+    const bHighlighted = orangeHighlightSet.value.has(b.containerCode)
+    if (aHighlighted !== bHighlighted) return aHighlighted ? -1 : 1
+    return b.slot - a.slot
+  })
+}
+
+const getTaskBinList = () => {
+  getReturnTaskBinList({taskNo: taskNo.value}).then(res => {
+    // 过滤并计算可用数量
+    binList.value = res.data
+        .filter(v => v.qty !== v.usedQty)
+        .map(item => ({
+          ...item,
+          availableQty: (item.qty || 0) - (item.usedQty || 0) // 计算可用数量
+        }))
+    sortBinList()
+  }).catch(err => {
+    scanError()
+    tips.value = `${err.message}`
+    showNotify({type: 'danger', duration: 3000, message: `${err.message}`})
+  })
+}
+
+// 检查当前料箱是否已完成还库
+const checkBinComplete = () => {
+  if (!workBinNo.value) return false
+  // 检查binList中是否还有匹配当前料箱号的数据
+  const remainingItems = binList.value.filter(item =>
+      item.containerCode && (item.containerCode === workBinNo.value || item.containerCode.startsWith(`${workBinNo.value}-`))
+  )
+  return remainingItems.length === 0
+}
+// 料箱回库(使用上架页面的逻辑)
+const onComplete = () => {
+  if (!workBinNo.value) {
+    scanType.value = 1
+    tips.value = '请先扫描料箱编号'
+    showNotify({ type: 'danger', duration: 3000, message: '请先扫描料箱编号' })
+    scanError()
+    return
+  }
+
+  // 从binList中获取container和externalCode信息
+  const currentBinItem = binList.value.find(item =>
+      item.containerCode && (item.containerCode === workBinNo.value || item.containerCode.startsWith(`${workBinNo.value}-`))
+  )
+
+  const container = currentBinItem?.container || containerNo.value
+  const externalCode = currentBinItem?.externalCode || taskNo.value
+
+  showConfirmDialog({
+    title: '料箱回库',
+    message: `${workBinNo.value},是否执行料箱回库?`,
+  })
+      .then(() => {
+        showLoading()
+        boxReturn({ warehouse, container, boxCode: workBinNo.value, externalCode }).then(res => {
+          closeLoading()
+          scanSuccess()
+          tips.value = '料箱回库成功,请继续扫描料箱编号'
+          showNotify({ type: 'success', duration: 3000, message: '料箱回库成功,请继续扫描料箱编号' })
+          resetAllFields()
+        }).catch(err => {
+          closeLoading()
+          scanError()
+          tips.value = err.message || '料箱回库失败'
+          showNotify({ type: 'danger', duration: 3000, message: err.message || '料箱回库失败' })
+        })
+      })
+      .catch(() => {})
+}
+
+// 料箱回库处理(使用上架页面的逻辑)
+const setGoBack = async (item) => {
+  if (item.active === '1') {
+    // 料箱回库
+    if (!workBinNo.value) {
+      scanType.value = 1
+      tips.value = '请先扫描料箱编号'
+      showNotify({ type: 'danger', duration: 3000, message: '请先扫描料箱编号' })
+      scanError()
+      return
+    }
+
+    const container =  containerNo.value
+    const externalCode =  taskNo.value
+    showLoading()
+    try {
+      await boxReturn({ warehouse, container, boxCode: workBinNo.value, externalCode })
+      closeLoading()
+      scanSuccess()
+      // 重置所有状态
+      resetAllFields()
+      tips.value = '料箱回库成功,请继续扫描料箱编号'
+      showNotify({ type: 'success', duration: 3000, message: '料箱回库成功,请继续扫描料箱编号' })
+    } catch (err) {
+      closeLoading()
+      scanError()
+      tips.value = err.message || '料箱回库失败'
+      showNotify({ type: 'danger', duration: 3000, message: err.message || '料箱回库失败' })
+    }
+  }
+}
+
+// 重置所有字段
+const resetAllFields = () => {
+  workBinNo.value = ''
+  searchBarcode.value = ''
+  originalBarcode.value = ''
+  searchLocation.value = ''
+  recommendedLocation.value = ''
+  count.value = ''
+  barcodeActive.value = {}
+  orangeHighlightSet.value.clear()
+  greenHighlightSet.value.clear()
+  scanType.value = 1
+  getTaskBinList()
+}
+
+onUnmounted(() => {
+  closeListener()
+})
+
+const onClickLeft = () => {
+  router.push({name:'ReturnList'})
+};
+// 数据刷新
+const loadData = () => {
+  scanType.value = 1
+  tips.value = '请扫描料箱编号'
+  getTaskBinList()
+}
+
+onUnmounted(() => {
+  closeListener()
+})
+
+window.onRefresh = loadData
+</script>
+
+<style scoped lang="scss">
+.container {
+  .move-stock {
+    .code {
+      background: #e9f4ff;
+      box-sizing: border-box;
+      padding: 8px 0;
+    }
+
+    .code-title {
+      display: flex;
+      justify-content: space-between;
+      padding: 0 15px 8px 15px;
+    }
+
+    .code-input {
+      ::v-deep(.van-field) {
+        min-height: 50px;
+        display: flex;
+        align-items: center;
+      }
+
+      ::v-deep(.van-field__body) {
+        min-height: 50px;
+        display: flex;
+        align-items: center;
+      }
+
+      ::v-deep(.van-cell) {
+        padding: 5px 20px 0 20px;
+      }
+
+      ::v-deep(.van-field__control) {
+        border-bottom: 2px solid #efefef;
+        height: 50px;
+        line-height: 50px;
+        font-size: 16px;
+        display: flex;
+        align-items: center;
+      }
+
+      ::v-deep(.van-field__button) {
+        display: flex;
+        align-items: center;
+        height: 50px;
+      }
+
+      ::v-deep(.van-field__label) {
+        width: unset;
+        font-size: 16px;
+      }
+
+      .search-input-barcode {
+        ::v-deep(.van-field__control) {
+          border-bottom: 2px solid #0077ff;
+          z-index: 2;
+        }
+      }
+    }
+    .code-button{
+      padding: 10px 15px;
+    }
+    .code-tips {
+      color: #ed6a0c;
+      flex: 1;
+    }
+
+    .code-count {
+      font-size: 16px;
+      font-weight: bold;
+
+      span {
+        color: #0077ff;
+      }
+    }
+
+    .nav-right {
+      padding: 14px 0 12px 5px;
+    }
+
+    .move-stock-list {
+      width: 100%;
+      overflow-y: auto;
+      max-height: 280px;
+      min-height: 100px;
+
+      .move-button {
+        background: #1989fa;
+        color: #fff;
+        width: 100%;
+        height: 30px;
+        font-size: 14px;
+        line-height: 30px;
+        font-weight: bold;
+      }
+
+      .task-table,
+      .task-table-bin,
+      .task-table-box {
+        width: 100%;
+        table-layout: fixed;
+        border-collapse: collapse;
+        font-size: 13px;
+      }
+
+      .task-table th,
+      .task-table-bin th,
+      .task-table td,
+      .task-table-bin td,
+      .task-table-box th,
+      .task-table-box td {
+        text-align: center;
+        border: 1px solid #ccc;
+        word-wrap: break-word;
+        word-break: break-all;
+      }
+
+      .task-table thead,
+      .task-table-bin thead,
+      .task-table-box thead {
+        background-color: #3f8dff;
+        position: sticky;
+        top: 0;
+        color: white;
+        font-size: 13px;
+      }
+
+      .task-table-bin thead {
+        background-color: #3f8dff;
+      }
+
+      .task-table-bin tbody {
+        background: #cde7ff;
+      }
+
+      .task-table tbody tr.pricking-active {
+        background-color: #d6f9e7;
+      }
+
+      .task-table tbody tr.stock-active {
+        background-color: #fffadd;
+      }
+
+      .task-table tbody tr.virtual-active {
+        background-color: #cacaca;
+      }
+
+      .task-table tbody tr.container-highlight {
+        background-color: #ffa500;
+      }
+
+      .task-table tbody tr.barcode-highlight {
+        background-color: #90ee90 !important;
+      }
+    }
+
+    .move-stock-content {
+      margin-bottom: 10px;
+
+      .compact-table {
+        width: 100%;
+        border-collapse: collapse;
+        margin: 0;
+        font-size: 14px;
+        line-height: 1.2;
+        border: 1px solid #ccc;
+
+        td {
+          padding: 8px 12px;
+          border: 1px solid #ccc;
+          background: transparent;
+          text-align: center;
+        }
+
+        td:first-child {
+          color: #333;
+          font-size: 20px;
+        }
+
+        td:last-child {
+          color: #e63535;
+          font-size: 30px;
+
+        }
+      }
+    }
+  }
+}
+
+.count-input {
+  ::v-deep(.van-field__value) {
+    border-bottom: 2px solid #0077ff;
+    font-size: 16px;
+  }
+}
+
+.completion {
+  text-align: right;
+  font-size: 14px;
+  line-height: 35px;
+  color: #0077ff;
+  padding: 0 10px;
+  cursor: pointer;
+  text-decoration: underline;
+}
+
+.tips {
+  font-size: 13px;
+  text-align: left;
+  padding: 5px 15px;
+}
+</style>

+ 1 - 1
src/views/inventory/transfer/index.vue

@@ -234,7 +234,7 @@ const _handlerScan = (code) => {
       warehouse,
       barcode: productBarcode.value,
       location: sourceLocation.value,
-      locationRegexp: '^(?!STAGE_|SORTATION_).*$',
+      locationRegexp: '^(?!STAGE_|SORTATION_|TRANSFER_).*$',
     }
     showLoading()
     getInventory(params).then(res => {

+ 1 - 1
src/views/outbound/check/moveStock/index.vue

@@ -282,7 +282,7 @@ const _getInventoryList = async (barcode) => {
     }
     scanSuccess()
     searchBarcode.value= res.data[0].sku
-    const params={ warehouse, barcode,locationRegexp:'^(?!STAGE_|SORTATION_|REVERSEPICK_|FJ-).*$',queryLocationInfo:true }
+    const params={ warehouse, barcode,locationRegexp:'^(?!STAGE_|SORTATION_|REVERSEPICK_|FJ-|TRANSFER_).*$',queryLocationInfo:true }
     if(barcode==='') return
     getInventory(params).then(res=>{
       totalList.value=res.data

+ 28 - 12
src/views/outbound/picking/list/index.vue

@@ -170,6 +170,8 @@ const count=ref('')
 const countRef= ref(null)
 const countTrueFalseBy=ref(false)
 const  activeItem=ref({})
+// 存储需要跳转的库位(当弹框显示时扫描到库位)
+const pendingLocationJump=ref('')
 //容器号ref
 const scanCountRef = ref(null)
 onMounted(() => {
@@ -373,7 +375,8 @@ const _handlerScan=(code)=> {
       scannedLocation = locationContainerMap.value[barcodeToUpperCase(code)]
     }
     if (taskMap.value[barcodeToUpperCase(scannedLocation)]) {
-      // 扫描到当前任务中的库位,自动确认
+      // 扫描到当前任务中的库位,自动确认,接口成功后再跳转
+      pendingLocationJump.value = barcodeToUpperCase(scannedLocation)
       onSubmitCount()
       return
     }
@@ -472,12 +475,8 @@ const _handlerScan=(code)=> {
               onScan(4)
               activeItem.value=activeBarcode
               countTrueFalseBy.value=true
-              setTimeout(()=>{
-                countRef.value?.focus()
-              },200)
               // setTimeout(()=>{
-              //   //匹配到条码扣减库存
-              //   onCount(activeBarcode,0)
+              //   countRef.value?.focus()
               // },200)
             }).catch(()=>{
               const allOperationTimeExist = modelLocative.list.every(({ operationTime }) => operationTime);
@@ -493,11 +492,9 @@ const _handlerScan=(code)=> {
             //匹配到条码扣减库存
             activeItem.value=activeBarcode
             countTrueFalseBy.value=true
-            setTimeout(()=>{
-              countRef.value?.focus()
-            },200)
-            // onCount(activeBarcode,0)
-          // },200)
+            // setTimeout(()=>{
+            //   countRef.value?.focus()
+            // },200)
         }
     }else{
       //查询组合条码
@@ -715,7 +712,6 @@ const _setPickingDetail=(params,type)=>{
         }
       })
     }
-    scanSuccess()
     if(selectTask.value.length===taskItem.value.length){
       showDialog({
         title: '温馨提示',
@@ -730,12 +726,32 @@ const _setPickingDetail=(params,type)=>{
         }
       })
     }
+    // 如果任务全部完成,则不跳转,优先执行完成逻辑
+    if (selectTask.value.length === taskItem.value.length) {
+      pendingLocationJump.value = '' // 清空待跳转标记
+      scanSuccess()
+    } else {
+      // 检查是否有待跳转的库位(弹框显示时扫描库位的情况)
+      if (pendingLocationJump.value) {
+        nextTick(() => {
+          nextLocation.value = ''
+          activeIndex.value = locationList.value.findIndex(item => item.location === pendingLocationJump.value)
+          onScan(3)
+          scanSuccess()
+          pendingLocationJump.value = '' // 清空标记
+        })
+      } else {
+        scanSuccess()
+      }
+    }
   }).catch((err)=> {
     closeLoading()
     scanBarcode.value = ''
     onScan(4)
     messageTips.value = err.message
     scanError()
+    // 接口调用失败,清空待跳转库位标记
+    pendingLocationJump.value = ''
     if(err.code=='ERR_NETWORK'){
       messageTips.value = '网络开小车了,请稍后重试!'
     }if(err.code=='ECONNABORTED'){

+ 278 - 0
src/views/robot/merge/components/BoxSelectionDialog.vue

@@ -0,0 +1,278 @@
+<template>
+  <van-popup
+    v-model:show="show"
+    position="bottom"
+    round
+    :style="{ maxHeight: '70%' }"
+    @close="onClose"
+  >
+    <div class="dialog-container">
+      <div class="dialog-header">
+        <span class="dialog-title">请选择需要回库的料箱</span>
+        <van-icon name="cross" @click="onClose" />
+      </div>
+
+      <div class="dialog-content">
+        <!-- 表头 -->
+        <div class="table-header">
+          <div class="header-left">料箱</div>
+          <div class="header-right">
+            <span class="select-all-link" @click="toggleAll">全选</span>
+          </div>
+        </div>
+
+        <van-checkbox-group v-model="checkedBoxes">
+          <van-cell-group inset>
+            <!-- 料箱列表 -->
+            <van-cell
+              v-for="(boxCode, index) in boxList"
+              clickable
+              :key="boxCode"
+              @click="toggleBox(index)"
+            >
+              <template #title>
+                <div class="cell-content">
+                  <div class="cell-left">{{ boxCode }}</div>
+                  <div class="cell-right">
+                    <van-checkbox
+                      :name="boxCode"
+                      :ref="el => checkboxRefs[index] = el"
+                      @click.stop
+                    />
+                  </div>
+                </div>
+              </template>
+            </van-cell>
+          </van-cell-group>
+        </van-checkbox-group>
+      </div>
+
+      <div class="dialog-footer">
+        <van-button
+          size="large"
+          block
+          type="primary"
+          :disabled="checkedBoxes.length === 0"
+          @click="confirmSelection"
+        >
+          确认回库 ({{ checkedBoxes.length }}个料箱)
+        </van-button>
+      </div>
+    </div>
+  </van-popup>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue'
+import { showToast, showLoadingToast, closeToast } from 'vant'
+import { boxInbound } from '@/api/location/merge'
+import { scanError, scanSuccess } from '@/utils/android'
+import { log } from 'console'
+import { BoxInboundResult } from '@/api/location/merge.ts'
+
+interface Props {
+  boxList: string[]
+  warehouse: string
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  boxList: () => [],
+  warehouse: ''
+})
+
+const emit = defineEmits<{
+  success: []
+  close: []
+}>()
+
+const show = ref(false)
+const checkedBoxes = ref<string[]>([])
+const checkboxRefs = ref<any[]>([])
+
+// 计算是否全选
+const isAllSelected = computed(() => {
+  return checkedBoxes.value.length === props.boxList.length && props.boxList.length > 0
+})
+
+// 显示弹窗
+const showDialog = () => {
+  checkedBoxes.value = []
+  show.value = true
+}
+
+// 关闭弹窗
+const onClose = () => {
+  show.value = false
+  emit('close')
+}
+
+// 切换单个料箱
+const toggleBox = (index: number) => {
+  const boxCode = props.boxList[index]
+  const checkbox = checkboxRefs.value[index]
+  if (checkbox) {
+    checkbox.toggle()
+  }
+}
+
+// 切换全选
+const toggleAll = () => {
+  if (isAllSelected.value) {
+    checkedBoxes.value = []
+  } else {
+    checkedBoxes.value = [...props.boxList]
+  }
+}
+
+// 确认选择
+const confirmSelection = async () => {
+  if (checkedBoxes.value.length === 0) return
+
+  try {
+    showLoadingToast({ message: '正在呼唤机器人...', forbidClick: true })
+    const { data } = await boxInbound(props.warehouse, checkedBoxes.value)
+    closeToast()
+    // 显示详细结果
+    let message = '呼唤机器人结果:\n'
+    data.forEach((item: BoxInboundResult) => {
+      message += `${item.t1}: ${item.t2}\n`
+    })
+
+    // 判断整体结果
+    const hasSuccess = data.some((item: BoxInboundResult) =>
+      item.t2.includes('回库成功')
+    )
+    const hasFailure = data.some((item: BoxInboundResult) =>
+      !item.t2.includes('回库成功')
+    )
+
+    if (hasSuccess && !hasFailure) {
+      // 全部成功
+      scanSuccess()
+      showToast(message.trim())
+    } else if (!hasSuccess && hasFailure) {
+      // 全部失败
+      scanError()
+      showToast(message.trim())
+    } else {
+      // 部分成功
+      scanSuccess()
+      showToast(message.trim())
+    }
+
+    show.value = false
+    emit('success')
+
+  } catch (error: any) {
+    closeToast()
+    scanError()
+    showToast(error.message || '呼唤机器人失败')
+  }
+}
+
+// 暴露方法给父组件使用
+defineExpose({
+  showDialog
+})
+</script>
+
+<style scoped lang="scss">
+.dialog-container {
+  padding: 16px;
+  max-height: 70vh;
+  display: flex;
+  flex-direction: column;
+}
+
+.dialog-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-bottom: 12px;
+  border-bottom: 1px solid #eee;
+
+  .dialog-title {
+    font-size: 16px;
+    font-weight: 500;
+    color: #333;
+  }
+
+  .van-icon {
+    cursor: pointer;
+    color: #999;
+  }
+}
+
+.dialog-content {
+  flex: 1;
+  padding: 12px 0;
+  overflow-y: auto;
+
+  .hint-text {
+    font-size: 14px;
+    color: #666;
+    margin-bottom: 12px;
+    text-align: center;
+  }
+
+  .table-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 12px 16px;
+    background-color: #f8f8f8;
+    border-radius: 8px;
+    margin-bottom: 8px;
+    font-weight: 500;
+    color: #333;
+
+    .header-left {
+      flex: 1;
+      text-align: left;
+    }
+
+    .header-right {
+      text-align: right;
+
+      .select-all-link {
+        color: #1989fa;
+        cursor: pointer;
+        text-decoration: none;
+
+        &:hover {
+          text-decoration: underline;
+        }
+      }
+    }
+  }
+
+  .van-cell-group {
+    margin: 0;
+  }
+
+  .van-cell {
+    .cell-content {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      width: 100%;
+
+      .cell-left {
+        flex: 1;
+        font-size: 14px;
+        color: #333;
+        text-align: left;
+      }
+
+      .cell-right {
+        margin-left: 12px;
+      }
+    }
+  }
+}
+
+.dialog-footer {
+  padding-top: 12px;
+  border-top: 1px solid #eee;
+}
+</style>

+ 188 - 0
src/views/robot/merge/components/MergeTaskDetailsDialog.vue

@@ -0,0 +1,188 @@
+<template>
+  <van-popup
+    v-model:show="show"
+    position="bottom"
+    round
+    :style="{ maxHeight: '70%' }"
+    @close="onClose"
+  >
+    <div class="dialog-container">
+      <div class="dialog-header">
+        <span class="dialog-title">并库任务详情</span>
+        <van-icon name="cross" @click="onClose" />
+      </div>
+
+      <div class="dialog-content">
+        <div v-if="loading" class="loading-container">
+          <van-loading size="24px">加载中...</van-loading>
+        </div>
+        <div v-else-if="taskList.length === 0" class="empty-container">
+          <van-empty description="暂无并库任务" />
+        </div>
+        <div v-else class="task-list">
+          <div
+            v-for="(task, index) in taskList"
+            :key="task.id"
+            class="task-item"
+          >
+            <div class="task-header">
+              <span class="task-index">#{{ index + 1 }}</span>
+              <span class="task-sku">SKU: {{ task.sku }}</span>
+            </div>
+            <div class="task-details">
+              <div class="detail-row">
+                <span class="detail-label">推荐清空库位:</span>
+                <span class="detail-value">{{ task.sourceLocation }}</span>
+              </div>
+              <div class="detail-row">
+                <span class="detail-label">目标库位:</span>
+                <span class="detail-value">{{ task.targetLocation }}</span>
+              </div>
+              <div class="detail-row">
+                <span class="detail-label">推荐数量:</span>
+                <span class="detail-value">{{ task.moveQty }}</span>
+              </div>
+              <div class="detail-row">
+                <span class="detail-label">涉及料箱:</span>
+                <span class="detail-value">{{ task.involveBox ? task.involveBox.join(', ') : '-' }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </van-popup>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { LocationMergeDetails } from '@/api/location/merge'
+import { getWorkingMergeTasks } from '@/api/location/merge'
+import { showToast } from 'vant'
+
+const taskList = ref<LocationMergeDetails[]>([])
+const loading = ref(false)
+const show = ref(false)
+
+// 显示弹窗并加载数据
+const showDialog = async (warehouse: string) => {
+  show.value = true
+  loading.value = true
+
+  try {
+    const res = await getWorkingMergeTasks(warehouse)
+    taskList.value = res.data || []
+  } catch (error: any) {
+    showToast(error.message || '获取并库任务详情失败')
+    show.value = false
+  } finally {
+    loading.value = false
+  }
+}
+
+// 关闭弹窗
+const onClose = () => {
+  show.value = false
+}
+
+// 暴露方法给父组件使用
+defineExpose({
+  showDialog
+})
+</script>
+
+<style scoped lang="scss">
+.dialog-container {
+  padding: 16px;
+}
+
+.dialog-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-bottom: 12px;
+  border-bottom: 1px solid #eee;
+
+  .dialog-title {
+    font-size: 16px;
+    font-weight: 500;
+    color: #333;
+  }
+
+  .van-icon {
+    cursor: pointer;
+    color: #999;
+  }
+}
+
+.dialog-content {
+  padding: 12px 0;
+  max-height: 400px;
+  overflow-y: auto;
+}
+
+.loading-container,
+.empty-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 40px 0;
+}
+
+.task-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.task-item {
+  background: #f9f9f9;
+  border-radius: 8px;
+  padding: 12px;
+  border: 1px solid #e5e5e5;
+}
+
+.task-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+
+  .task-index {
+    font-size: 12px;
+    color: #1989fa;
+    font-weight: 500;
+  }
+
+  .task-sku {
+    font-size: 14px;
+    font-weight: 500;
+    color: #333;
+  }
+}
+
+.task-details {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.detail-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+
+  .detail-label {
+    font-size: 12px;
+    color: #666;
+    min-width: 80px;
+  }
+
+  .detail-value {
+    font-size: 12px;
+    color: #333;
+    text-align: right;
+    flex: 1;
+  }
+}
+</style>

+ 251 - 87
src/views/robot/merge/index.vue

@@ -1,11 +1,14 @@
 <template>
   <div class="merge-container">
     <van-nav-bar
-      title="海康并库" left-arrow fixed placeholder @click-left="goBack">
+      title="海康并库" left-arrow fixed placeholder @click-left="goBack" @click-right="onDetailsClick">
       <template #left>
         <van-icon name="arrow-left" size="25" />
         <div style="color: #fff">返回</div>
       </template>
+      <template #right>
+        <div style="color: #fff">详情</div>
+      </template>
     </van-nav-bar>
     <!-- 扫描输入框区域 -->
     <div class="scan-section">
@@ -106,58 +109,109 @@
       </div>
     </div>
 
-    <!-- 料箱选择区域 -->
-    <div class="grid-section">
-      <div class="grid-container">
-        <div
-          v-for="station in stationList"
-          :key="station.id"
-          class="box-wrapper"
-        >
-          <!-- 站台序号在上方 -->
-          <div class="box-number">{{ station.displayNumber }}</div>
-          <div
-            class="box-item"
-            :class="{
-              'box-filled': station.status === 'filled' && !station.splitCount,
-              'box-empty-box': station.status === 'emptyBox',
-              'box-waiting': station.status === 'waiting',
-              'box-error': station.status === 'error',
-              'box-selected': selectedBox === station.stationCode,
-              'box-split': station.splitCount && station.status !== 'error' && station.status !== 'waiting'
-            }"
-                        @click="handleStationClick(station)"
-          >
-                      <!-- 分割的料箱(异常/等待调箱状态不渲染分割) -->
-            <template v-if="station.splitCount && station.subLocations && station.status !== 'error' && station.status !== 'waiting'">
-              <div class="sub-grid" :style="getSubGridStyle(station.splitCount)">
-                <div
-                  v-for="sub in station.subLocations"
-                  :key="sub.id"
-                  class="sub-location"
-                  :class="{
-                    'sub-filled': sub.status === 'filled',
-                    'sub-selected': selectedBox === sub.locationCode,
-                    'sub-disabled': !isLocationClickable(sub.locationCode)
-                  }"
-                  @click.stop="isLocationClickable(sub.locationCode) && selectSubLocation(station, sub)"
-                ></div>
+    <!-- 站点类型选择和料箱选择区域 -->
+    <van-tabs v-model:active="selectedStationType" @change="handleStationTypeChange" class="station-tabs">
+      <van-tab title="上架站点" name="shelf">
+        <div class="grid-section">
+          <div class="grid-container">
+            <div
+              v-for="station in stationList"
+              :key="station.id"
+              class="box-wrapper"
+            >
+              <!-- 站台序号在上方 -->
+              <div class="box-number">{{ station.displayNumber }}</div>
+              <div
+                class="box-item"
+                :class="{
+                  'box-filled': station.status === 'filled' && !station.splitCount,
+                  'box-empty-box': station.status === 'emptyBox',
+                  'box-waiting': station.status === 'waiting',
+                  'box-error': station.status === 'error',
+                  'box-selected': selectedBox === station.stationCode,
+                  'box-split': station.splitCount && ['offline', 'filled', 'emptyBox'].includes(station.status)
+                }"
+                @click="handleStationClick(station)"
+              >
+                <!-- 分割的料箱(异常/等待调箱状态不渲染分割) -->
+                <template v-if="station.splitCount && station.subLocations && ['offline', 'filled', 'emptyBox'].includes(station.status)">
+                  <div class="sub-grid" :style="getSubGridStyle(station.splitCount)">
+                    <div
+                      v-for="sub in station.subLocations"
+                      :key="sub.id"
+                      class="sub-location"
+                      :class="{
+                        'sub-filled': sub.status === 'filled',
+                        'sub-selected': selectedBox === sub.locationCode,
+                        'sub-disabled': !isLocationClickable(sub.locationCode)
+                      }"
+                      @click.stop="isLocationClickable(sub.locationCode) && selectSubLocation(station, sub)"
+                    ></div>
+                  </div>
+                </template>
+                <!-- 普通站台或异常状态 -->
+                <template v-else>
+                  <span v-if="station.label" class="box-label">{{ station.label }}</span>
+                </template>
               </div>
-            </template>
-            <!-- 普通站台或异常状态 -->
-            <template v-else>
-              <span v-if="station.label" class="box-label">{{ station.label }}</span>
-            </template>
+            </div>
           </div>
         </div>
-      </div>
-    </div>
+      </van-tab>
+      <van-tab title="退货缓存站点" name="return">
+        <div class="grid-section">
+          <div class="grid-container">
+            <div
+              v-for="station in stationList"
+              :key="station.id"
+              class="box-wrapper"
+            >
+              <!-- 站台序号在上方 -->
+              <div class="box-number">{{ station.displayNumber }}</div>
+              <div
+                class="box-item"
+                :class="{
+                  'box-filled': station.status === 'filled' && !station.splitCount,
+                  'box-empty-box': station.status === 'emptyBox',
+                  'box-waiting': station.status === 'waiting',
+                  'box-error': station.status === 'error',
+                  'box-selected': selectedBox === station.stationCode,
+                  'box-split': station.splitCount && ['offline', 'filled', 'emptyBox'].includes(station.status)
+                }"
+                @click="handleStationClick(station)"
+              >
+                <!-- 分割的料箱(异常/等待调箱状态不渲染分割) -->
+                <template v-if="station.splitCount && station.subLocations && ['offline', 'filled', 'emptyBox'].includes(station.status)">
+                  <div class="sub-grid" :style="getSubGridStyle(station.splitCount)">
+                    <div
+                      v-for="sub in station.subLocations"
+                      :key="sub.id"
+                      class="sub-location"
+                      :class="{
+                        'sub-filled': sub.status === 'filled',
+                        'sub-selected': selectedBox === sub.locationCode,
+                        'sub-disabled': !isLocationClickable(sub.locationCode)
+                      }"
+                      @click.stop="isLocationClickable(sub.locationCode) && selectSubLocation(station, sub)"
+                    ></div>
+                  </div>
+                </template>
+                <!-- 普通站台或异常状态 -->
+                <template v-else>
+                  <span v-if="station.label" class="box-label">{{ station.label }}</span>
+                </template>
+              </div>
+            </div>
+          </div>
+        </div>
+      </van-tab>
+    </van-tabs>
 
     <!-- 底部按钮 -->
     <div class="footer-buttons">
       <van-button class="btn-robot" size="small" @click="callRobot">呼唤机器人</van-button>
       <div class="btn-right">
-        <van-button type="primary" class="btn-reset" size="small" @click="resetInput">重新输入</van-button>
+        <van-button type="primary" class="btn-reset" size="small" @click="resetAllData">重新输入</van-button>
         <van-button class="btn-submit" size="small" @click="submitMove">提交移库</van-button>
       </div>
     </div>
@@ -170,11 +224,14 @@
       :style="{ maxHeight: '70%' }"
     >
       <div class="location-popup">
-        <div class="popup-header">
-          <span class="popup-title">库位信息</span>
-          <van-icon name="cross" @click="showLocationPopup = false" />
-        </div>
         <div class="popup-content">
+          <!-- 推荐类型信息 -->
+          <div class="recommend-type-section">
+            <div class="recommend-type">
+              {{ currentLocation.recommendType === 'clear' ? '此库位推荐清空' : '此库位推荐保留' }}
+            </div>
+            <van-icon name="cross" @click="showLocationPopup = false" />
+          </div>
           <div class="info-row">
             <span class="info-label">库位编号</span>
             <span class="info-value">{{ currentLocation.id }}</span>
@@ -186,7 +243,7 @@
           <!-- 推荐库位列表 -->
           <div class="recommend-section">
             <div class="recommend-title">
-              {{ currentLocation.recommendType === 'clear' ? '推荐清空' : '推荐保留' }}
+              {{ currentLocation.recommendType === 'clear' ? '推荐目标库位列表' : '推荐来源库位列表' }}
             </div>
             <div v-if="currentLocation.relatedLocations.length > 0" class="recommend-list">
               <div
@@ -206,6 +263,12 @@
         </div>
       </div>
     </van-popup>
+
+    <!-- 并库任务详情弹框 -->
+    <MergeTaskDetailsDialog ref="taskDetailsDialogRef"/>
+
+    <!-- 料箱选择弹框 -->
+    <BoxSelectionDialog ref="boxSelectionDialogRef" :box-list="currentBoxList" :warehouse="warehouse" @success="onBoxSelectionSuccess" />
   </div>
 </template>
 
@@ -214,11 +277,12 @@ import { closeListener, openListener, scanInit } from '@/utils/keydownListener'
 import { onMounted, onUnmounted, ref, reactive, nextTick } from 'vue'
 import { showToast, showLoadingToast, closeToast } from 'vant'
 import { useStore } from '@/store/modules/user'
-import { getWorkingDetailsByBox, getBoxSplitCode, boxInbound, reissueTask, getBoxStatus, type BoxRelatedMergeDetailsVO, type LocationMergeDetails } from '@/api/location/merge'
+import { getWorkingDetailsByBox, getBoxSplitCode, boxAndStationUnbindTask, reissueTask, getBoxStatus, forceCompleteBoxTask, type BoxRelatedMergeDetailsVO, type LocationMergeDetails } from '@/api/location/merge'
 import { getInventory, inventoryMovement } from '@/api/inventory'
-import { boxAndStationUnbindTask } from '@/api/haikang'
 import { showConfirmDialog } from 'vant'
 import { getHeader, androidFocus, goBack, scanError, scanSuccess } from '@/utils/android'
+import MergeTaskDetailsDialog from './components/MergeTaskDetailsDialog.vue'
+import BoxSelectionDialog from './components/BoxSelectionDialog.vue'
 
 try {
   getHeader()
@@ -244,6 +308,8 @@ const boxCode = ref('')
 const sourceLocation = ref('')
 // 商品条码
 const scanBarcode = ref('')
+// 选中的站点类型:'return'(退货缓存站点)或 'shelf'(上架站点)
+const selectedStationType = ref('return')
 
 // 轮询定时器
 let pollingTimer: ReturnType<typeof setInterval> | null = null
@@ -382,6 +448,16 @@ const onTargetLocationEnter = () => {
   showToast(`已输入目标库位: ${productInfo.targetLocationNew}`)
 }
 
+// 处理站点类型切换
+const handleStationTypeChange = () => {
+  // 重新初始化站点(显示不同类型的站点)
+  initStations()
+  // 重新加载站点数据
+  if (boxCode.value) {
+    refreshBoxData()
+  }
+}
+
 // 当前选中的库存数据(用于提交移库)
 const currentInventoryData = ref<any>(null)
 
@@ -454,6 +530,7 @@ const resetAllData = () => {
   mergeDataList.value = []
   clickableLocationsMap.value = new Map()
   resetProductInfo()
+  productInfo.targetLocationNew = '' // 清空目标库位
   initStations()
   scanType.value = 1
   focusBoxCodeInput()
@@ -492,6 +569,12 @@ const resetProductInfo = () => {
 const loadBoxData = async (code: string) => {
   if (!code) return
 
+  // 检查是否为站台编码(以RLOCHK开头)
+  if (code.startsWith('RLOCHK')) {
+    handleStationCodeForceComplete(code)
+    return
+  }
+
   try {
     showLoadingToast({ message: '加载中...', forbidClick: true })
 
@@ -665,12 +748,19 @@ interface StationItem {
 // 站台列表
 const stationList = ref<StationItem[]>([])
 
-// 初始化站台列表(12个站台,编号13-24
+// 初始化站台列表(根据站点类型显示不同范围的站台
 const initStations = () => {
-  stationList.value = Array.from({ length: 12 }, (_, i) => {
-    const num = String(i + 13).padStart(2, '0')
+  const stationRange = selectedStationType.value === 'return'
+    ? { start: 13, end: 24 }  // 退货缓存站点
+    : { start: 1, end: 12 }   // 上架站点
+
+  const { start, end } = stationRange
+  const length = end - start + 1
+
+  stationList.value = Array.from({ length }, (_, i) => {
+    const num = String(start + i).padStart(2, '0')
     return {
-      id: i + 1,
+      id: start + i,
       stationCode: `RLOCHK${num}A01011`,
       displayNumber: num,
       status: 'offline' as const
@@ -733,7 +823,7 @@ const updateStationList = (boxDetailsList: BoxRelatedMergeDetailsVO[], splitMap:
       status = 'offline'
     } else if (boxStatus === 40) {
       status = 'error'
-      label = '异'
+      label = '调库'
     }
 
     // 获取分割数量
@@ -820,7 +910,7 @@ const handleStationClick = (station: StationItem) => {
     return
   }
   // 空料箱状态弹出解绑确认框(仅当料箱无分割,即料箱即库位时才能触发)
-  if (station.status === 'emptyBox' && (!station.splitCount || station.splitCount === 1)) {
+  if (station.status === 'emptyBox') {
     showUnbindConfirm(station)
     return
   }
@@ -830,11 +920,35 @@ const handleStationClick = (station: StationItem) => {
   selectStation(station)
 }
 
+// 处理站台编码强制完成任务
+const handleStationCodeForceComplete = async (stationCode: string) => {
+  showConfirmDialog({
+    title: '强制完成任务',
+    message: `检测到站台编码:${stationCode},是否强制完成该站台料箱任务?`
+  }).then(async () => {
+    try {
+      showLoadingToast({ message: '正在强制完成任务...', forbidClick: true })
+      await forceCompleteBoxTask(warehouse, stationCode, null)
+      closeToast()
+      scanSuccess()
+      showToast('强制完成任务成功')
+      // 重新加载数据
+      refreshBoxData()
+    } catch (error: any) {
+      closeToast()
+      scanError()
+      showToast(error.message || '强制完成任务失败')
+    }
+  }).catch(() => {
+    // 用户取消
+  })
+}
+
 // 显示重新下发确认框
 const showReissueConfirm = (station: StationItem) => {
   showConfirmDialog({
     title: '重新下发任务',
-    message: `站台${station.displayNumber}的料箱出现异常,是否重新下发任务?`
+    message: `料箱${station.boxCode}调用海康异常,是否重新下发任务?`
   })
     .then(() => {
       doReissueTask(station)
@@ -865,8 +979,7 @@ const doUnbindTask = async (station: StationItem) => {
     const data = {
       warehouse,
       boxCode: station.boxCode,
-      stationCode: station.stationCode,
-      releaseStation: false
+      stationCode: station.stationCode
     }
     await boxAndStationUnbindTask(data)
     closeToast()
@@ -930,7 +1043,7 @@ const refreshBoxStatus = async () => {
         status = 'offline'
       } else if (newBoxStatus === 40) {
         status = 'error'
-        label = '异'
+        label = '调库'
       }
 
       return { ...station, status, label }
@@ -1072,6 +1185,11 @@ const selectSubLocation = (station: StationItem, sub: SubLocation) => {
     return
   }
 
+  if (station.status === 'emptyBox') {
+    showUnbindConfirm(station)
+    return
+  }
+
   selectedBox.value = sub.locationCode
 
   // 获取库位推荐信息
@@ -1112,6 +1230,24 @@ const confirmSelectLocation = async () => {
   }
 }
 
+// 并库任务详情弹框
+const taskDetailsDialogRef = ref<any>(null)
+// 点击详情按钮
+const onDetailsClick = () => {
+  taskDetailsDialogRef.value?.showDialog(warehouse)
+}
+
+// 料箱选择弹框
+const boxSelectionDialogRef = ref<any>(null)
+// 当前可选择的料箱列表
+const currentBoxList = ref<string[]>([])
+// 料箱选择成功
+const onBoxSelectionSuccess = () => {
+  // 重置页面数据并聚焦到料箱输入框
+  refreshBoxData()
+}
+
+
 // 根据SKU查询库存信息
 const queryInventoryBySku = async (sku: string, location: string) => {
   try {
@@ -1186,25 +1322,17 @@ const getTaskRecommendQty = (location: string): number => {
 }
 
 // 呼唤机器人
-const callRobot = async () => {
-  try {
-    showLoadingToast({ message: '正在呼唤机器人...', forbidClick: true })
-    await boxInbound(warehouse)
-    closeToast()
-    scanSuccess()
-    showToast('呼唤机器人成功')
-    // 重置页面数据并聚焦到料箱输入框
-    resetAllData()
-  } catch (error: any) {
-    closeToast()
-    scanError()
-    showToast(error.message || '呼唤机器人失败')
+const callRobot = () => {
+  // 获取当前所有料箱列表
+  currentBoxList.value = getBoxCodeList()
+
+  if (currentBoxList.value.length === 0) {
+    showToast('暂无需要回库的料箱')
+    return
   }
-}
 
-// 重新输入(不重置料箱号)
-const resetInput = () => {
-  resetExceptBoxCode()
+  // 显示选择Dialog
+  boxSelectionDialogRef.value?.showDialog()
 }
 
 // 提交移库
@@ -1255,7 +1383,8 @@ const submitMove = () => {
         lotNum: lotNumber,
         warehouse,
         quantity: Number(productInfo.actualMoveQty),
-        toLocation: productInfo.targetLocationNew
+        toLocation: productInfo.targetLocationNew,
+        transactionType: 'MERGE_TRANSFER'
       }
       showLoadingToast({ message: '提交中...', forbidClick: true })
       // 保存移库前的数据用于判断是否清空
@@ -1320,6 +1449,22 @@ const submitMove = () => {
   }
 }
 
+.station-tabs {
+  margin-bottom: 50px;
+  background: #fff;
+  border-radius: 4px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+
+  :deep(.van-tabs__nav) {
+    background: transparent;
+  }
+
+  :deep(.van-tab) {
+    font-size: 14px;
+    font-weight: 500;
+  }
+}
+
 .info-table {
   background: #fff;
   border: 1px solid #ddd;
@@ -1430,7 +1575,7 @@ const submitMove = () => {
 .grid-section {
   background: #fff;
   padding: 12px;
-  margin-bottom: 80px;
+  margin-bottom: 12px;
   border-radius: 4px;
 
   .grid-container {
@@ -1497,12 +1642,11 @@ const submitMove = () => {
     }
 
         &.box-error {
-      background: #ffcccc;
       border-color: #ff6666;
       cursor: pointer;
 
       .box-label {
-        color: #cc0000;
+        color: #ff6666;
       }
     }
 
@@ -1619,7 +1763,27 @@ const submitMove = () => {
   }
 
   .popup-content {
-    padding: 12px 0;
+    padding: 6px 0;
+
+    .recommend-type-section {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 10px;
+
+      .recommend-type {
+        font-size: 16px;
+        font-weight: 500;
+        color: #1989fa;
+        text-align: left;
+      }
+
+      .van-icon {
+        font-size: 18px;
+        color: #666;
+        cursor: pointer;
+      }
+    }
 
     .info-row {
       display: flex;
@@ -1649,15 +1813,15 @@ const submitMove = () => {
   }
 
   .recommend-section {
-    margin-top: 12px;
-    padding-top: 12px;
-    border-top: 1px solid #eee;
+    margin-top: 6px;
+    padding-top: 6px;
 
     .recommend-title {
       font-size: 14px;
       font-weight: 500;
       color: #333;
       margin-bottom: 8px;
+      text-align: left;
     }
 
     .recommend-list {

+ 7 - 5
src/views/robot/takeDelivery/task/index.vue

@@ -643,11 +643,11 @@ const isCheck = () => {
       return false
     }
     // 检查失效日期是否小于当前日期
-    if (expirationDate <= currentDate) {
-      scanError()
-      showToast({ duration: 3000, message: `失效日期不能小于等于当前日期` })
-      return false
-    }
+    // if (expirationDate <= currentDate) {
+    //   scanError()
+    //   showToast({ duration: 3000, message: `失效日期不能小于等于当前日期` })
+    //   return false
+    // }
   }
   if(productionDate){
     // 如果有生产日期,进行有效性检查
@@ -689,6 +689,7 @@ const onConfirm = () => {
   if(isCheck()){
     const lotMap = toMap(lotData.value, 'field', 'mapping')
     const { asnLineNo, asnNo, warehouse } = asnInfo.value
+    const {taskNo: taskCode } = taskInfo.value
     const data = {
       asnLineNo,
       asnNo,
@@ -697,6 +698,7 @@ const onConfirm = () => {
       warehouse,
       serialNos: uniqueCodeList.value.length > 0 ? uniqueCodeList.value : undefined,
       ...lotMap,
+      taskNo:taskCode
     }
     showLoading()
     inputBarcodeType.value='task'