فهرست منبع

手持-还库-下达任务更改拣货站点

zhaohuanhuan 1 هفته پیش
والد
کامیت
69a4322a66

+ 23 - 0
src/api/haikang/index.ts

@@ -116,6 +116,29 @@ export function createHikBoxInboundTask(data:any) {
     data
   })
 }
+
+/** 海康可用拣选站点列表 */
+export function getHikUsableStationList(params: { warehouse: string; typeEnum: string }) {
+  return request({
+    url: '/api/wms/order/picking/getHikUsableStationList',
+    method: 'get',
+    params,
+  })
+}
+
+/** 批量创建海康料箱出库任务(呼叫小车) */
+export function batchCreateHikBoxOutboundTask(data: {
+  boxCodeList: string[]
+  warehouse: string
+  deviceType: string
+  stationCode: string
+}) {
+  return request({
+    url: '/api/wms/robot/task/batchCreateHikBoxOutboundTask',
+    method: 'post',
+    data,
+  })
+}
 /**
 * 海康-站点解绑
 * @param data

+ 10 - 1
src/api/returnTask/index.ts

@@ -1,7 +1,7 @@
 // @ts-ignore
 import request from '@/utils/request'
 // @ts-ignore
-import {createReturnFirstSeedType, createReturnTaskType, getReturnTaskBinListType, getReturnTaskPageType, returnTaskFirstStepCompleteType, updateReturnTaskUsedQtyType} from '@/types/returnTask'
+import {createReturnFirstSeedType, createReturnTaskType, getReturnTaskBinListType, getReturnTaskPageType, returnTaskCompleteType, returnTaskFirstStepCompleteType, updateReturnTaskUsedQtyType} from '@/types/returnTask'
 
 //分页查询还库任务列表
 export function getReturnTaskPage(params:getReturnTaskPageType) {
@@ -53,6 +53,15 @@ export function returnTaskAbandon(data:getReturnTaskBinListType) {
   })
 }
 
+//完成还库任务
+export function returnTaskComplete(data: returnTaskCompleteType) {
+  return request({
+    url: '/api/wms/return/task/complete',
+    method: 'post',
+    data
+  })
+}
+
 //完成分配格口和呼叫校车
 export function returnTaskFirstStepComplete(data:returnTaskFirstStepCompleteType) {
   return request({

+ 1 - 1
src/static/setting.txt

@@ -1 +1 @@
-93
+95

+ 9 - 0
src/types/returnTask.ts

@@ -61,6 +61,15 @@ export interface getReturnTaskBinListType {
   [property: string]: any;
 }
 
+//完成还库任务
+export interface returnTaskCompleteType {
+  /**
+   * 任务号
+   */
+  taskNo: string;
+  [property: string]: any;
+}
+
 //完成分配格口和呼叫校车
 export interface returnTaskFirstStepCompleteType {
   /**

+ 11 - 31
src/views/inventory/returnTask/bin/index.vue

@@ -103,6 +103,7 @@
         </van-cell>
       </van-cell-group>
     </van-action-sheet>
+    <ReturnCallHikCartPopup ref="callHikCartRef" :warehouse="warehouse"/>
   </div>
 </template>
 
@@ -115,14 +116,14 @@ 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 {showDialog, showNotify, showToast} from 'vant'
 import {getRecommendedLocation} from "@/api/haikang/index";
 import {
   createReturnFirstSeed,
   getReturnTaskBinList,
   returnTaskAbandon,
-  returnTaskFirstStepComplete
 } from "@/api/returnTask/index";
+import ReturnCallHikCartPopup from '@/views/inventory/returnTask/components/ReturnCallHikCartPopup.vue'
 
 const router = useRouter()
 const route = useRoute()
@@ -305,41 +306,20 @@ const getTaskBinList = () => {
 const onClickRightIcon = () => {
   modeTrueFalseBy.value = true
 }
-// 完成任务
+
+const callHikCartRef = ref(null)
+
+// 完成任务:打开呼叫小车(选站点)弹层
 const onComplete = () => {
-  if(binList.value.length==0){
+  if (binList.value.length === 0) {
     scanError()
     tips.value = '当前未扫描商品,请先扫描商品条码'
     showToast({duration: 3000, message: tips.value})
     return
   }
-  showConfirmDialog({
-    title: '温馨提示',
-    message:
-        '您正在进行完成操作,是否立即呼叫小车?',
-    confirmButtonText: '立即呼叫',
-    cancelButtonText: '不呼叫',
-  })
-    .then(() => {
-      _returnTaskFirstStepComplete(true)
-    })
-    .catch(() => {
-      _returnTaskFirstStepComplete(false)
-    });
-}
-const _returnTaskFirstStepComplete=(callHikQuickIn)=>{
-  showLoading()
-  returnTaskFirstStepComplete({taskNo:taskNo.value,callHikQuickIn}).then(res => {
-    scanSuccess()
-    if(callHikQuickIn){
-      router.replace({ name: 'ReturnTask', query: { code:taskNo.value,container:containerNo.value } });
-    }else {
-      router.push({name: 'ReturnList'})
-    }
-  }).catch(err=>{
-    scanError()
-  }).finally(_ => {
-    closeLoading()
+  callHikCartRef.value?.open({
+    taskNo: taskNo.value,
+    containerCode: containerNo.value,
   })
 }
 // 数据刷新

+ 337 - 0
src/views/inventory/returnTask/components/ReturnCallHikCartPopup.vue

@@ -0,0 +1,337 @@
+<template>
+  <van-popup
+    v-model:show="visible"
+    position="center"
+    round
+    :close-on-click-overlay="false"
+    class="return-call-hik-popup"
+  >
+    <div class="return-call-hik-panel">
+      <div class="return-call-hik-header">
+        <div class="return-call-hik-title">{{ headerTitle }}</div>
+      </div>
+      <div class="return-call-hik-list-wrap">
+        <van-radio-group v-model="selectedStationCode" class="return-call-hik-radio-group">
+          <div
+            v-for="s in stationList"
+            :key="s.code"
+            class="return-call-hik-item"
+            :class="{ 'return-call-hik-item--active': selectedStationCode === s.code }"
+            role="button"
+            @click="selectedStationCode = s.code"
+          >
+            <van-radio :name="s.code" checked-color="#3f8dff" icon-size="16px" />
+            <div class="return-call-hik-item-body">
+              <span class="return-call-hik-code">{{ s.code }}</span>
+              <!-- 站点状态:接口里是数字,这里直接写死两种 -->
+              <span v-if="s.status === 0" class="return-call-hik-tag return-call-hik-tag--idle">空闲</span>
+              <span v-else-if="s.status === 1" class="return-call-hik-tag return-call-hik-tag--busy">繁忙</span>
+            </div>
+          </div>
+        </van-radio-group>
+      </div>
+      <div class="return-call-hik-actions">
+        <van-button
+          class="return-call-hik-btn return-call-hik-btn--ghost"
+          round
+          plain
+          hairline
+          type="default"
+          @click="onNoCall"
+        >
+          不呼叫
+        </van-button>
+        <van-button
+          class="return-call-hik-btn return-call-hik-btn--primary"
+          round
+          type="primary"
+          @click="onCallNow"
+        >
+          立即呼叫小车
+        </van-button>
+      </div>
+    </div>
+  </van-popup>
+</template>
+
+<script setup>
+import {ref} from 'vue'
+import {useRouter} from 'vue-router'
+import {showToast} from 'vant'
+import {closeLoading, showLoading} from '@/utils/loading'
+import {scanError, scanSuccess} from '@/utils/android'
+import {batchCreateHikBoxOutboundTask, getHikUsableStationList} from '@/api/haikang/index'
+import {getReturnTaskBinList, returnTaskFirstStepComplete} from '@/api/returnTask/index'
+
+const props = defineProps({
+  warehouse: {type: String, required: true},
+  headerTitle: {
+    type: String,
+    default: '分格口完成,呼叫小车请选择海康拣货站点',
+  },
+})
+
+const router = useRouter()
+
+const visible = ref(false)
+const stationList = ref([])
+const selectedStationCode = ref('')
+// 弹层打开到关闭之间要用的上下文(父组件只调 open,不重复传参)
+const pendingTaskNo = ref('')
+const pendingContainerCode = ref('')
+const pendingBoxCodeList = ref([])
+
+// 交互失败既要 Toast,也要给 PDA 扫码枪那边一个失败信号(和原逻辑一致)
+function scanFail(message) {
+  scanError()
+  showToast({duration: 3000, message})
+}
+
+function resetSession() {
+  pendingTaskNo.value = ''
+  pendingContainerCode.value = ''
+  pendingBoxCodeList.value = []
+  stationList.value = []
+  selectedStationCode.value = ''
+}
+
+// 格口行里的 containerCode 可能带后缀(如箱号-格),呼车只要物理箱号:取第一个 '-' 前面那段,再去重
+function boxCodesFromBinRows(rows) {
+  const set = new Set()
+  for (const item of rows) {
+    const raw = item.containerCode
+    if (!raw) continue
+    const base = String(raw).split('-')[0].trim()
+    if (base) set.add(base)
+  }
+  return [...set]
+}
+
+// taskNo:还库任务号;containerCode:选填,呼车成功后回还库任务页要带到 query 里做筛选/高亮
+const open = async ({taskNo, containerCode = ''}) => {
+  if (!taskNo) {
+    showToast({duration: 3000, message: '任务号缺失'})
+    return
+  }
+
+  resetSession()
+  pendingTaskNo.value = taskNo
+  pendingContainerCode.value = containerCode
+
+  showLoading()
+  try {
+    const binRes = await getReturnTaskBinList({taskNo})
+    const boxCodeList = boxCodesFromBinRows(binRes.data || [])
+    if (!boxCodeList.length) {
+      scanFail('未获取到料箱号,请先完成格口分配')
+      return
+    }
+    pendingBoxCodeList.value = boxCodeList
+
+    const stationRes = await getHikUsableStationList({
+      warehouse: props.warehouse,
+      typeEnum: 'HIK_PICKING_STATION',
+    })
+    const rawList = stationRes.data || []
+    if (!rawList.length) {
+      scanFail('暂无可选站点')
+      return
+    }
+    // status:0 空闲 1 繁忙,把空闲排前面少点一次
+    const sorted = [...rawList].sort(
+      (a, b) => (a.status === 0 ? 0 : 1) - (b.status === 0 ? 0 : 1),
+    )
+    stationList.value = sorted
+    selectedStationCode.value = (sorted.find((s) => s.status === 0) || sorted[0])?.code || ''
+
+    visible.value = true
+  } catch (e) {
+    scanFail(e.message || '加载失败')
+  } finally {
+    closeLoading()
+  }
+}
+
+// 不呼海康:只走还库第一步完成,回到列表
+const onNoCall = async () => {
+  visible.value = false
+  const taskNo = pendingTaskNo.value
+  if (!taskNo) return
+
+  showLoading()
+  try {
+    await returnTaskFirstStepComplete({taskNo, callHikQuickIn: false})
+    scanSuccess()
+    router.push({name: 'ReturnList'})
+  } catch {
+    // 接口 catch 里原先就不弹 Toast,只震动
+    scanError()
+  } finally {
+    closeLoading()
+    resetSession()
+  }
+}
+
+// 先批量下发出库任务(呼车),再标记第一步完成,跳回还库任务页继续扫
+const onCallNow = async () => {
+  if (!selectedStationCode.value) {
+    showToast({duration: 3000, message: '请选择站点'})
+    return
+  }
+  const taskNo = pendingTaskNo.value
+  const boxCodeList = pendingBoxCodeList.value
+  if (!taskNo || !boxCodeList.length) {
+    showToast({duration: 3000, message: '数据异常,请重试'})
+    return
+  }
+
+  visible.value = false
+  showLoading()
+  try {
+    await batchCreateHikBoxOutboundTask({
+      boxCodeList,
+      warehouse: props.warehouse,
+      deviceType: 'HIK_PICKING_STATION',
+      stationCode: selectedStationCode.value,
+    })
+    await returnTaskFirstStepComplete({taskNo, callHikQuickIn: true})
+    scanSuccess()
+    router.replace({
+      name: 'ReturnTask',
+      query: {code: taskNo, container: pendingContainerCode.value},
+    })
+  } catch (e) {
+    scanFail(e.message || '操作失败')
+  } finally {
+    closeLoading()
+    resetSession()
+  }
+}
+
+defineExpose({open})
+</script>
+
+<style scoped lang="scss">
+$rh-blue: #3f8dff;
+$rh-border: #ebedf0;
+
+.return-call-hik-popup {
+  background: transparent;
+}
+
+.return-call-hik-panel {
+  width: calc(100vw - 56px);
+  max-width: 296px;
+  box-sizing: border-box;
+  border-radius: 10px;
+  overflow: hidden;
+  background: #fff;
+  box-shadow: 0 2px 14px rgba(0, 0, 0, 0.12);
+}
+
+.return-call-hik-header {
+  padding: 10px 12px;
+  text-align: center;
+  color: #fff;
+  background: $rh-blue;
+}
+
+.return-call-hik-title {
+  margin: 0;
+  font-size: 13px;
+  font-weight: 600;
+  line-height: 1.35;
+}
+
+.return-call-hik-list-wrap {
+  padding: 8px;
+  max-height: min(32vh, 188px);
+  overflow-y: auto;
+  -webkit-overflow-scrolling: touch;
+  background: #f7f8fa;
+}
+
+.return-call-hik-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 6px;
+  padding: 8px;
+  background: #fff;
+  border: 1px solid $rh-border;
+  border-radius: 6px;
+
+  &:last-child {
+    margin-bottom: 0;
+  }
+
+  &--active {
+    border-color: $rh-blue;
+    background: #f0f6ff;
+  }
+}
+
+.return-call-hik-item-body {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 6px;
+  min-width: 0;
+}
+
+.return-call-hik-code {
+  font-size: 14px;
+  font-weight: 600;
+  color: #323233;
+}
+
+.return-call-hik-tag {
+  flex-shrink: 0;
+  padding: 1px 6px;
+  font-size: 10px;
+  font-weight: 600;
+  border-radius: 4px;
+
+  &--idle {
+    color: #07c160;
+    background: #e6f7ed;
+  }
+
+  &--busy {
+    color: #ed6a0c;
+    background: #fff3e8;
+  }
+}
+
+.return-call-hik-actions {
+  display: flex;
+  gap: 8px;
+  padding: 10px;
+  border-top: 1px solid $rh-border;
+
+  :deep(.return-call-hik-btn) {
+    flex: 1;
+    height: 36px;
+    font-size: 13px;
+    font-weight: 600;
+  }
+
+  :deep(.return-call-hik-btn--ghost) {
+    color: #646566;
+  }
+
+  :deep(.return-call-hik-btn--primary.van-button--primary) {
+    background: $rh-blue;
+    border-color: $rh-blue;
+  }
+}
+
+:deep(.return-call-hik-radio-group .van-radio) {
+  margin-right: 0;
+}
+
+:deep(.return-call-hik-radio-group .van-radio__icon) {
+  line-height: 1;
+}
+</style>

+ 57 - 23
src/views/inventory/returnTask/list/index.vue

@@ -28,8 +28,8 @@
                   <th>任务号</th>
                   <th style="width: 50px">状态</th>
                   <th style="width: 75px">创建日期</th>
-                  <th style="width: 55px">剩余数量</th>
-                  <th style="width: 70px">操作</th>
+                  <th style="width: 50px">剩余数</th>
+                  <th style="width: 80px">操作</th>
                 </tr>
                 </thead>
                 <tbody>
@@ -39,8 +39,12 @@
                   <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="onComplete(row)" v-if="row.status=='CREATED'" >作业</van-button>
-                    <van-button type="primary" size="mini" plain @click="linkTask(row)">查看</van-button>
+                    <div class="op-btns">
+                      <van-button type="primary" size="mini" plain @click="onComplete(row)" v-if="row.status=='CREATED'" >作业</van-button>
+                    
+                      <van-button type="primary" size="mini" plain @click="linkTask(row)">查看</van-button>
+                      <van-button type="success" size="mini" plain @click="onTaskComplete(row)" v-if="row.status === 'WORKING'">完成</van-button>
+                    </div>
                   </td>
                 </tr>
                 <tr v-if="taskList.length === 0 && !loading">
@@ -59,6 +63,7 @@
       </div>
     </div>
     <input-barcode ref="inputBarcodeRef" @setBarcode="setBarcode" />
+    <ReturnCallHikCartPopup ref="callHikCartRef" :warehouse="warehouse" />
   </div>
 </template>
 
@@ -66,12 +71,13 @@
 import { computed, onMounted, ref, nextTick } from 'vue'
 import { getHeader, goBack, scanError, scanSuccess } from '@/utils/android'
 import { useStore } from '@/store/modules/user'
-import {showConfirmDialog, showToast} from 'vant'
+import {showConfirmDialog, showNotify} 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, returnTaskFirstStepComplete} from "@/api/returnTask/index.ts"
+import {createReturnTask, getReturnTaskPage, returnTaskComplete} from "@/api/returnTask/index.ts"
+import ReturnCallHikCartPopup from '@/views/inventory/returnTask/components/ReturnCallHikCartPopup.vue'
 const router = useRouter()
 try {
   getHeader()
@@ -196,28 +202,49 @@ const linkTask = (item) => {
   const name = item.status === 'WORKING' ? 'ReturnTask' : 'ReturnBin'
   router.push({ name, query: { code: item.taskNo, container: item.containerCode, status: item.status } })
 }
-// 完成任务
+
+const callHikCartRef = ref(null)
+
+// 作业:与分格口页共用呼叫小车弹层(选站点、批量出库 / 不呼叫)
 const onComplete = (item) => {
-  showConfirmDialog({
-    title: '温馨提示',
-    message:
-        '您正在进行作业操作,是否立即呼叫小车?',
-    confirmButtonText: '立即呼叫',
+  if (!item.taskNo) {
+    showNotify({ type: 'danger', duration: 3000, message: '任务号缺失' })
+    scanError()
+    return
+  }
+  callHikCartRef.value?.open({
+    taskNo: item.taskNo,
+    containerCode: item.containerCode,
   })
-      .then(() => {
-        _returnTaskFirstStepComplete(true,item)
-      })
 }
-const _returnTaskFirstStepComplete=(callHikQuickIn,item)=>{
-  showLoading()
-  returnTaskFirstStepComplete({taskNo:item.taskNo,callHikQuickIn}).then(res => {
-    scanSuccess()
-    router.replace({ name: 'ReturnTask', query: { code:item.taskNo,container:item.containerNo } });
-  }).catch(err=>{
+
+// 列表页完成还库任务(与 task/index 一致)
+const onTaskComplete = (item) => {
+  if (!item.taskNo) {
+    showNotify({ type: 'danger', duration: 3000, message: '任务号缺失' })
     scanError()
-  }).finally(_ => {
-    closeLoading()
+    return
+  }
+  showConfirmDialog({
+    title: '温馨提示',
+    message: '确认完成该还库任务?',
   })
+    .then(() => {
+      showLoading()
+      returnTaskComplete({ taskNo: item.taskNo })
+        .then(() => {
+          closeLoading()
+          scanSuccess()
+          showNotify({ type: 'success', duration: 3000, message: '任务已完成' })
+          onRefresh()
+        })
+        .catch((err) => {
+          closeLoading()
+          scanError()
+          showNotify({ type: 'danger', duration: 3000, message: err.message || '完成任务失败' })
+        })
+    })
+    .catch(() => {})
 }
 
 window.onRefresh = onRefresh
@@ -243,4 +270,11 @@ window.onRefresh = onRefresh
       .content
         flex: 1
         font-size: 13px
+
+        .op-btns
+          display: flex
+          flex-wrap: wrap
+          gap: 4px
+          justify-content: center
+          align-items: center
 </style>

+ 57 - 2
src/views/inventory/returnTask/task/index.vue

@@ -12,7 +12,12 @@
         <div style="color: #fff">返回</div>
       </template>
       <template #right>
-        <div style="color: #fff;line-height: 46px " @click="onComplete">料箱回库</div>
+        <div style="display: flex; align-items: center;">
+          <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>
+        </div>
       </template>
     </van-nav-bar>
     <div class="move-stock">
@@ -79,6 +84,13 @@
         </table>
       </div>
     </div>
+    <van-action-sheet
+        v-model:show="modeTrueFalseBy"
+        :actions="actions"
+        cancel-text="取消"
+        close-on-click-action
+        @select="onSelectMode"
+    />
     <!--  单据选择-->
     <van-action-sheet :show="lotBarcodeTrueFalseBy" cancel-text="取消" description="请选择商品批次"
                       close-on-click-action
@@ -110,10 +122,11 @@ import {androidFocus, getHeader, goBack, scanError, scanSuccess} from '@/utils/a
 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 {showConfirmDialog, showNotify} from 'vant'
 import { barcodeToUpperCase } from '@/utils/dataType'
 import {
   getReturnTaskBinList,
+  returnTaskComplete,
   updateReturnTaskUsedQty
 } from "@/api/returnTask/index";
 import { movementReturn } from '@/api/check/index'
@@ -171,6 +184,19 @@ const orangeHighlightSet = ref(new Set())
 // 高亮状态:绿色高亮(条码匹配)
 const greenHighlightSet = ref(new Set())
 
+const actions = [
+  { name: '完成任务', key: 'complete' }
+]
+const modeTrueFalseBy = ref(false)
+const onClickRightIcon = () => {
+  modeTrueFalseBy.value = true
+}
+const onSelectMode = (value) => {
+  if (value.key === 'complete') {
+    onTaskComplete()
+  }
+}
+
 // 检查是否应该高亮
 const isContainerHighlighted = (containerCode) => orangeHighlightSet.value?.has(containerCode)
 const isBarcodeHighlighted = (containerCode, lotNum) => greenHighlightSet.value?.has(`${containerCode}-${lotNum}`)
@@ -571,6 +597,35 @@ const onComplete = () => {
       .catch(() => {})
 }
 
+// 完成任务
+const onTaskComplete = () => {
+  if (!taskNo.value) {
+    showNotify({ type: 'danger', duration: 3000, message: '任务号缺失' })
+    scanError()
+    return
+  }
+  showConfirmDialog({
+    title: '温馨提示',
+    message: '确认完成该还库任务?',
+  })
+      .then(() => {
+        showLoading()
+        returnTaskComplete({ taskNo: taskNo.value })
+            .then(() => {
+              closeLoading()
+              scanSuccess()
+              showNotify({ type: 'success', duration: 3000, message: '任务已完成' })
+              router.push({ name: 'ReturnList' })
+            })
+            .catch(err => {
+              closeLoading()
+              scanError()
+              showNotify({ type: 'danger', duration: 3000, message: err.message || '完成任务失败' })
+            })
+      })
+      .catch(() => {})
+}
+
 // 料箱回库处理(使用上架页面的逻辑)
 const setGoBack = async (item) => {
   if (item.active === '1') {