zh 2 månader sedan
förälder
incheckning
c55622c8db

+ 53 - 21
src/api/location/merge.ts

@@ -3,25 +3,26 @@ 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
+  owner?: string
+  qualityStatus?: string
+  warehouseType?: string
+  productionDate?: string
+  expiryDate?: string
+  involveBox?: string[]
 }
 
 // 料箱相关的并库详情VO
@@ -61,12 +62,31 @@ export function getBoxSplitCode(warehouse: string, locations: string[]) {
 /**
  * 执行料箱回库(呼唤机器人)
  * @param warehouse 仓库
+ * @param boxCodes 可选的料箱编码列表,如果不传则回库所有料箱
  */
-export function boxInbound(warehouse: string) {
+export function boxInbound(warehouse: string, boxCodes?: string[]) {
+  const params: any = { warehouse }
+  if (boxCodes && boxCodes.length > 0) {
+    params.boxCodes = boxCodes
+  }
+
   return request<[string, string][]>({
     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 +129,15 @@ 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 }
+  })
+}

+ 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>

+ 124 - 27
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,6 +109,14 @@
       </div>
     </div>
 
+    <!-- 站点类型选择 -->
+    <div class="station-type-selector">
+      <van-radio-group v-model="selectedStationType" @change="handleStationTypeChange" direction="horizontal">
+        <van-radio name="shelf">上架站点</van-radio>
+        <van-radio name="return">退货缓存站点</van-radio>
+      </van-radio-group>
+    </div>
+
     <!-- 料箱选择区域 -->
     <div class="grid-section">
       <div class="grid-container">
@@ -170,11 +181,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 +200,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 +220,12 @@
         </div>
       </div>
     </van-popup>
+
+    <!-- 并库任务详情弹框 -->
+    <MergeTaskDetailsDialog ref="taskDetailsDialogRef"/>
+
+    <!-- 料箱选择弹框 -->
+    <BoxSelectionDialog ref="boxSelectionDialogRef" :box-list="currentBoxList" :warehouse="warehouse" @success="onBoxSelectionSuccess" />
   </div>
 </template>
 
@@ -219,6 +239,8 @@ 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 +266,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 +406,16 @@ const onTargetLocationEnter = () => {
   showToast(`已输入目标库位: ${productInfo.targetLocationNew}`)
 }
 
+// 处理站点类型切换
+const handleStationTypeChange = () => {
+  // 重新初始化站点(显示不同类型的站点)
+  initStations()
+  // 重新加载站点数据
+  if (boxCode.value) {
+    refreshBoxData()
+  }
+}
+
 // 当前选中的库存数据(用于提交移库)
 const currentInventoryData = ref<any>(null)
 
@@ -665,12 +699,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
@@ -1112,6 +1153,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 = () => {
+  // 重置页面数据并聚焦到料箱输入框
+  resetAllData()
+}
+
+
 // 根据SKU查询库存信息
 const queryInventoryBySku = async (sku: string, location: string) => {
   try {
@@ -1186,20 +1245,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
   }
+
+  // 显示选择Dialog
+  boxSelectionDialogRef.value?.showDialog()
 }
 
 // 重新输入(不重置料箱号)
@@ -1321,6 +1377,27 @@ const submitMove = () => {
   }
 }
 
+.station-type-selector {
+  margin-bottom: 12px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-size: 13px;
+  padding: 8px 12px;
+  background: #fff;
+  border-radius: 4px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+
+  .van-radio-group {
+    display: flex;
+    gap: 13px;
+  }
+
+  .van-button {
+    flex-shrink: 0;
+  }
+}
+
 .info-table {
   background: #fff;
   border: 1px solid #ddd;
@@ -1620,7 +1697,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;
@@ -1650,15 +1747,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 {