Преглед изворни кода

Merge remote-tracking branch 'origin/master'

zengjun пре 6 месеци
родитељ
комит
e3b87d10ca
38 измењених фајлова са 4189 додато и 122 уклоњено
  1. 96 0
      src/App.vue
  2. 30 0
      src/api/container/index.ts
  3. 21 0
      src/api/inventory/index.ts
  4. 2 1
      src/api/picking/index.ts
  5. 24 0
      src/api/processing/index.ts
  6. 28 0
      src/api/putaway/index.ts
  7. 14 0
      src/api/takeDelivery/index.ts
  8. BIN
      src/assets/login_bg.jpg
  9. 68 0
      src/components/SelectWarehouse.vue
  10. 136 0
      src/hooks/basic/menu.js
  11. 33 3
      src/router/index.ts
  12. 35 0
      src/types/container.ts
  13. 39 0
      src/types/inventory.ts
  14. 22 0
      src/types/putaway.ts
  15. 11 1
      src/utils/dataType.js
  16. 9 0
      src/utils/date.ts
  17. 214 0
      src/views/equipment/container/bindUnbind/index.vue
  18. 13 1
      src/views/haikang/putaway/dispatch/index.vue
  19. 2 0
      src/views/haikang/putaway/putaway/index.vue
  20. 80 0
      src/views/inbound/putaway/components/LocationList.vue
  21. 612 0
      src/views/inbound/putaway/task/index.vue
  22. 2 2
      src/views/inbound/takeDelivery/task/index.vue
  23. 215 42
      src/views/index.vue
  24. 0 0
      src/views/inventory/moveTask/down/index.vue
  25. 0 0
      src/views/inventory/moveTask/list/index.vue
  26. 0 0
      src/views/inventory/moveTask/putaway/index.vue
  27. 861 0
      src/views/inventory/transfer/index.vue
  28. 19 6
      src/views/login/login.vue
  29. 166 57
      src/views/outbound/check/activity/index.vue
  30. 7 0
      src/views/outbound/check/components/BatchPacking.vue
  31. 118 0
      src/views/outbound/check/components/CheckBarcodeCombine.vue
  32. 6 0
      src/views/outbound/check/components/CheckPacking.vue
  33. 8 4
      src/views/outbound/check/large/index.vue
  34. 34 2
      src/views/outbound/picking/list/index.vue
  35. 3 3
      src/views/outbound/picking/task/index.vue
  36. 183 0
      src/views/processing/register/components/addRegisterType.vue
  37. 236 0
      src/views/processing/register/index.vue
  38. 842 0
      src/views/robot/takeDelivery/task/index.vue

+ 96 - 0
src/App.vue

@@ -24,6 +24,102 @@ router.afterEach(() => {
 })
 </script>
 <style >
+.flex {
+  display: flex;
+}
+
+.flexColumn {
+  flex-direction: column;
+}
+
+.center {
+  align-items: center;
+  justify-content: center;
+}
+
+.alignCenter {
+  align-items: center;
+}
+
+.alignStart {
+  align-items: flex-start;
+}
+
+.alignEnd {
+  align-items: flex-end;
+}
+
+.spaceBetween {
+  justify-content: space-between;
+}
+
+.spaceEvenly {
+  justify-content: space-evenly;
+}
+
+.spaceAround {
+  justify-content: space-around;
+}
+
+.spaceCenter {
+  justify-content: center;
+}
+
+.justifyEnd {
+  justify-content: flex-end;
+}
+
+.allFlex {
+  flex: 1;
+}
+
+.flexWrap {
+  flex-wrap: wrap;
+}
+
+.flexNoWrap {
+  flex-wrap: nowrap;
+}
+
+.width1 {
+  width: 10%;
+}
+
+.width2 {
+  width: 20%;
+}
+
+.width3 {
+  width: 30%;
+}
+
+.width4 {
+  width: 40%;
+}
+
+.width5 {
+  width: 50%;
+}
+
+.width6 {
+  width: 60%;
+}
+
+.width7 {
+  width: 70%;
+}
+
+.width8 {
+  width: 80%;
+}
+
+.width9 {
+  width: 90%;
+}
+
+.width10 {
+  width: 100%;
+}
 
 
 

+ 30 - 0
src/api/container/index.ts

@@ -0,0 +1,30 @@
+// @ts-ignore
+import request from '@/utils/request'
+// @ts-ignore
+import { bindContainerLocationType, unbindContainerLocationType } from '@/types/container'
+
+/**
+ * 容器绑定库位
+ * @param data
+ */
+export function bindContainerLocation(data:bindContainerLocationType) {
+  return request({
+    url: '/api/wms/container/location/relation/bindContainerLocation',
+    method: 'post',
+    data:JSON.stringify(data),
+  })
+}
+
+
+/**
+ * 解绑容器库位
+ * @param data
+ */
+export function unbindContainerLocation(data:unbindContainerLocationType) {
+  return request({
+    url: '/api/wms/container/location/relation/unbindContainerLocation',
+    method: 'post',
+    data:JSON.stringify(data),
+  })
+}
+

+ 21 - 0
src/api/inventory/index.ts

@@ -0,0 +1,21 @@
+// @ts-ignore
+import request from '@/utils/request'
+// @ts-ignore
+import { getInventoryType, inventoryMovementType } from '@/types/inventory'
+//获取库存信息
+export function getInventory(params:getInventoryType) {
+  return request({
+    url: 'api/wms/inventory/get',
+    method: 'get',
+    params
+  })
+}
+
+// 执行移库
+export function inventoryMovement(data:inventoryMovementType) {
+  return request({
+    url: '/api/wms/movement',
+    method: 'post',
+    data
+  })
+}

+ 2 - 1
src/api/picking/index.ts

@@ -92,7 +92,8 @@ export function setPickingDetail(data: any[]) {
   return request({
     url: 'api/wms/picking/details',
     method: 'post',
-    data:JSON.stringify(data)
+    data:JSON.stringify(data),
+    params:{skipContainerVerify:false}
   })
 }
 

+ 24 - 0
src/api/processing/index.ts

@@ -0,0 +1,24 @@
+// @ts-ignore
+import request from '@/utils/request'
+/**
+ * 加工-获取加工类型
+ * @param params
+ */
+export function getProcessingTypeList() {
+  return request({
+    url: 'api/device/check/processing/typeList',
+    method: 'get',
+  })
+}
+/**
+ * 加工-创建加工单
+ * @param params
+ */
+export function createProcessing(data:any) {
+  return request({
+    url: 'api/device/check/processing/reference',
+    method: 'post',
+    data,
+  })
+}
+

+ 28 - 0
src/api/putaway/index.ts

@@ -0,0 +1,28 @@
+// @ts-ignore
+import request from '@/utils/request'
+// @ts-ignore
+import { batchPutawayType, getWaitPutawayInfoType } from '@/types/putaway'
+
+/**
+ * 批量上架
+ * @param data
+ */
+export function batchPutaway(data:batchPutawayType) {
+  return request({
+    url: '/api/wms/inbound/batch',
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 获取待上架信息
+ * @param params
+ */
+export function getWaitPutawayInfo(params:getWaitPutawayInfoType) {
+  return request({
+    url: '/api/wms/inbound/task',
+    method: 'get',
+    params
+  })
+}

+ 14 - 0
src/api/takeDelivery/index.ts

@@ -99,4 +99,18 @@ export function setReceiving(data:setReceivingType) {
 }
 
 
+/**
+ * 完成收货
+ * @param params
+ */
+export function setBotReceiving(data:setReceivingType) {
+  return request({
+    url: '/api/wms/receiving/botReceiving',
+    method: 'post',
+    data
+  })
+}
+
+
+
 

BIN
src/assets/login_bg.jpg


+ 68 - 0
src/components/SelectWarehouse.vue

@@ -0,0 +1,68 @@
+<template>
+  <van-popup v-model:show="warehouseTrueFalseBy" position="center" round :close-on-click-overlay="false">
+    <div class="select-warehouses">
+      <div class="title">
+        工作仓库设置
+      </div>
+      <van-radio-group v-model="code">
+        <van-cell-group inset>
+          <van-cell :title="value" clickable v-for="(value, key) in warehousesMap" @click="groupChange(key)">
+            <template #right-icon>
+              <van-radio :name="key" />
+            </template>
+          </van-cell>
+        </van-cell-group>
+      </van-radio-group>
+      <div class="btn" @click="onConfirm"> 确定</div>
+    </div>
+  </van-popup>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue'
+import { useStore } from '@/store/modules/user'
+// 创建响应式数据
+const warehouseTrueFalseBy = ref(false)
+const warehousesMap = ref({
+  'WH99': '测试仓库(WH99)',
+  'WH01': '九干仓(WH01)',
+  'WH02': '新浜一仓(WH02)',
+  'WH10': '新浜二仓(WH10)',
+})
+const code = ref('')
+const show = (warehouse) => {
+  warehouseTrueFalseBy.value = true
+  code.value = warehouse
+}
+const groupChange = (n) => {
+  code.value = n
+}
+// 确认按钮事件
+const storeUser = useStore()
+const emit = defineEmits(['setWarehouse'])
+const onConfirm = () => {
+  storeUser.setToken({ warehouse: code.value, token: storeUser.token })
+  emit('setWarehouse', code.value)
+  warehouseTrueFalseBy.value = false
+}
+defineExpose({ show })
+</script>
+<style scoped lang="sass">
+.select-warehouses
+  padding: 10px 15px
+  width: 300px
+
+  .title
+    font-size: 16px
+    font-weight: 500
+    padding-bottom: 10px
+
+  .radio-item
+    margin-bottom: 10px
+
+  .btn
+    color: #3c9cff
+    line-height: 30px
+    text-align: center
+    cursor: pointer
+</style>

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

@@ -0,0 +1,136 @@
+import {
+  reactive,
+  ref,
+} from 'vue'
+
+export default function() {
+
+  const menuList = reactive([
+    {
+      title: '工作台',
+      id: 0,
+      icon: 'home-o',
+      menu: [
+        {
+          title: '入库',
+          children: [
+            {
+              title: '收货',
+              icon: 'newspaper-o',
+              path: 'take-delivery',
+            },
+            {
+              title: '盲扫',
+              icon: 'newspaper-o',
+              path: 'blind-receiving',
+            },
+            {
+              title: '宝时快上',
+              icon: 'newspaper-o',
+              path: 'putaway',
+            },
+          ],
+        },
+        {
+          title: '出库',
+          children: [
+            {
+              title: '拣货',
+              icon: 'newspaper-o',
+              path: 'picking',
+            },
+            {
+              title: '拣货(巷道)',
+              icon: 'newspaper-o',
+              path: 'picking-aisle',
+            },
+            {
+              title: '活动单复核',
+              icon: 'newspaper-o',
+              path: 'check-activity',
+            },
+            {
+              title: 'B2B复核',
+              icon: 'newspaper-o',
+              path: 'check-large',
+            },
+            {
+              title: '复核还库',
+              icon: 'newspaper-o',
+              path: 'check-move-stock',
+            },
+          ],
+        },
+        {
+          title: '库存',
+          children: [
+            {
+              title: '移库任务',
+              icon: 'newspaper-o',
+              path: 'move-list',
+            },
+            {
+              title: '移库',
+              icon: 'newspaper-o',
+              path: 'inventory-transfer',
+            },
+          ],
+        },
+        {
+          title: '机器人',
+          children: [
+            {
+              title: '海康快上',
+              icon: 'newspaper-o',
+              path: 'hik-putaway-allocation',
+            },
+            {
+              title: '海康上架',
+              icon: 'newspaper-o',
+              path: 'hik-putaway',
+            },
+            {
+              title: '海康入库',
+              icon: 'newspaper-o',
+              path: 'hik-box-return',
+            },
+            {
+              title: '智能仓收货',
+              icon: 'newspaper-o',
+              path: 'robot-take-delivery',
+            },
+          ],
+        },
+        {
+          title: '其他',
+          children: [
+            {
+              title: '加工单',
+              icon: 'newspaper-o',
+              path: 'processing',
+            },
+            {
+              title: '计件面板',
+              icon: 'newspaper-o',
+              path: 'piece-dashboard',
+            },
+            {
+              title: '退货登记',
+              icon: 'newspaper-o',
+              path: 'returned-register',
+            },
+            {
+              title: '容器移动',
+              icon: 'newspaper-o',
+              path: 'container-operation',
+            },
+
+          ],
+        },
+      ],
+    },
+  ])
+  return {
+    menuList,
+  }
+}

+ 33 - 3
src/router/index.ts

@@ -79,6 +79,12 @@ const routes: RouteRecordRaw[] = [
     meta:{title:'海康-入库'},
     component: () => import('@/views/haikang/boxReturn/boxReturn/index.vue')
   },
+  {
+    path: '/robot-take-delivery',
+    name: 'RobotTakeDelivery',
+    meta:{title:'智能仓收货'},
+    component: () => import('@/views/robot/takeDelivery/task/index.vue')
+  },
   {
     path: '/check-activity',
     name: 'CheckActivity',
@@ -95,19 +101,19 @@ const routes: RouteRecordRaw[] = [
     path: '/move-list',
     name: 'MoveList',
     meta:{title:'移库列表'},
-    component: () => import('@/views/transfer/move/list/index.vue')
+    component: () => import('@/views/inventory/moveTask/list/index.vue')
   },
   {
     path: '/move-down',
     name: 'MoveDown',
     meta:{title:'移库下架'},
-    component: () => import('@/views/transfer/move/down/index.vue')
+    component: () => import('@/views/inventory/moveTask/down/index.vue')
   },
   {
     path: '/move-putaway',
     name: 'MovePutaway',
     meta:{title:'移库上架'},
-    component: () => import('@/views/transfer/move/putaway/index.vue')
+    component: () => import('@/views/inventory/moveTask/putaway/index.vue')
   },
   {
     path: '/returned-register',
@@ -115,6 +121,30 @@ const routes: RouteRecordRaw[] = [
     meta:{title:'退货登记'},
     component: () => import('@/views/returned/register/index.vue')
   },
+  {
+    path: '/processing',
+    name: 'Processing',
+    meta:{title:'加工登记'},
+    component: () => import('@/views/processing/register/index.vue')
+  },
+  {
+    path: '/putaway',
+    name: 'Putaway',
+    meta:{title:'宝时快上'},
+    component: () => import('@/views/inbound/putaway/task/index.vue')
+  },
+  {
+    path: '/container-operation',
+    name: 'containerOperation',
+    meta:{title:'容器移动'},
+    component: () => import('@/views/equipment/container/bindUnbind/index.vue')
+  },
+  {
+    path: '/inventory-transfer',
+    name: 'InventoryTransfer',
+    meta:{title:'移库'},
+    component: () => import('@/views/inventory/transfer/index.vue')
+  },
 ];
 
 // 创建路由实例

+ 35 - 0
src/types/container.ts

@@ -0,0 +1,35 @@
+/**
+ * 容器绑定库位
+ */
+export interface bindContainerLocationType {
+  /**
+   * 仓库ID
+   */
+  warehouseId: string;
+  /**
+   * 容器库位
+   */
+  dataList: ContainerLocationDataDTO[];
+  [property: string]: any;
+}
+
+export interface ContainerLocationDataDTO {
+  /**
+   * 容器号
+   */
+  containerCode: string;
+  /**
+   * 库位号
+   */
+  locationId: string;
+  [property: string]: any;
+}
+
+/**
+ * 解绑容器库位
+ */
+export interface unbindContainerLocationType {
+  containerCode: string;
+  warehouseId: string;
+  [property: string]: any;
+}

+ 39 - 0
src/types/inventory.ts

@@ -0,0 +1,39 @@
+
+/**
+ *  查询库存
+ * @param warehouse 仓库
+ * @param barcode  条码
+ * @param locationRegexp=^(?!STAGE_|SORTATION_|REVERSEPICK_|FJ-).*$
+ */
+export interface getInventoryType {
+  /**
+   * barcode
+   */
+  barcode: string;
+  /**
+   * location
+   */
+  location: string;
+  /**
+   * locationRegexp
+   */
+  locationRegexp: string;
+  /**
+   * warehouse
+   */
+  warehouse: string;
+  [property: string]: any;
+}
+
+export interface inventoryMovementType {
+  fmContainer?: string;
+  fmLocation?: string;
+  lotNum?: string;
+  owner?: string;
+  quantity?: number;
+  sku?: string;
+  toContainer?: string;
+  toLocation?: string;
+  warehouse?: string;
+  [property: string]: any;
+}

+ 22 - 0
src/types/putaway.ts

@@ -0,0 +1,22 @@
+
+/**
+ * 批量上架
+ */
+export interface batchPutawayType {
+  container?: string;
+  location?: string;
+  quantity?: number;
+  taskLineNo?: string;
+  taskNo?: string;
+  warehouse?: string;
+  [property: string]: any;
+}
+
+/**
+ * 上架任务列表
+ */
+export interface getWaitPutawayInfoType {
+  container: string;
+  warehouse: string;
+}
+

+ 11 - 1
src/utils/dataType.js

@@ -12,6 +12,11 @@ export function toMap(data, key, val=undefined, optional = {}) {
   }
   // 遍历数据
   data.forEach(item => {
+    // 跳过 key 值为 null、undefined 或空字符串的项
+    if (item[key] == null || item[key] === '') {
+      return;
+    }
+    
     // 如果 optional.type 是 String,则处理字符串转换
     if (optional.type === String) {
       item[key] = item[key] + '';  // 转为字符串
@@ -24,9 +29,14 @@ export function toMap(data, key, val=undefined, optional = {}) {
 
   return map;
 }
-
+//字符串分割数组
+export function toArray(code) {
+  // 使用正则表达式分割字符串,去除多余的空白字符或换行符
+  return code.split(/[,,|\n\r\s]+/).filter(Boolean); // 使用 filter 去除空元素
+}
 
  export function barcodeToUpperCase(code){
+   if(!code) return code
    if (/[a-zA-Z]/.test(code)) {
      return  code.toUpperCase();  // 强制转换为大写字母
     }

+ 9 - 0
src/utils/date.ts

@@ -31,3 +31,12 @@ export function getCurrentTime() {
   const seconds = String(now.getSeconds()).padStart(2, '0');
   return `${hours}:${minutes}:${seconds}`;
 };
+/*
+ *获取当前时间
+ */
+export function formatDate(date:any) {
+  const year = date.getFullYear(); // 获取年份
+  const month = String(date.getMonth() + 1).padStart(2, '0'); // 获取月份
+  const day = String(date.getDate()).padStart(2, '0'); // 获取日期
+  return `${year}-${month}-${day}`;
+}

+ 214 - 0
src/views/equipment/container/bindUnbind/index.vue

@@ -0,0 +1,214 @@
+
+<template>
+  <div class="box-return">
+    <van-nav-bar
+      title="容器-绑定/解绑" left-arrow fixed placeholder @click-left="goBack" @click-right="onConfirm()">
+      <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="box-return-content">
+      <div class="content-tips">
+        <div style="flex: 1"><van-notice-bar left-icon="volume-o">{{ tips }}</van-notice-bar></div>
+      </div>
+      <van-tabs v-model:active="active"   type="card" @change="activeChange">
+        <van-tab title="绑定" name="1"></van-tab>
+        <van-tab title="解绑" name="2"  ></van-tab>
+      </van-tabs>
+        <div class="content-code">
+          <div class="barcode-input" v-if="active==1">
+            <van-search
+              ref="boxRef"
+              v-model.lazy="locationNo"
+              placeholder="请扫描库位号"
+              @search="_handlerScan(locationNo)"
+              label="库位号:"
+              left-icon=""
+              :class="[scanType===1?'search-input-barcode':'','van-hairline--bottom']"
+              @focus="scanType=1"
+              autocomplete="off"
+            >
+            </van-search>
+          </div>
+          <div class="barcode-input">
+            <van-search
+              v-model.lazy="containerNo"
+              placeholder="请扫描容器号"
+              @search="_handlerScan(containerNo)"
+              label="容器号:"
+              left-icon=""
+              :class="[scanType===2?'search-input-barcode':'','van-hairline--bottom']"
+              @focus="scanType=2"
+              autocomplete="off"
+            >
+            </van-search>
+          </div>
+        </div>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import { onMounted, onUnmounted, ref } from 'vue'
+import { getHeader, goBack, scanError, scanSuccess } from '@/utils/android'
+import { closeListener, openListener, scanInit } from '@/utils/keydownListener'
+import { barcodeToUpperCase } from '@/utils/dataType'
+import { showConfirmDialog, showNotify } from 'vant'
+import { useStore } from '@/store/modules/user'
+import { closeLoading, showLoading } from '@/utils/loading'
+import { bindContainerLocation, unbindContainerLocation } from '@/api/container'
+const tips=ref('请扫描库位号')
+const scanType = ref(1)
+const locationNo=ref('')
+const containerNo=ref('')
+try {
+  getHeader()
+} catch (error) {
+}
+// 页面初始化
+onMounted(() => {
+  openListener()
+  scanInit(_handlerScan)
+})
+const store = useStore()
+const warehouse = store.warehouse
+const active=ref('1')
+const activeChange=(value)=>{
+  scanType.value=Number(value)
+  locationNo.value=''
+  containerNo.value=''
+  tips.value=active.value=='1'?'请扫描库位号':'请扫描容器号'
+}
+// 扫描条码监听
+const _handlerScan = (code) => {
+  if(!code) return
+  if (scanType.value == 1) {
+      locationNo.value = barcodeToUpperCase(code)
+      scanType.value=2
+      tips.value='请扫描库位号'
+  }else if(scanType.value == 2){
+    if(code){
+      containerNo.value = barcodeToUpperCase(code)
+      onConfirm()
+    }
+  }
+}
+const onConfirm=()=>{
+  if(!locationNo.value && active.value!=2){
+    tips.value='请先扫描库位号'
+    scanType.value=1
+    showNotify({ type: 'danger', duration: 3000, message:'请先扫描库位号' })
+    scanError()
+    return
+  }
+  if(!containerNo.value){
+    tips.value='请先扫描容器号'
+    scanType.value=2
+    showNotify({ type: 'danger', duration: 3000, message:'请先扫描容器号' })
+    scanError()
+    return
+  }
+  const message=active.value==1?'您正在进行绑定是否继续':'您正在进行解绑是否继续'
+  showConfirmDialog({
+    title: '温馨提示',
+    message
+  }).then(() => {
+    if(active.value==1){
+      _bindContainerLocation()
+    }else {
+      _unbindContainerLocation()
+    }
+  }).catch(() => {})
+}
+const _bindContainerLocation=()=>{
+  const data={
+    warehouseId:warehouse,
+    dataList:[
+      {
+        containerCode:containerNo.value,
+        locationId:locationNo.value,
+      }
+    ]
+  }
+  showLoading()
+  bindContainerLocation(data).then(res=>{
+    if(res.data){
+      tips.value='请继续扫描库位号'
+      scanType.value=1
+      locationNo.value=''
+      containerNo.value=''
+      showNotify({ type: 'success', duration: 3000, message:'容器绑定成功,请继续扫描库位号' })
+      scanSuccess()
+    }
+  }).catch(err=>{
+    tips.value=err.message
+    scanError()
+  }).finally(()=>{
+    closeLoading()
+  })
+}
+const _unbindContainerLocation=()=>{
+  const params={
+    warehouseId:warehouse,
+    containerCode:containerNo.value,
+  }
+  showLoading()
+  unbindContainerLocation(params).then(res=>{
+    if(res.data){
+      tips.value='请继续扫描容器'
+      scanType.value=1
+      containerNo.value=''
+      showNotify({ type: 'success', duration: 3000, message:'容器解绑成功,请继续扫描容器号' })
+      scanSuccess()
+    }
+  }).catch(err=>{
+    tips.value=err.message
+    scanError()
+  }).finally(()=>{
+    closeLoading()
+  })
+}
+onUnmounted(() => {
+  closeListener()
+})
+// window.onRefresh = loadData
+</script>
+<style scoped lang="sass">
+.box-return
+  .box-return-content
+    .content-code
+      text-align: left
+      background: #FFFFFF
+
+      .barcode-input
+        ::v-deep(.van-search)
+          padding: 0
+          height: 60px
+
+
+        ::v-deep(.van-search__field)
+          border-bottom: 2px solid #ffffff
+
+        ::v-deep(.van-search__content)
+          background: #fff
+          align-items: center
+
+        ::v-deep(.van-field__control)
+          font-size: 15px
+          font-weight: bold
+
+
+        ::v-deep(.van-search__label)
+          font-size: 15px
+          font-weight: bold
+
+        .search-input-barcode
+          ::v-deep(.van-search__field)
+            border-bottom: 2px solid #0077ff
+            z-index: 2
+            line-height: 58px
+            height: 60px
+</style>

+ 13 - 1
src/views/haikang/putaway/dispatch/index.vue

@@ -341,7 +341,7 @@ onUnmounted(() => {
   closeListener()
 })
 
-window.onRefresh = loadData
+
 
 //删除分拨
 const onClickRight = () => {
@@ -355,6 +355,8 @@ const onClickRight = () => {
     finishTask(params).then(res=>{
       showNotify({ type: 'success', duration: 3000, message: `解绑成功` })
       scanSuccess()
+      reset()
+      loadData()
     }).catch(err=>{
       scanError()
     }).finally(() => {
@@ -362,6 +364,16 @@ const onClickRight = () => {
     })
   }).catch(() => {})
 }
+const reset=()=>{
+  containerNo.value=''
+  wallNo.value=''
+  scanType.value=3
+  searchBarcode.value = ''
+  bin.value=''
+  locationActive.value = []
+  tips.value='请扫描容器号'
+}
+window.onRefresh = loadData
 </script>
 <style scoped lang="sass">
 .container

+ 2 - 0
src/views/haikang/putaway/putaway/index.vue

@@ -126,6 +126,7 @@ const wallBinList = ref([
   ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20'],
   ['21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '40'],
   ['41', '42', '43', '44', '45', '46', '47', '48', '49', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '60'],
+  ['61', '62', '63', '64', '65', '66', '67', '68', '69', '70', '71', '72', '73', '74', '75', '76', '77', '78', '79', '80'],
 ])
 const getRows = (wall) => {
   let rows = []
@@ -543,6 +544,7 @@ window.onRefresh = loadData
               justify-content: center
               padding: 8px 0
               font-size: 14px
+              font-weight: 400
               color: #333
               position: relative
               //.wall-item-bin-text

+ 80 - 0
src/views/inbound/putaway/components/LocationList.vue

@@ -0,0 +1,80 @@
+<template>
+  <div class="move-stock-list">
+    <table class="task-table">
+      <thead>
+      <tr>
+        <th>库位</th>
+        <th>类型</th>
+        <th width="60px">数量</th>
+        <th width="60px">上线值</th>
+      </tr>
+      </thead>
+      <tbody>
+      <tr v-for="(item, index) in props.locationList" :key="index" v-if="props.locationList.length>0">
+        <td>{{ item.location }}</td>
+        <td>{{ locationType[item.type] || item.type }}</td>
+        <td>{{ item.quantity || 0 }}</td>
+        <td>{{ item.max || '无' }}</td>
+      </tr>
+      <tr v-else>
+        <td colspan="4">
+          <div>暂无数据</div>
+        </td>
+      </tr>
+      </tbody>
+    </table>
+  </div>
+</template>
+<script setup lang="ts">
+//库位类型
+const locationType = {
+  'EA': '件拣货库位',
+  'AP': '补充拣货位',
+  'CS': '箱拣货库位',
+  'HP': '快拣补货位',
+  'PC': '箱/件合并拣货库位',
+  'PT': '播种库位',
+  'RS': '存储库位',
+  'SS': '理货站',
+  'ST': '过渡库位',
+  'WB': '组装工作区',
+}
+const props = defineProps({
+  locationList: Array,
+});
+</script>
+<style scoped lang="sass">
+.move-stock-list
+  width: 100%
+  overflow-y: auto
+  max-height: 60vh
+  min-height: 100px
+
+  .move-button
+    background: #1989fa
+    color: #fff
+    width: 100%
+    height: 30px
+    font-size: 15px
+    line-height: 30px
+    font-weight: bold
+
+  .task-table, .task-table-bin, .task-table-box
+    width: 100%
+    table-layout: fixed
+    border-collapse: collapse
+    font-size: 15px
+
+  .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-bin thead
+    background-color: #3f8dff
+
+  .task-table-bin tbody
+    background: #cde7ff
+
+</style>

+ 612 - 0
src/views/inbound/putaway/task/index.vue

@@ -0,0 +1,612 @@
+<template>
+  <div class="container">
+    <van-nav-bar title="宝时快上" left-arrow fixed placeholder @click-left="goBack" z-index="100"
+                 @click-right="refresh()">
+      <template #left>
+        <van-icon name="arrow-left" size="25" />
+        <div style="color: #fff">返回</div>
+      </template>
+      <template #right>
+        <div style="color: #fff">刷新<van-icon name="replay" /></div>
+      </template>
+    </van-nav-bar>
+    <div class="take-delivery">
+      <div class="take-info">
+        <div class="take-info-no">
+          <div class="info-no-tips">
+            <div>货主:<span style="color: #333;font-weight: bold;">{{ ownerMap[taskInfo.owner] || '--' }}</span></div>
+            <div>待上架数:<span style="color: #0077ff;font-weight: bold;">{{ totalQuantity || 0 }}</span></div>
+          </div>
+        </div>
+        <div class="take-info-number">
+          <div class="info-number-left">
+            <div class="number-left-box">
+              <div>开始时间</div>
+              <div class="left-box-title">{{ currentTime }}</div>
+            </div>
+            <div class="number-left-box">
+              <div>已用时</div>
+              <div class="left-box-title">{{ formattedTime }}</div>
+            </div>
+          </div>
+          <div class="info-number-right">
+            <div>容器号</div>
+            <div style="display: flex;justify-content:center;align-items: center;">
+              <div style="flex: 1;font-size: 14px;font-weight: bold;color: #0077ff;line-height: 34px">
+                {{ containerNo || '--' }}
+              </div>
+              <div style="width:50px">
+                <van-button type="primary" size="mini" plain @click="switchTask">切换容器</van-button>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="take-barcode">
+        <div class="barcode-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"
+            @input="onAsnCancel"
+            @clear="reset"
+          >
+          </van-search>
+        </div>
+        <div class="barcode-input">
+          <van-search
+            ref="locationRef"
+            v-model="searchLocation"
+            placeholder="请扫描库位编号"
+            @search="_handlerScan(searchLocation)"
+            label="库位编号:"
+            left-icon=""
+            :class="[scanType===3?'search-input-barcode':'','van-hairline--bottom']"
+            @focus="scanType=3"
+            autocomplete="off"
+          >
+          </van-search>
+        </div>
+        <div class="barcode-input">
+          <van-search
+            ref="numberRef"
+            v-model="searchCount"
+            placeholder="请输入上架数量"
+            type="number"
+            label="上架数量:"
+            left-icon=""
+            autocomplete="off"
+            show-action
+            :min="1"
+            :max="barcodeQuantity(barcodeActiveList)"
+            @search="onConfirm"
+            :class="[scanType===4?'search-input-barcode':'','van-hairline--bottom','search-input-number']"
+            @focus="scanType=4"
+          >
+            <template #action>
+              <div style="display: flex; align-items: center;margin-left: 20px">
+                <div style="font-size: 12px">预计:</div>
+                <div style="font-size: 18px;font-weight: bold;color: #ee0a25">{{ barcodeQuantity(barcodeActiveList) }}
+                </div>
+              </div>
+            </template>
+          </van-search>
+        </div>
+      </div>
+      <div class="take-lot" v-if="barcodeActiveList.length>0">
+        <van-cell-group>
+          <div class="take-lot-title">批次信息</div>
+          <template v-for="(value, key) in lotAttributes" :key="key">
+            <van-cell v-if="barcodeActiveList[0][key]">
+              <template #title>
+                <van-icon name="warning-o" color="#ed6a0c" />
+                <span class="custom-title">{{ value.title }}</span>
+              </template>
+              <template #value>
+                <div>{{ barcodeActiveList[0][key] }}</div>
+              </template>
+            </van-cell>
+          </template>
+        </van-cell-group>
+      </div>
+      <div class="take-button">
+        <div class="btn" type="primary" size="large" round style="height: 36px" @click="onConfirm">上架</div>
+      </div>
+      <div>
+        <location-list :locationList="locationList" />
+      </div>
+    </div>
+  </div>
+  <!-- 条码输入组件 -->
+  <input-barcode :back="back" @setBarcode="setBarcode" ref="inputBarcodeRef" />
+  <!--  单据选择-->
+  <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="onDetailActive(item)">
+        <template #title>
+          {{ item[0].barcode }}({{ item[0].lotNumber }}-{{ barcodeQuantity(item) }}件)
+        </template>
+        <template #label>
+          生产日期:{{ item[0].lotAtt01 || '--' }}-失效日期:{{ item[0].lotAtt02 || '--' }}
+        </template>
+      </van-cell>
+    </van-cell-group>
+  </van-action-sheet>
+  <!--  推荐库位列表-->
+  <van-action-sheet v-model:show="locationTrueFalseBy" cancel-text="取消" description="推荐库位列表"
+                    close-on-click-action>
+    <div style="max-height: 60vh;overflow: auto;">
+      <location-list :locationList="locationList" />
+    </div>
+  </van-action-sheet>
+</template>
+
+<script setup>
+import { onMounted, onUnmounted, ref, computed } from 'vue'
+import { androidFocus, getHeader, goBack, scanError, scanSuccess } from '@/utils/android'
+import InputBarcode from '@/views/outbound/picking/components/InputBarcode.vue'
+import LocationList from '@/views/inbound/putaway/components/LocationList.vue'
+import { openListener,closeListener,scanInit } from '@/utils/keydownListener.js'
+import { useRouter } from 'vue-router'
+import { closeLoading, showLoading } from '@/utils/loading'
+import { useStore } from '@/store/modules/user'
+import { showNotify, showToast } from 'vant'
+import { getCurrentTime } from '@/utils/date'
+import { batchPutaway, getWaitPutawayInfo } from '@/api/putaway/index'
+import { barcodeToUpperCase } from '@/utils/dataType.js'
+import { getRecommendedLocation } from '@/api/haikang/index'
+import { getOwnerList } from '@/hooks/basic/index'
+
+const router = useRouter()
+const store = useStore()
+try {
+  getHeader()
+  androidFocus()
+} catch (error) {
+  router.push('/login')
+}
+// 页面初始化
+onMounted(() => {
+  openListener()
+  scanInit(_handlerScan)
+  loadData()
+})
+const warehouse = store.warehouse
+//容器号
+const containerNo = ref('')
+//数据列表
+const dataList = ref([])
+//
+const dataMap = ref({})
+//库位列表
+const locationTrueFalseBy = ref(false)
+const locationList = ref([])
+//商品条码
+const searchBarcode = ref('')
+//库位
+const searchLocation = ref('')
+//收货数量
+const searchCount = ref('')
+//收货详情
+const taskInfo = ref({})
+//开始时间
+const currentTime = ref('--')
+const scanType = ref(2)
+
+const lotAttributes = {
+  lotAtt01: { title: '生产日期' },
+  lotAtt02: { title: '失效日期' },
+  lotAtt03: { title: '入库日期' },
+  lotAtt04: { title: '生产批号' },
+  lotAtt05: { title: '属性仓' },
+  lotAtt08: { title: '质量状态' },
+}
+// 获取货主
+const { ownerMap, getOwnerData } = getOwnerList()
+getOwnerData()
+//待上架数
+const totalQuantity = computed(() => {
+  return dataList.value.reduce((sum, item) => sum + Number(item.quantity), 0)
+})
+//待上架数
+const barcodeQuantity = (list) => {
+  return list.reduce((sum, item) => sum + Number(item.quantity), 0)
+}
+const back = ref(true)
+const inputBarcodeType = ref('task')
+//输入框组件
+const inputBarcodeRef = ref(null)
+const oldSearchBarcode = ref('')
+/**
+ * 计算时分秒
+ */
+// 时器的总秒数
+let totalSeconds = ref(0)
+//时分秒
+const formattedTime = ref('00:00:00')
+let windowTimer = null // 计时器的引用
+const updateFormattedTime = () => {
+  let hours = Math.floor(totalSeconds.value / 3600)
+  let minutes = Math.floor((totalSeconds.value % 3600) / 60)
+  let seconds = totalSeconds.value % 60
+  formattedTime.value = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
+}
+// 启动计时器
+const startTimer = () => {
+  if (!windowTimer) {
+    windowTimer = setInterval(() => {
+      totalSeconds.value++
+      updateFormattedTime()
+    }, 1000)
+  }
+}
+
+// 停止计时器
+const stopTimer = () => {
+  if (windowTimer) {
+    clearInterval(windowTimer)
+    windowTimer = null
+  }
+}
+// 设置容器号
+const setBarcode = (code, type) => {
+  showLoading()
+  if (!type) { //切换任务时初始化计时器
+    stopTimer()
+    formattedTime.value = '00:00:00'
+    totalSeconds.value = 0
+  }
+  const params = { warehouse, container: code }
+  getWaitPutawayInfo(params).then(res => {
+    reset()
+    scanType.value=2
+    back.value = true
+    if (!type) {//切换任务成功重启计时器
+      currentTime.value = getCurrentTime()
+      startTimer()
+    }
+    if (res.data.length > 0) {
+      taskInfo.value = res.data[0]
+      dataList.value = res.data
+      dataMap.value = groupedData(res.data)
+      containerNo.value = code
+      scanSuccess()
+    } else {
+      reset()
+      taskInfo.value = {}
+      dataMap.value = {}
+      dataList.value = []
+      inputBarcodeRef.value?.show('', '请扫描容器号', '暂无待上架信息')
+      scanError()
+    }
+  }).catch(err => {
+    reset()
+    taskInfo.value = {}
+    dataMap.value = {}
+    dataList.value = []
+    inputBarcodeRef.value?.show('', '请扫描容器号', err.message)
+    scanError()
+  }).finally(() => {
+    closeLoading()
+  })
+}
+//根据条码批次分组数据
+const groupedData = (data) => {
+  return data.reduce((acc, item) => {
+    const key = `(${item.barcode}、${item.barcodeAs}、${item.sku})-${item.lotNumber}`
+    if (acc[key]) {
+      acc[key].push(item)
+    } else {
+      acc[key] = [item]
+    }
+    return acc
+  }, {})
+}
+//匹配待上架列表数据
+const matchingBarcodeItem = (data, barcode) => {
+  const matchingItems = []
+  for (const key in data) {
+    const barcodeList = key.match(/\((.*?)\)/)[1].split('、')
+    if (data.hasOwnProperty(key)) {
+      if (barcodeList.some(item => barcodeToUpperCase(item) === barcodeToUpperCase(barcode))) {
+        matchingItems.push(data[key])
+      }
+    }
+  }
+  return matchingItems.length > 0 ? matchingItems : []
+}
+// setBarcode()
+//切换任务
+const switchTask = () => {
+  inputBarcodeType.value = 'switchTask'
+  back.value = false
+  inputBarcodeRef.value?.show('', `请扫描容器号`, '')
+}
+
+//批次数据
+const lotBarcodeList = ref([])
+const lotBarcodeTrueFalseBy = ref(false)
+const barcodeActiveList = ref([])
+const reset = () => {
+  searchCount.value = ''
+  searchBarcode.value = ''
+  searchLocation.value = ''
+  oldSearchBarcode.value = ''
+  locationList.value = []
+  barcodeActiveList.value = []
+}
+// 选择单据
+const onDetailActive = (item) => {
+  barcodeActiveList.value = item
+  lotBarcodeTrueFalseBy.value = false
+  searchCount.value = 1
+  scanType.value = 3
+  _getRecommendedLocation(item[0].lotNumber, item[0].owner)
+  scanSuccess()
+}
+const onAsnCancel = () => {
+  if (searchBarcode.value === '' || (oldSearchBarcode.value.length != searchBarcode.value.length && oldSearchBarcode.value != '')) {
+    barcodeActiveList.value = []
+    searchCount.value = ''
+    locationList.value = []
+  }
+}
+// 扫描条码监听
+const _handlerScan = (code) => {
+  if (scanType.value == 2) {
+    searchBarcode.value = code
+    oldSearchBarcode.value = code
+    lotBarcodeList.value = matchingBarcodeItem(dataMap.value, code)
+    if (lotBarcodeList.value.length > 0) {
+      if (lotBarcodeList.value.length == 1) {
+        barcodeActiveList.value = lotBarcodeList.value[0]
+        _getRecommendedLocation(barcodeActiveList.value[0].lotNumber, barcodeActiveList.value[0].owner)
+        scanType.value = 3
+        scanSuccess()
+      } else if (lotBarcodeList.value.length > 1) {
+        locationList.value = []
+        barcodeActiveList.value = []
+        searchCount.value = ''
+        searchLocation.value = ''
+        lotBarcodeTrueFalseBy.value = true
+      }
+    } else {
+      scanError()
+      showNotify({ type: 'danger', duration: 3000, message: `${code}-商品条码不匹配,请重新扫描` })
+      reset()
+    }
+  } else if (scanType.value == 3) {
+    searchLocation.value = barcodeToUpperCase(code)
+    scanType.value = 4
+    searchCount.value = 1
+    scanSuccess()
+  }
+}
+// 获取库存数据
+const _getRecommendedLocation = async (lotNum, owner) => {
+  try {
+    const params = { warehouse, lotNum, owner }
+    const res = await getRecommendedLocation(params)
+    locationList.value = res.data
+  } catch (err) {
+    console.error(err)
+  }
+}
+const numberRef = ref(null)
+const locationRef = ref(null)
+// 完成收货校验
+const isCheck = () => {
+  if (searchLocation.value == '') {
+    locationRef.value?.focus()
+    scanError()
+    showToast({ duration: 3000, message: '请先扫描库位编号' })
+    return false
+  }
+  if (searchCount.value == '') {
+    numberRef.value?.focus()
+    scanError()
+    showToast({ duration: 3000, message: '请先输入上架数量' })
+    return false
+  }
+  const maxQuantity = barcodeQuantity(barcodeActiveList.value)
+  if (Number(searchCount.value) > maxQuantity) {
+    numberRef.value?.focus()
+    scanError()
+    showToast({ duration: 3000, message: `上架数量最大为:${maxQuantity}` })
+    return false
+  }
+  return true
+}
+// 上架
+const onConfirm = () => {
+  if (isCheck()) {
+    const quantity = searchCount.value
+    const data = []
+    const list = structuredClone(barcodeActiveList.value) // 深拷贝,保证原始数据不被修改
+    let remainingQuantity = quantity
+    for (let i = 0; i < list.length && remainingQuantity > 0; i++) {
+      const { taskNo, taskLineNo, warehouse, quantity } = list[i]
+      const takeQuantity = Math.min(quantity, remainingQuantity)
+      data.push({
+        taskNo,
+        taskLineNo,
+        warehouse,
+        container: '*',
+        targetCode: searchLocation.value,
+        quantity: takeQuantity, // 当前项扣除的数量
+      })
+      remainingQuantity -= takeQuantity
+    }
+    showLoading()
+    batchPutaway(data).then(res => {
+      if (totalQuantity.value - Number(searchCount.value) == 0) {
+        containerNo.value = ''
+        dataMap.value = {}
+        dataList.value = []
+        showNotify({ type: 'success', message: '当前任务已上架完成,请扫描下一个容器号', duration: 3000 })
+        inputBarcodeRef.value?.show('', '请扫描容器号', '')
+        stopTimer()
+      } else {
+        showNotify({ type: 'success', message: '上架成功,请继续扫描商品进行上架', duration: 3000 })
+        setBarcode(containerNo.value, 'success')
+        scanType.value = 2
+      }
+      reset()
+      scanSuccess()
+    }).catch(err => {
+      scanError()
+    }).finally(() => {
+      closeLoading()
+    })
+  }
+}
+const refresh=()=>{
+  reset()
+  scanType.value=2
+  loadData()
+}
+
+// 数据刷新
+const loadData = () => {
+  if (!containerNo.value) {
+    inputBarcodeRef.value?.show('', '请扫描容器号', '')
+    return
+  } else {
+    setBarcode(containerNo.value, 'container')
+    // currentTime.value=getCurrentTime()
+    // startTimer()
+  }
+}
+onUnmounted(() => {
+  closeListener()
+  stopTimer()
+})
+window.onRefresh = loadData
+
+</script>
+<style scoped lang="sass">
+.take-delivery
+  .take-info
+    padding: 6px 10px
+    background: linear-gradient(to left, #c8e2fb, #ffffff)
+    display: flex
+    flex-direction: column
+    text-align: left
+
+    .take-info-no
+      flex: 1
+
+      .info-no-title
+        font-size: 19px
+        font-width: 500
+        display: flex
+        justify-content: space-between
+        align-items: center
+
+      .info-no-tips
+        font-size: 14px
+        color: #666666
+        display: flex
+        justify-content: space-between
+        padding: 6px 0
+
+    .take-info-number
+      flex: 1
+      border-top: 1.5px solid #efefef
+      display: flex
+      justify-content: space-between
+      gap: 10px
+      color: #666
+      font-size: 14px
+      padding-top: 10px
+
+      .info-number-left
+        flex: 1
+        display: flex
+        justify-content: space-evenly
+        align-items: center
+
+        .number-left-box
+          flex: 1
+          display: flex
+          flex-direction: column
+          align-items: center
+
+          .left-box-title
+            font-size: 14px
+            font-weight: bold
+            color: #000
+            line-height: 34px
+
+      .info-number-right
+        width: 45%
+        text-align: center
+
+        .van-search
+          padding: 0
+
+  .take-barcode
+    margin-top: 10px
+    text-align: left
+    background: #FFFFFF
+
+    .barcode-input
+      ::v-deep(.van-search)
+        padding: 0
+
+      ::v-deep(.van-search__field)
+        border-bottom: 2px solid #ffffff
+
+      ::v-deep(.van-search__content)
+        background: #fff
+
+      ::v-deep(.van-field__control)
+        font-size: 15px
+        font-weight: bold
+
+      ::v-deep(.van-search__label)
+        font-size: 15px
+        font-weight: bold
+
+      .search-input-barcode
+        ::v-deep(.van-search__field)
+          border-bottom: 2px solid #0077ff
+          z-index: 2
+
+      .search-input-number
+        ::v-deep(.van-field__control)
+          font-size: 18px
+          font-weight: bold
+          color: #ee0a25
+
+  .take-lot
+    text-align: left
+    margin-top: 5px
+
+    ::v-deep(.van-cell)
+      padding: 5px 8px
+
+    .take-lot-title
+      font-size: 15px
+      font-weight: bold
+      padding: 0 5px
+      border-left: 3px solid #1989fa
+      color: #333
+      margin-bottom: 3px
+
+  .take-button
+    padding: 10px 20px
+    .btn
+      background: #1989fa
+      color: #fff
+      font-size: 16px
+      line-height: 35px
+      border-radius: 8px
+</style>

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

@@ -83,8 +83,7 @@
             @focus="scanType=4"
           >
             <template #action>
-              <div style="display: flex; align-items: center;flex-direction: column;margin-left: 20px"
-                   v-if="asnInfo.asnNo">
+              <div style="display: flex; align-items: center;flex-direction: column;margin-left: 20px" v-if="asnInfo.asnNo">
                 <div style="height: 20px;font-size: 12px">已收/预计</div>
                 <div style="font-size: 16px;font-weight: bold">{{ asnInfo.receivedQuantity}}/{{ asnInfo.expectedQuantity }}
                 </div>
@@ -288,6 +287,7 @@ const setBarcode = (code, type) => {
       taskInfo.value=res.data
       taskNo.value=code
     }
+    scanType.value=2
     uniqueCodeList.value=[]
     scanSuccess()
   }).catch(err=>{

+ 215 - 42
src/views/index.vue

@@ -1,58 +1,231 @@
 <template>
-  <div class="container">
-    <van-image class="image" :src="imageUrl" ></van-image>
-    <div class="name" >{{userInfo.username}}</div>
-    <div class="home" @click="onRouter('picking')">拣货</div>
-<!--    <div class="home" @click="onRouter('picking-aisle')">巷道拣货</div>-->
-    <div class="home" @click="onRouter('blind-receiving')">盲扫</div>
-<!--    <div class="home" @click="onRouter('check-move-stock')">反拣还库</div>-->
-    <div class="home" @click="onRouter('piece-dashboard')">计件面板</div>
-    <div class="home" @click="onRouter('take-delivery')">收货</div>
-    <div class="home" @click="onRouter('hik-putaway-allocation')">海康上架-调度</div>
-    <div class="home" @click="onRouter('hik-putaway')">海康上架</div>
-    <div class="home" @click="onRouter('hik-box-return')">海康-入库/解绑</div>
-    <div class="home" @click="onRouter('check-activity')">复核-活动单</div>
-    <div class="home" @click="onRouter('check-large')">复核-大件单</div>
-    <div class="home" @click="onRouter('move-list')">调拨-移库</div>
-    <div class="home" @click="onRouter('returned-register')">退货-登记</div>
+  <div class="warehouse container">
+    <div v-if="menuTrueFalseBy" class="menu">
+      <div class="name">
+        {{ userInfo.username }}
+      </div>
+      <div
+        v-for="(item, index) in menuList"
+        :key="index"
+        :class="menuId === item.id ? 'active' : ''"
+        @click="onConfirm(item)"
+        class="title flex"
+      >
+        <div class="icon">
+          <van-icon :name="item.icon"></van-icon>
+        </div>
+        <div>{{ item.title }}</div>
+      </div>
+      <div class="title flex" style="margin-top: 30px;" @click="outLogin">
+        <div class="icon">
+          <van-icon name="upgrade" />
+        </div>
+        <div>退出登录</div>
+      </div>
+    </div>
+    <div :class="menuTrueFalseBy ? 'content-box' : 'content-show'">
+      <div class="content">
+        <van-nav-bar
+          placeholder
+          :fixed="!menuTrueFalseBy"
+          @click-left="back"
+          @click-right="showSelect"
+        >
+          <template #left>
+            <div class="u-nav-slot">
+              <van-icon name="bars" size="25" />
+            </div>
+          </template>
+          <template #title>
+            <div class="flex flexColumn alignCenter">
+              <div>工作台</div>
+            </div>
+          </template>
+          <template #right>
+            <div style="color: #fff">{{ warehousesMap[warehouse] }}</div>
+          </template>
+        </van-nav-bar>
+        <div class="menu-box">
+          <div v-for="(item, index) in menuList[menuId].menu" class="menu-list">
+            <div class="menu-title">{{ item.title }}</div>
+            <div class="menu-item-box">
+              <div v-for="(items, index) in item.children" class="menu-item" @click="onRouter(items.path)">
+                <div class="menu-item-icon">
+                  <van-icon :name="items.icon" size="28" />
+                </div>
+                <div class="menu-item-title">{{ items.title }}</div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div v-if="menuTrueFalseBy" @click="hideMenu" class="hide-menu"></div>
+    </div>
+    <select-warehouse ref="selectWarehouseRef" @setWarehouse="setWarehouse" />
   </div>
 </template>
+
 <script setup>
-import imageUrl from '@/assets/img.png'
-import { useRouter } from 'vue-router'
-import { getHeader } from '@/utils/android.ts'
-import { useStore } from '@/store/modules/user.ts'
-import { getUserInfo } from '@/api/login/index.ts'
 import { ref } from 'vue'
+import menu from '@/hooks/basic/menu.js'
+import { useRouter } from 'vue-router'
+import { getHeader, goBack } from '@/utils/android'
+import { useStore } from '@/store/modules/user'
+import { getUserInfo } from '@/api/login/index'
+import SelectWarehouse from '@/components/SelectWarehouse.vue'
+import { showToast } from 'vant'
+
 const router = useRouter()
-const onRouter=(path)=>{
-  router.push(`/${path}`)
-}
 const store = useStore()
-const userInfo=ref({})
+const userInfo = ref({})
 try {
   getHeader()
-}catch (error) {
+} catch (error) {
 }
-setTimeout(()=>{
+setTimeout(() => {
   getUserInfo().then(res => {
-    userInfo.value=res.data
+    userInfo.value = res.data
   })
-},300)
+}, 300)
 
+const { menuList } = menu()
+const menuId = ref(0)
+const menuTrueFalseBy = ref(false)
+const storeUser = useStore()
+const warehouse = ref(storeUser.warehouse)
+const warehousesMap = ref({
+  'WH99': '测试仓库(WH99)',
+  'WH01': '九干仓(WH01)',
+  'WH02': '新浜一仓(WH02)',
+  'WH10': '新浜二仓(WH10)',
+})
+const onRouter = (path) => {
+  router.push(`/${path}`)
+}
+const onConfirm = (item) => {
+  menuTrueFalseBy.value = false
+}
+const back = () => {
+  try {
+    window.android.goBack()
+  }catch (e){
+    menuTrueFalseBy.value = true
+  }
+}
+const selectWarehouseRef = ref(null)
+const showSelect = () => {
+  try {
+    if(window.android.getHeader()){
+      showToast('app内不支持切换仓库')
+    }
+  }catch (e){
+    selectWarehouseRef.value.show(warehouse.value)
+  }
+
+}
+const hideMenu = () => {
+  if (menuTrueFalseBy.value) {
+    menuTrueFalseBy.value = false
+  }
+}
+const setWarehouse = (code) => {
+  warehouse.value = code
+}
+const outLogin = () => {
+  router.push(`/login`)
+}
 </script>
-<style scoped lang="sass" >
-.container
+
+<style lang="sass" scoped>
+.warehouse
+  width: 100%
   height: 100vh
-  .image
-    margin: 20px
-  .home
-    cursor: pointer
-    padding: 10px
-    font-size: 18px
-    text-decoration: underline
-    color: #0077ff
-  .name
-    font-size: 20px
-    color: #0077ff
+  display: flex
+
+  .menu
+    width: 50%
+    margin-left: 15px
+
+    .name
+      margin: 50px auto
+      text-align: center
+      font-size: 18px
+      font-weight: 500
+
+    .title
+      line-height: 40px
+
+    .active
+      color: #3c9cff
+
+  .content-box
+    width: 50%
+    transition: width 0.2s
+    overflow: hidden
+    background-color: #ffffff
+    margin: 80px 0 110px 0
+    position: relative
+
+    .content
+      width: 375px
+      overflow: hidden
+      transform: scale(0.75)
+      transform-origin: left top
+      pointer-events: none
+
+      .menu-box
+        .menu-list
+          background: #fff
+          padding: 10px 0
+          font-size: 13px
+          margin-bottom: 10px
+          color: #333
+
+          .menu-title
+            padding: 0 15px 10px 15px
+            text-align: left
+
+          .menu-item-box
+            display: flex
+
+            .menu-item
+              width: 25%
+
+    .hide-menu
+      position: absolute
+      height: 100%
+      width: 100%
+      left: 0
+      right: 0
+      top: 0
+      bottom: 0
+
+
+  .content-show
+    width: 100%
+
+    .content
+      .menu-box
+        .menu-list
+          background: #fff
+          font-size: 13px
+          margin-bottom: 10px
+          color: #333
+
+          .menu-title
+            padding: 10px 15px 10px 15px
+            text-align: left
+
+          .menu-item-box
+            display: flex
+            flex-wrap: wrap
+
+            .menu-item
+              width: 20%
+              margin-bottom: 10px
+
+  .grid-text
+    font-size: 14px
+    color: #909399
+    padding: 5px 0 10px 0
 </style>

+ 0 - 0
src/views/transfer/move/down/index.vue → src/views/inventory/moveTask/down/index.vue


+ 0 - 0
src/views/transfer/move/list/index.vue → src/views/inventory/moveTask/list/index.vue


+ 0 - 0
src/views/transfer/move/putaway/index.vue → src/views/inventory/moveTask/putaway/index.vue


+ 861 - 0
src/views/inventory/transfer/index.vue

@@ -0,0 +1,861 @@
+<template>
+  <div class="transfer-container">
+    <van-nav-bar title="移库" left-arrow fixed placeholder @click-left="goBack" @click-right="onConfirm">
+      <template #left>
+        <van-icon name="arrow-left" size="25" />
+        <div style="color: #fff">返回</div>
+      </template>
+      <template #right>
+        <span style="color: #fff">确认移库</span>
+      </template>
+    </van-nav-bar>
+    <!-- 搜索表单 -->
+    <div class="top" ref="topRef">
+      <van-notice-bar :text="tips" speed="50" class="tip-notice" />
+      <div class="search-section">
+        <van-cell-group>
+          <van-field
+            v-model.lazy="sourceLocation"
+            label="来源库位"
+            placeholder="请输入来源库位"
+            clearable
+            :class="{ 'field-focused': scanType === 1 }"
+            @focus="scanType = 1"
+            @keyup.enter="_handlerScan(sourceLocation)"
+            ref="sourceLocationRef"
+            autocomplete="off"
+          />
+          <van-field
+            v-model.lazy="productBarcode"
+            label="商品条码"
+            placeholder="请输入商品条码"
+            clearable
+            :class="{ 'field-focused': scanType === 2 }"
+            @focus="scanType = 2"
+            @keyup.enter="_handlerScan(productBarcode)"
+            @input="onAsnCancel"
+            ref="productBarcodeRef"
+            autocomplete="off"
+          />
+          <van-field
+            v-model="transferQuantity"
+            label="移库数量"
+            placeholder="请输入移库数量"
+            ref="countRef"
+            type="number"
+            clearable
+            :class="{ 'field-focused': scanType === 3,'active-color': scanType != 3}"
+            @focus="scanType = 3"
+            @keyup.enter="_handlerScan(transferQuantity)"
+            autocomplete="off"
+          >
+            <template #right-icon v-if="selectedProducts['id']">
+              <div class="quantity-display">
+                <span class="quantity-label">库存</span>
+                <span class="quantity-value">{{ selectedProducts.quantityAvailable }}</span>
+                <span class="quantity-label">件</span>
+              </div>
+            </template>
+          </van-field>
+          <van-field
+            v-model="targetLocation"
+            label="目标库位"
+            placeholder="请输入目标库位"
+            clearable
+            :class="{ 'field-focused': scanType === 4 }"
+            @focus="scanType = 4"
+            autocomplete="off"
+            ref="targetLocationRef"
+            @keyup.enter="_handlerScan(targetLocation)"
+          />
+        </van-cell-group>
+      </div>
+      <!-- 已选择商品展示 -->
+      <div v-if="selectedProducts['id']" class="selected-section">
+        <van-cell-group>
+          <van-cell class="selected-product-cell">
+            <template #title>
+              <div class="selected-product-header">
+                <div class="location-info">
+                  <span class="location-label">跟踪号</span>
+                  <span class="location-value">{{ selectedProducts.traceId }}</span>
+                </div>
+              </div>
+            </template>
+            <template #label>
+              <div class="selected-product-details">
+                <div class="detail-auto-grid">
+                  <span class="detail-text" v-if="selectedProducts.lotAtt01">生产日期:{{ selectedProducts.lotAtt01 }}</span>
+                  <span class="detail-text" v-if="selectedProducts.lotAtt02">失效日期:{{ selectedProducts.lotAtt02 }}</span>
+                  <span class="detail-text" v-if="selectedProducts.lotAtt05">属性仓:{{ selectedProducts.lotAtt05 }}</span>
+                  <span class="detail-text" v-if="selectedProducts.lotAtt08">质量状态:{{ selectedProducts.lotAtt08 }}</span>
+                  <span class="detail-text" v-if="selectedProducts.lotNumber">批次号:{{ selectedProducts.lotNumber }}</span>
+                  <span class="detail-text" v-if="selectedProducts.lotAtt04">生产批号:{{ selectedProducts.lotAtt04 }}</span>
+                </div>
+              </div>
+            </template>
+          </van-cell>
+        </van-cell-group>
+      </div>
+    </div>
+    <!-- 查询结果表格 -->
+    <div class="table-section">
+      <div class="table-container">
+        <div class="table-title" v-if="productList.length==0">库位商品明细</div>
+        <div class="table-wrapper" :style="'height:'+tableHeight+'px'">
+          <div class="table-header-fixed">
+            <table class="product-table header-table">
+              <thead>
+              <tr>
+                <th style="width: 40px">货主</th>
+                <th>库位</th>
+                <th>条码</th>
+                <th>余量</th>
+                <th>分配</th>
+                <th>可用</th>
+                <th style="width: 25px">操作</th>
+              </tr>
+              </thead>
+            </table>
+          </div>
+          <div class="table-body-wrapper">
+            <table class="product-table body-table">
+              <tbody>
+              <tr v-if="productList.length>0"
+                  v-for="(product, index) in productList"
+                  :key="product.id"
+                  :class="{ selected: isSelected(product) }"
+                  :style="getRowStyle(product)"
+                  @click="onProductSelect(product)"
+                  class="clickable-row"
+              >
+                <td
+                  v-if="shouldShowOwner(product, index)"
+                  :rowspan="getOwnerRowspan(product, index)"
+                  class="owner-cell"
+                  style="width: 40px"
+                >
+                  {{ shouldShowOwnerText(product, index) ? product.owner : '' }}
+                </td>
+                <td class="location-cell">{{ product.location }}</td>
+                <td class="barcode-cell">{{ product.barcode }}</td>
+                <td class="quantity-cell">{{ product.quantity }}</td>
+                <td class="quantity-cell">{{ product.qtyPreAllocated }}</td>
+                <td class="quantity-cell">{{ product.quantityAvailable }}</td>
+                <td class="checkbox-cell">
+                  <van-checkbox
+                    :model-value="isSelected(product)"
+                    :disabled="product.quantityAvailable <= 0"
+                    @update:model-value="onProductSelect(product)"
+                  />
+                </td>
+              </tr>
+              <tr v-else>
+                <td colspan="7">
+                  <van-empty :image="<string>nodataUrl" image-size="140"></van-empty>
+                </td>
+              </tr>
+              </tbody>
+            </table>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted, nextTick } from 'vue'
+import { showConfirmDialog, showNotify } from 'vant'
+import nodataUrl from '@/assets/nodata.png'
+import { useRouter } 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 { getInventory, inventoryMovement } from '@/api/inventory'
+import { closeLoading, showLoading } from '@/utils/loading'
+
+const router = useRouter()
+const store = useStore()
+try {
+  getHeader()
+  androidFocus()
+} catch (error) {
+  router.push('/login')
+}
+const warehouse = store.warehouse
+// 页面初始化
+onMounted(() => {
+  openListener()
+  scanInit(_handlerScan)
+  // 初始化表格高度
+  setTimeout(() => {
+    calculateTableHeight()
+  }, 100)
+
+  // 监听视窗变化(键盘弹起/收回)
+  if (window.visualViewport) {
+    window.visualViewport.addEventListener('resize', calculateTableHeight)
+  }
+  // 兼容方案:监听窗口大小变化
+  window.addEventListener('resize', calculateTableHeight)
+})
+const sourceLocation = ref('')
+const productBarcode = ref('')
+const transferQuantity = ref('')
+const targetLocation = ref('')
+const productList = ref([])
+const selectedProducts = ref({})
+const oldProductBarcode = ref('')
+const countRef = ref(null)
+const targetLocationRef = ref(null)
+const productBarcodeRef = ref(null)
+const sourceLocationRef = ref(null)
+// 扫描类型控制焦点高亮
+const scanType = ref(2) // 1: 来源库位, 2: 商品条码, 3: 移库数量, 4: 目标库位
+const tips = ref('请扫描商品条码/来源库位')
+
+// 扫描条码监听
+const _handlerScan = (code) => {
+  if (!code) return
+  if (scanType.value == 2 || scanType.value == 1) {
+    if (scanType.value == 2) {
+      productBarcode.value = code
+      oldProductBarcode.value = code
+    } else if (scanType.value == 1) {
+      sourceLocation.value = code
+    }
+    const params = {
+      warehouse,
+      barcode: productBarcode.value,
+      location: sourceLocation.value,
+      locationRegexp: '^(?!STAGE_|SORTATION_).*$',
+    }
+    showLoading()
+    getInventory(params).then(res => {
+      if (res.data.length > 0) {
+        res.data.forEach((item, i) => {
+          item.id = i + 1
+        })
+        // 按照locationUsage排序:PC、CS、EA、RS、ST、SS,其他的放最后
+        const sortOrder = {
+          'PC': 1,
+          'CS': 2,
+          'EA': 3,
+          'RS': 4,
+          'ST': 5,
+          'SS': 6,
+        }
+        res.data.sort((a, b) => {
+          const orderA = sortOrder[a.locationUsage] || 999
+          const orderB = sortOrder[b.locationUsage] || 999
+          // 先按locationUsage排序
+          if (orderA !== orderB) {
+            return orderA - orderB
+          }
+          // locationUsage相同时,再按location排序
+          return a.location.localeCompare(b.location)
+        })
+        productList.value = res.data
+        // 查找第一个可用数量大于0的商品进行选中
+        const firstAvailableProduct = productList.value.find(item => item.quantityAvailable > 0)
+        if (firstAvailableProduct) {
+          onProductSelect(firstAvailableProduct)
+        } else {
+          // 如果所有商品可用数量都为0,则不选择任何商品
+          selectedProducts.value = {}
+        }
+        // 数据加载完成后重新计算表格高度
+        setTimeout(() => {
+          calculateTableHeight()
+        }, 100)
+        closeLoading()
+        scanSuccess()
+        tips.value = '请扫描目标库位/修改移库数量'
+        scanType.value = 4
+      } else {
+        scanError()
+        closeLoading()
+        showNotify({ type: 'warning', duration: 3000, message: `暂未查询到《${code}》库存数据` })
+        tips.value = `暂未查询到《${code}》库存数据`
+        productBarcode.value = ''
+        reset()
+      }
+    }).catch((err) => {
+      scanError()
+      closeLoading()
+      tips.value = err.message || '系统异常,请联系技术支持'
+      reset()
+    })
+  } else if (scanType.value == 3) {
+    countRef.value?.blur()
+    scanSuccess()
+    if (targetLocation.value) {
+      tips.value = '请手动进行移库操作'
+    } else {
+      tips.value = '请扫描目标库位'
+      scanType.value = 4
+    }
+  } else if (scanType.value == 4) {
+    scanSuccess()
+    targetLocation.value = code
+    onConfirm()
+  }
+  targetLocationRef.value?.blur()
+  productBarcodeRef.value?.blur()
+  sourceLocationRef.value?.blur()
+}
+const onConfirm = () => {
+  if (!selectedProducts.value.id) {
+    tips.value = '请先查询库存信息'
+    scanType.value = 2
+    showNotify({ type: 'warning', duration: 3000, message: `请先查询库存信息` })
+    return
+  }
+  if (!productBarcode.value) {
+    scanError()
+    tips.value = '请先扫描商品条码'
+    scanType.value = 2
+    showNotify({ type: 'warning', duration: 3000, message: `请先扫描商品条码` })
+    return
+  }
+  if (!transferQuantity.value) {
+    scanError()
+    tips.value = '请先输入移库数量'
+    countRef.value?.focus()
+    showNotify({ type: 'warning', duration: 3000, message: `请先输入移库数量` })
+    return
+  }
+  if (transferQuantity.value <= 0 || transferQuantity.value > selectedProducts.value.quantityAvailable) {
+    scanError()
+    countRef.value?.focus()
+    tips.value = '请先输入正确的移库数量'
+    showNotify({ type: 'warning', duration: 3000, message: `请先输入正确的移库数量` })
+    return
+  }
+  if (!targetLocation.value) {
+    scanError()
+    tips.value = '请先扫描目标库位'
+    scanType.value = 4
+    showNotify({ type: 'warning', duration: 3000, message: `请先扫描目标库位` })
+    return
+  }
+  showConfirmDialog({
+    title: '移库确认',
+    message:
+      `${productBarcode.value}从"${sourceLocation.value}"移动至"${targetLocation.value}"共:${transferQuantity.value}件`,
+  })
+    .then(() => {
+      const { traceId, lotNumber, ownerCode, sku } = selectedProducts.value
+      const data = {
+        fmLocation: sourceLocation.value,
+        fmContainer: traceId,
+        owner: ownerCode,
+        sku,
+        lotNum: lotNumber,
+        warehouse,
+        quantity: transferQuantity.value,
+        toLocation: targetLocation.value,
+      }
+      showLoading()
+      inventoryMovement(data).then(() => {
+        closeLoading()
+        scanSuccess()
+        showNotify({ type: 'success', duration: 3000, message: `操作成功,请继续扫描商品条码` })
+        tips.value = '操作成功,请继续扫描商品条码'
+        scanType.value = 2
+        productBarcode.value = ''
+        reset()
+      }).catch(err => {
+        scanError()
+        closeLoading()
+        tips.value = err.message || '系统异常,请联系技术支持'
+      })
+    })
+    .catch(() => {
+    })
+}
+const onAsnCancel = () => {
+  if (productBarcode.value === '' || (oldProductBarcode.value.length != productBarcode.value.length && oldProductBarcode.value != '')) {
+    productList.value = []
+    transferQuantity.value = ''
+    selectedProducts.value = {}
+    scanType.value = 2
+  }
+}
+const reset = () => {
+  productList.value = []
+  sourceLocation.value = ''
+  targetLocation.value = ''
+  transferQuantity.value = ''
+  oldProductBarcode.value = ''
+  selectedProducts.value = {}
+}
+const topRef = ref(null)
+const tableHeight = ref(261)
+
+// 计算表格高度
+const calculateTableHeight = () => {
+  if (topRef.value) {
+    // 使用 nextTick 确保DOM更新完成后再计算
+    nextTick(() => {
+      // 获取键盘高度
+      const keyboardHeight = window.innerHeight - window.visualViewport?.height || 0
+      // 视图高度 - topRef高度 - 导航栏高度(约46px) - 键盘高度
+      const newHeight = window.innerHeight - topRef.value.offsetHeight - 46 - keyboardHeight
+      tableHeight.value = Math.max(newHeight, 200) // 最小高度200px
+    })
+  }
+}
+
+const onProductSelect = (product) => {
+  // 如果可用数量为0,则不允许选择
+  if (product.quantityAvailable <= 0) {
+    return
+  }
+
+  selectedProducts.value = product
+  // 选择商品时自动赋值移库数量
+  transferQuantity.value = product.quantityAvailable
+  sourceLocation.value = product.location
+  scanType.value = 4
+  // 重新计算表格高度
+  calculateTableHeight()
+}
+
+const isSelected = (product) => {
+  return selectedProducts.value?.id === product.id
+}
+
+// 根据 locationUsage 返回行样式
+const getRowStyle = (product) => {
+  const locationUsageColors = {
+    'PC': 'white',
+    'CS': 'white',
+    'EA': 'white',
+    'RS': 'rgb(255, 250, 250)', // Snow
+    'ST': 'lightgray',
+  }
+
+  const backgroundColor = locationUsageColors[product.locationUsage] || 'white'
+
+  return {
+    backgroundColor,
+  }
+}
+
+// 判断是否应该显示货主单元格(始终显示单元格)
+const shouldShowOwner = (product, index: number) => {
+  // 如果当前行被选中,则显示单元格
+  if (isSelected(product)) {
+    return true
+  }
+
+  // 检查同一货主组中是否有选中的行
+  const sameOwnerProducts = productList.value.filter(p => p.owner === product.owner)
+  const hasSelectedInGroup = sameOwnerProducts.some(p => isSelected(p))
+
+  // 如果同一货主组中有选中的行,则其他未选中行显示空单元格
+  if (hasSelectedInGroup) {
+    // 检查当前行是否为未选中的连续组的第一行
+    if (index === 0 || productList.value[index - 1].owner !== product.owner || isSelected(productList.value[index - 1])) {
+      return true
+    }
+    return false
+  }
+
+  // 如果同一货主组中没有选中的行,则在第一行显示单元格
+  return isFirstInOwnerGroup(product, index)
+}
+
+// 判断是否应该显示货主文字
+const shouldShowOwnerText = (product, index: number) => {
+  // 如果当前行被选中,则显示文字
+  if (isSelected(product)) {
+    return true
+  }
+
+  // 检查同一货主组中是否有选中的行
+  const sameOwnerProducts = productList.value.filter(p => p.owner === product.owner)
+  const hasSelectedInGroup = sameOwnerProducts.some(p => isSelected(p))
+
+  // 如果同一货主组中有选中的行,则其他未选中行不显示文字
+  if (hasSelectedInGroup) {
+    return false
+  }
+
+  // 如果同一货主组中没有选中的行,则在第一行显示文字
+  return isFirstInOwnerGroup(product, index)
+}
+
+// 检查是否为同一货主的第一行
+const isFirstInOwnerGroup = (product, index: number) => {
+  if (index === 0) return true
+  return productList.value[index - 1].owner !== product.owner
+}
+
+// 计算相同货主的行跨度
+const getOwnerRowspan = (product, index: number) => {
+  // 如果当前行被选中,rowspan为1
+  if (isSelected(product)) {
+    return 1
+  }
+
+  // 检查同一货主组中是否有选中的行
+  const sameOwnerProducts = productList.value.filter(p => p.owner === product.owner)
+  const hasSelectedInGroup = sameOwnerProducts.some(p => isSelected(p))
+
+  // 如果同一货主组中有选中的行,则计算未选中行的rowspan
+  if (hasSelectedInGroup) {
+    let rowspan = 1
+    // 向下查找连续的未选中的同货主行
+    for (let i = index + 1; i < productList.value.length; i++) {
+      if (productList.value[i].owner === product.owner && !isSelected(productList.value[i])) {
+        rowspan++
+      } else {
+        break
+      }
+    }
+    return rowspan
+  }
+
+  // 计算正常的rowspan(没有选中行的情况)
+  let rowspan = 1
+  for (let i = index + 1; i < productList.value.length; i++) {
+    if (productList.value[i].owner === product.owner) {
+      rowspan++
+    } else {
+      break
+    }
+  }
+  return rowspan
+}
+
+// 数据刷新
+const loadData = () => {
+}
+onUnmounted(() => {
+  closeListener()
+  // 移除键盘事件监听器
+  if (window.visualViewport) {
+    window.visualViewport.removeEventListener('resize', calculateTableHeight)
+  }
+  window.removeEventListener('resize', calculateTableHeight)
+})
+// window.onRefresh = loadData
+</script>
+
+<style scoped>
+.transfer-container {
+  display: flex;
+  flex-direction: column;
+}
+
+/* 搜索表单美化样式 */
+.search-section {
+  margin: 0;
+  background: white;
+  border-radius: 0;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  overflow: hidden;
+}
+
+.search-section ::v-deep(.van-cell-group) {
+  background: transparent;
+}
+
+.search-section ::v-deep(.van-field) {
+  position: relative;
+  background: white;
+  transition: all 0.3s ease;
+  border-radius: 0;
+  padding: 0 15px;
+}
+
+.search-section ::v-deep(.van-field:last-child) {
+  border-bottom: none;
+}
+
+.search-section ::v-deep(.van-field__label) {
+  font-weight: 600;
+  color: #323233;
+  font-size: 14px;
+  line-height: 38px;
+  width: 65px;
+}
+
+/* 基于scanType的动态高亮效果 */
+.search-section ::v-deep(.van-field__body) {
+  border-bottom: 2px solid #e7eaf3;
+  height: 36px;
+}
+
+.search-section ::v-deep(.van-field__control) {
+  font-size: 15px;
+  color: #323233;
+  font-weight: 500;
+}
+
+.search-section ::v-deep(.van-field__control::placeholder) {
+  color: #c8c9cc;
+  font-weight: 400;
+}
+
+/* 基于scanType的动态高亮效果 */
+.field-focused ::v-deep(.van-field__body) {
+  border-bottom: 2px solid #1989fa;
+  background: linear-gradient(to right, rgba(25, 137, 250, 0.02), rgba(25, 137, 250, 0.05));
+  position: relative;
+  height: 36px;
+}
+
+.field-focused ::v-deep(.van-field::after) {
+  content: '';
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 2px;
+  background: #1989fa;
+  z-index: 1;
+}
+
+.field-focused ::v-deep(.van-field__label) {
+  color: #1989fa !important;
+  font-weight: 700;
+}
+
+.field-focused ::v-deep(.van-field__control) {
+  color: #1989fa !important;
+  font-weight: 600;
+}
+
+.active-color ::v-deep(.van-field__control) {
+  color: orangered !important;
+  font-weight: 600;
+}
+
+/* 移除原有的焦点样式 */
+.search-section ::v-deep(.van-field--focused .van-cell) {
+  border-color: transparent !important;
+}
+
+.selected-section {
+  margin: 5px 0;
+  padding: 0 8px;
+}
+
+/* 库存数量展示样式 */
+.quantity-display {
+  display: flex;
+  align-items: center;
+  gap: 2px;
+  margin-right: 10px;
+  padding: 4px 8px;
+  background: rgba(25, 137, 250, 0.1);
+  border-radius: 6px;
+  border: 1px solid rgba(25, 137, 250, 0.2);
+}
+
+.quantity-label {
+  font-size: 10px;
+  color: #666;
+  font-weight: 500;
+}
+
+.quantity-value {
+  font-size: 14px;
+  color: orangered;
+  align-items: center;
+  font-weight: 700;
+}
+
+/* 已选商品展示 - 稍微醒目的灰蓝色主题 */
+.selected-product-cell {
+  background: #f0f5ff;
+  color: #4a5568;
+  border-radius: 0;
+  overflow: hidden;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
+  border: 1px solid #d4edff;
+}
+
+::v-deep(.selected-product-cell.van-cell) {
+  padding: 8px;
+  border-bottom: none;
+}
+
+.selected-product-header {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 100%;
+  margin-bottom: 8px;
+}
+
+.location-info {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  line-height: 14px;
+}
+
+.location-label {
+  font-size: 14px;
+  color: #4a5568;
+  font-weight: 400;
+}
+
+.location-value {
+  font-size: 14px;
+  font-weight: 600;
+  color: #2d3748;
+  margin-left: 8px;
+}
+
+.selected-product-details {
+  background: rgba(116, 143, 252, 0.08);
+  padding: 5px 0;
+  border-radius: 0;
+  border: 1px solid rgba(116, 143, 252, 0.15);
+}
+
+.detail-auto-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 8px 12px;
+  align-items: center;
+}
+
+.detail-text {
+  font-size: 12px;
+  color: #4a5568;
+  font-weight: 500;
+  flex: 1;
+  text-align: center;
+  line-height: 12px;
+}
+
+.table-section {
+  flex: 1;
+}
+
+.table-container {
+  background: white;
+  border-radius: 6px;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
+  position: relative;
+}
+
+.table-wrapper {
+  position: relative;
+  overflow: hidden;
+}
+
+.table-header-fixed {
+  position: sticky;
+  top: 0;
+  z-index: 10;
+  background: white;
+  border-bottom: 2px solid #ebedf0;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.table-body-wrapper {
+  overflow-y: auto;
+  overflow-x: hidden;
+  max-height: calc(100% - 40px); /* 减去表头高度 */
+}
+
+.header-table,
+.body-table {
+  table-layout: fixed;
+}
+
+.header-table th:nth-child(1),
+.body-table td:nth-child(1) {
+  width: 40px;
+}
+
+.header-table th:nth-child(2),
+.body-table td:nth-child(2) {
+  width: 25%;
+}
+
+.header-table th:nth-child(3),
+.body-table td:nth-child(3) {
+  width: 20%;
+}
+
+.header-table th:nth-child(4),
+.body-table td:nth-child(4) {
+  width: 12%;
+}
+
+.header-table th:nth-child(5),
+.body-table td:nth-child(5) {
+  width: 12%;
+}
+
+.header-table th:nth-child(6),
+.body-table td:nth-child(6) {
+  width: 12%;
+}
+
+.table-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #323233;
+  padding: 4px 16px 4px;
+  border-bottom: 1px solid #ebedf0;
+  background: #f7f8fa;
+}
+
+.product-table {
+  width: 100%;
+  border-collapse: collapse;
+}
+
+.product-table th,
+.product-table td {
+  padding: 8px 0;
+  text-align: center;
+  border-bottom: 1px solid #ebedf0;
+  font-size: 12px;
+  word-break: break-all;
+  color: #646566;
+}
+
+.product-table th {
+  background-color: #f7f8fa;
+  font-weight: 600;
+  color: #323233;
+  font-size: 13px;
+}
+
+.product-table tr.selected {
+  background-color: #e8f4ff;
+}
+
+.product-table tr.selected td {
+  color: #1989fa;
+}
+
+.clickable-row {
+  cursor: pointer;
+}
+
+.checkbox-cell {
+  width: 25px;
+}
+
+::v-deep(.checkbox-cell .van-checkbox) {
+  justify-content: center;
+}
+
+::v-deep(.checkbox-cell .van-checkbox__icon) {
+  background: #fff;
+  border-radius: 50%;
+}
+
+</style>

+ 19 - 6
src/views/login/login.vue

@@ -1,7 +1,10 @@
 
 <template>
   <div class="container">
-    <van-form  style="margin-top: 16px;" >
+    <div class="container-bg">
+
+    </div>
+    <van-form  style="margin-top: -26px;" >
       <van-cell-group inset>
         <van-field
           v-model="warehouse"
@@ -9,6 +12,7 @@
           name="仓库"
           label="仓库"
           placeholder="仓库"
+          autocomplete="off"
           :rules="[{ required: true, message: '请填写仓库' }]"
         />
         <van-field
@@ -17,9 +21,10 @@
           name="用户名"
           label="用户名"
           placeholder="用户名"
+          autocomplete="off"
+          label-align="top"
           :rules="[{ required: true, message: '请填写用户名' }]"
         />
-
         <van-field
           v-model="password"
           type="password"
@@ -27,14 +32,13 @@
           name="密码"
           label="密码"
           placeholder="密码"
+          autocomplete="off"
+          label-align="top"
           :rules="[{ required: true, message: '请填写密码' }]"
         />
-
       </van-cell-group>
       <div style="margin: 16px;">
-        <van-button round block type="primary" @click="onConfirm" native-type="submit">
-          提交
-        </van-button>
+        <van-button round block type="primary" @click="onConfirm" native-type="submit">登录</van-button>
       </div>
     </van-form>
   </div>
@@ -65,3 +69,12 @@ const onConfirm = async () => {
 }
 
 </script>
+<style scoped lang="sass">
+.container
+  .container-bg
+    height: 190px
+    width: 100%
+    background: url("../../assets/login_bg.jpg") no-repeat
+    background-size: 100%
+
+</style>

+ 166 - 57
src/views/outbound/check/activity/index.vue

@@ -6,9 +6,6 @@
         <van-icon name="arrow-left" size="25" />
         <div style="color: #fff">返回</div>
       </template>
-      <!--      <template #right>-->
-      <!--        <div class="nav-right" @click="onClickRight">提交任务</div>-->
-      <!--      </template>-->
     </van-nav-bar>
     <div class="activity">
       <div class="wave-title">
@@ -27,7 +24,6 @@
             <div>KG</div>
           </template>
         </van-field>
-<!--        :class="[totalWeight>0?'success-input-barcode':'error-input-barcode']"-->
       </div>
       <div class="order-detail">
         <div class="picking-no">
@@ -46,23 +42,26 @@
             <div  style="text-decoration: underline;color: #0077ff">设置打印机<van-icon name="edit" color="#0077ff"/> </div>
           </div>
           <div class="picking-order-count ">
-            <div>已复核数/总复核数:
+            <div>
+              <span style="font-size: 12px">已复核数/总复核数:</span>
               <span style="color: #333;font-weight: bold;font-size: 18px" v-if="orderMap.dataGroup">{{ successNumber }}/{{Object.keys(orderMap.dataGroup).length ||0}}</span>
               <span v-else>0</span>
             </div>
-            <div>取消单:
+            <div>
+              <span style="font-size: 12px">取消单:</span>
               <span style="color: #333;font-weight: bold;font-size: 18px" v-if="orderMap.cancelGroup">{{Object.keys(orderMap.cancelGroup).length ||0}}</span>
               <span v-else>0</span>
             </div>
-            <div>冻结单:
+            <div>
+              <span style="font-size: 12px">冻结单:</span>
               <span style="color: #333;font-weight: bold;font-size: 18px" v-if="orderMap.freezeGroup">{{Object.keys(orderMap.freezeGroup).length ||0}}</span>
               <span  v-else>0</span>
             </div>
           </div>
         </div>
         <div class="picking-button">
-          <div class="picking-button-item" @click="print('A3012_RF_WIFI_HDQD')">波次清单</div>
-          <div class="picking-button-item" @click="print('PRINT_WAYBILL')">打印面单</div>
+          <div class="picking-button-item" @click="print('A3012_RF_WIFI_HDQD',orderDetail.waveNo)">波次清单</div>
+          <div class="picking-button-item" @click="print('PRINT_WAYBILL',orderDetail.waveNo)">打印面单</div>
           <div class="picking-button-item" @click="endCheck()" >结束复核</div>
           <van-popover :actions="actions" placement="bottom-end" @select="onSelect" theme="dark">
             <template #reference>
@@ -80,21 +79,24 @@
              <th style="width: 40%">商品条码</th>
              <th>数量</th>
              <th>剩余</th>
-             <th v-if="isUniqueCode">标记</th>
+             <th>状态</th>
+             <th v-if="isUniqueCode || isQualityCheck">标记</th>
            </tr>
            </thead>
            <tbody>
-           <tr v-for="(item, index) in orderList" :key="index" v-if="orderList.length>0">
+           <tr v-for="(item, index) in orderList" :key="index" v-if="orderList.length>0"  :style="rowStyle(item)"  >
              <td>{{ item.barcode }} <van-tag type="success" v-if="item.universalCode">万用</van-tag></td>
              <td>{{item.qty}}/{{item.qtyOrdered}}</td>
              <td>{{ item.qty }}</td>
-             <td v-if="isUniqueCode">
+             <td>{{ statusMap[item.status] || '' }}</td>
+             <td v-if="isUniqueCode || isQualityCheck">
                <van-tag v-if="item.uniqueRegExp" type="warning" >唯一码</van-tag>
                <van-tag v-if="item.imeiRegExp" type="primary" >IMEI码</van-tag>
+               <van-tag v-if="item.qualityCheck" type="warning" >质检</van-tag>
              </td>
            </tr>
            <tr v-else>
-             <td colspan="3">
+             <td colspan="4">
                <div>暂无数据</div>
              </td>
            </tr>
@@ -105,31 +107,34 @@
     </div>
     <!-- 条码输入组件 -->
     <input-barcode :back="back" @setBarcode="setBarcode" ref="inputBarcodeRef" />
-    <!--    打印面单-->
+    <!-- 打印面单-->
     <printer ref="printerRef" @onPrint="onPrint"  />
-    <!--    订单列表-->
+    <!-- 订单列表-->
     <order-list-table ref="orderListRef" />
-    <!--    返拣-->
+    <!-- 返拣-->
     <reverse-picking ref="reversePickingRef" :warehouse="warehouse" :reversePickingContainerNo="reversePickingContainerNo" @load-data="loadData" @reversePickingReset="reversePickingReset" />
-    <!--    耗材-->
+    <!-- 耗材-->
     <related-materia ref="relatedMateriaRef" @cut-barcode="cutBarcode" />
+    <!-- 复核组合商品-->
+    <check-barcode-combine ref="checkBarcodeCombineRef"  @cutBarcode="cutBarcode" />
   </div>
 </template>
 <script setup>
-import { onMounted, onUnmounted, ref } from 'vue'
+import { computed, onMounted, onUnmounted, ref } from 'vue'
 import { androidFocus, getHeader, goBack, scanError, scanSuccess } from '@/utils/android'
 import { useStore } from '@/store/modules/user'
 import { closeListener, openListener, scanInit } from '@/utils/keydownListener'
 import { getPendingReviewTask, packingReview } from '@/api/check/index'
-import { barcodeToUpperCase } from '@/utils/dataType.js'
-import { closeToast, showConfirmDialog, showLoadingToast, showNotify } from 'vant'
+import { barcodeToUpperCase, toMap } from '@/utils/dataType.js'
+import { closeToast, showConfirmDialog, showDialog, showLoadingToast, showNotify, showToast } from 'vant'
 import { closeLoading, showLoading } from '@/utils/loading'
-import { fluxPrint } from '@/api/picking/index'
+import { fluxPrint, getListCombineSku } from '@/api/picking/index'
 import InputBarcode from '@/views/outbound/picking/components/InputBarcode.vue'
 import Printer from '@/components/Printer.vue'
 import orderListTable from '@/views/outbound/check/components/OrderListTable.vue'
 import ReversePicking from '@/views/outbound/check/components/ReversePicking.vue'
 import RelatedMateria from '@/views/outbound/check/components/RelatedMateria.vue'
+import CheckBarcodeCombine from '@/views/outbound/check/components/CheckBarcodeCombine.vue'
 const store = useStore()
 try {
   getHeader()
@@ -144,8 +149,10 @@ onMounted(() => {
 })
 const warehouse = store.warehouse
 //收货容器号
+// JH-WH99-990
 const waveNo = ref('')
 const  isUniqueCode=ref(false)
+const isQualityCheck=ref(false)
 // 错误提示
 const tips = ref('')
 //强制返回
@@ -164,40 +171,67 @@ const reversePickingContainerNo=containerNoMap[warehouse]
 const orderDetail=ref({})
 //订单详情
 const orderMap=ref({})
-const orderList=ref([])
+const dataList=ref([])
+const orderList = computed(() => {
+  return dataList.value
+    .sort((a, b) => {
+      const statusPriority = {
+        '60': 0,
+        '600': 1,
+        '50': 2,
+        '40': 3,
+        '30': 4,
+        '20': 5,
+        '00': 6,
+      }
+      if (statusPriority[a.status] !== statusPriority[b.status]) {
+        return statusPriority[a.status] - statusPriority[b.status]
+      }
+      return 0
+    })
+})
+const statusMap = {
+  '00': '创建',
+  '20': '预配',
+  '30': '部分分配',
+  '40': '已分配',
+  '50': '待拣货',
+  '60': '已拣货',
+  '61': '分拣完成',
+  '90': '订单取消',
+}
 //匹配条码
 const matchBarcodeList=ref([])
 const actions = [
   { text: '重新开始',value:'reset' },
-  // { text: '波次清单',value:'goods' },
   { text: '装箱清单',value:'packing' },
   { text: '取消单列表',value:'cancel' },
   { text: '冻结单列表',value: 'freeze' },
+  // { text: '结束复核',value: 'end' },
 ];
 // 扫描条码监听
 const relatedMateriaRef=ref(null)
+const matchedSku=ref([])
+const checkBarcodeCombineRef=ref(null)
 const _handlerScan = (code) => {
   if (code) {
+    if (isUniqueCode.value) {
+      scanError()
+      scanBarcode.value = ''
+      showNotify({ type: 'warning', duration: 3000, message: `此单包含唯一码/IMEI码,请到PC复核` })
+      return
+    }
     const barcode = [...new Set(
       orderList.value
+        .filter(item => (item.status == '60'))
         .flatMap(item => [item.barcode, item.barcode2, item.sku, item.universalCode])
         .filter(value => value !== null && value !== '' && value !== undefined),
     )];
     const checkBarcode = barcodeToUpperCase(code);
     if (barcode.some(item => barcodeToUpperCase(item) === checkBarcode)) {
-      matchBarcodeList.value=orderList.value.filter(item=>((item.barcode===checkBarcode || item.sku===checkBarcode || item.barcode2===checkBarcode || item.universalCode==checkBarcode) && item.qty>0) )
+      matchBarcodeList.value=orderList.value.filter(item=>((item.barcode===checkBarcode || item.sku===checkBarcode || item.barcode2===checkBarcode || item.universalCode==checkBarcode) && item.status == '60'  && item.qty>0) )
       if(matchBarcodeList.value.length>0){
         const itemActive = matchBarcodeList.value[0]
-        if(itemActive.uniqueRegExp){
-          scanBarcode.value=''
-          showNotify({ type: 'warning', duration: 3000, message: `此活动单包含唯一码,请到PC复核`})
-          return
-        }
-        if(itemActive.imeiRegExp){
-          scanBarcode.value=''
-          showNotify({ type: 'warning', duration: 3000, message: `此活动单包含IMEI码,请到PC复核`})
-          return
-        }
         scanBarcode.value=code
         if(itemActive.relatedMaterial && itemActive.relatedMaterial.length>0){
           relatedMateriaRef.value.show(itemActive)
@@ -210,12 +244,57 @@ const _handlerScan = (code) => {
        showNotify({ type: 'warning', duration: 3000, message: `商品条码${code},已全部扫描完成`})
      }
     }else {
-      scanBarcode.value=''
-      showNotify({ type: 'warning', duration: 3000, message: `商品条码${code},不匹配请重新扫描!`})
-      scanError()
+      //查询组合条码
+      matchedSku.value=[]
+      getListCombineSku({combineSku:code, workEnvironment:'check'}).then(res=>{
+        if(res.data.length>0){
+          const combineSkuMap=toMap(res.data,'barcode')
+          const matchedSkuList=getBarcodeCombine(orderList.value,combineSkuMap)
+          if(matchedSkuList.length>0){
+            if(matchedSkuList.length==res.data.length){
+              matchedSku.value=matchedSkuList
+              checkBarcodeCombineRef.value.show(matchedSku.value,orderList.value)
+            }else{
+              scanError()
+              showDialog({
+                title:'温馨提示',
+                message:'组合商品与拣货任务不匹配,请检查组合商品配置!'
+              })
+            }
+          }else {
+            scanError()
+            showDialog({
+              title:'温馨提示',
+              message:'组合商品与拣货任务不匹配,请检查组合商品配置!'
+            })
+          }
+        }else {
+          scanError()
+          scanBarcode.value=''
+          showNotify({ type: 'warning', duration: 3000, message: `商品条码${code},不匹配请重新扫描!`})
+        }
+      })
     }
   }
 }
+
+//组合商品匹配到的商品
+const getBarcodeCombine=(goodsList, combineSkuMap)=>{
+  const result = goodsList.map(item => {
+    const barcode = item.barcode || item.barcodeAs;
+    // 如果有匹配数据添加到 item 中
+    if (combineSkuMap[barcode] && item.qty >= combineSkuMap[barcode].quantity ) {
+      return {
+        ...item,
+        matchedJson: combineSkuMap[barcode]
+      };
+    }
+    return null
+  })
+    .filter(item => item !== null) // 过滤掉 null 元素,保留匹配到的项
+  return result.length > 0 ? result : []
+}
+
 //扣除商品数量
 const weightRef=ref(null)
 const cutBarcode = (itemActive, count) => {
@@ -232,11 +311,12 @@ const cutBarcode = (itemActive, count) => {
     tips.value = '请继续扫描条码';
     showNotify({ type: 'success', duration: 3000, message: '数量已扣除,请继续扫描条码' });
   } else {
-    // 如果所有商品已扫描完毕,根据总重量判断提示信息
     if (!totalWeight.value) {
       tips.value = '请输入重量';
       weightRef.value?.focus()
+      showNotify({ type: 'success', duration: 3000, message: '商品扫描完成,请输入重量' });
     } else {
+      endCheck()
       tips.value = '商品扫描完成,请点击结束复核';
     }
   }
@@ -253,7 +333,7 @@ const reset=()=>{
     totalWeight.value=''
     orderDetail.value={}
     orderMap.value={}
-    orderList.value=[]
+    dataList.value=[]
     matchBarcodeList.value=[]
     tips.value='请扫描商品条码'
     inputBarcodeRef.value?.show('', '请扫描波次/容器号', '')
@@ -275,13 +355,13 @@ const onPrint=(code)=>{
   printer.value=code
   localStorage.setItem('check-print',JSON.stringify(code))
 }
-const print=(templateCode)=>{
+const print=(templateCode,code)=>{
   if(!printer.value){
     scanError()
     showNotify({ type: 'warning', duration: 3000, message: '请先设置打印机' });
     return
   }
-  const data = {warehouse,code:orderDetail.value.waveNo,printServer: printer.value.server, printName:printer.value.printer,templateCode }
+  const data = {warehouse,code,printServer: printer.value.server, printName:printer.value.printer,templateCode }
   showLoading()
   fluxPrint(data)
     .then(res => {
@@ -299,6 +379,11 @@ const successNumber=ref(0)
 const errorNumber=ref(0)
 //结束复核
 const endCheck=()=>{
+  if(!printer.value){
+    scanError()
+    showNotify({ type: 'warning', duration: 3000, message: '请先设置打印机' });
+    return
+  }
   const allCount = orderList.value.reduce((sum, item) => sum + Math.max(Number(item.qty), 0), 0);
   if(allCount>0){
     scanError()
@@ -314,20 +399,26 @@ const endCheck=()=>{
     weightRef.value?.focus()
     return
   }
-  if (lastNumber > 0) {
-    const dataGroup = orderMap.value.dataGroup
-    for (const order of Object.values(dataGroup)) {
-      for (const item of order) {
-        if (item.qty !== 0) {
-          item.quantity = item.qty
-          item.qty = 0
+  showDialog({
+    title: '温馨提示',
+    message: '是否进行复核操作?',
+    theme: 'round-button',
+  }).then(() => {
+    if (lastNumber > 0) {
+      const dataGroup = orderMap.value.dataGroup
+      for (const order of Object.values(dataGroup)) {
+        for (const item of order) {
+          if (item.qty !== 0 && item.status=='60') {
+            item.quantity = item.qty
+            item.qty = 0
+          }
         }
       }
     }
-  }
-  packingRequests(0,lastNumber)
-  successNumber.value=0
-  errorNumber.value=0
+    packingRequests(0,lastNumber)
+    successNumber.value=0
+    errorNumber.value=0
+  })
 }
 //装箱
 const reversePickingRef=ref(null)
@@ -359,6 +450,7 @@ const packingRequests = async (startIndex = 0, lastNumber) => {
         code: item.orderNo,
         totalGrossWeight:Number(totalWeight.value),
         groupDetailList,
+        activityOrderFlag:true
       };
         showLoadingToast({
           duration: 0,
@@ -374,6 +466,9 @@ const packingRequests = async (startIndex = 0, lastNumber) => {
           orderMap.value.freezeGroup[item.orderNo] = item.orderNo
         }else {
           successNumber.value+=1
+          if(isQualityCheck.value && startIndex==0){
+            print('A3014_PACK_CARTON_QC',res.data)
+          }
         }
         await packingRequests(startIndex + 1, lastNumber)
       }
@@ -406,7 +501,10 @@ const onSelect=(item)=>{
   }else if(item.value=='reset'){
     reset()
   }else if(item.value=='packing'){
-    print('A3014_PACK_CARTON_PACKINGLIST')
+    print('A3014_PACK_CARTON_PACKINGLIST',orderDetail.value.waveNo)
+  }
+  else if(item.value=='end'){
+    endCheck()
   }
 }
 
@@ -429,12 +527,18 @@ const setBarcode = (code) => {
       tips.value = '请扫描商品条码'
       waveNo.value = code
       orderDetail.value = res.data
+      const allStatus= res.data.details.every(item => item.status == 60);
+      if(!allStatus){
+        inputBarcodeRef.value?.show('', '请扫描波次/容器号', `${code}:没有拣货完成请拣货`)
+        return
+      }
       orderMap.value=getDataList(res.data.details)
       if(orderMap.value.dataGroup && Object.keys(orderMap.value.dataGroup).length>0){
-        orderList.value=orderMap.value.dataGroup[Object.keys(orderMap.value.dataGroup)[0]]
-        isUniqueCode.value = orderList.value.some(item => item.uniqueRegExp || item.imeiRegExp );
+        dataList.value=orderMap.value.dataGroup[Object.keys(orderMap.value.dataGroup)[0]]
+        isUniqueCode.value = dataList.value.some(item => item.uniqueRegExp || item.imeiRegExp );
+        isQualityCheck.value = dataList.value.some(item => item.qualityCheck === true);
       }else {
-        orderList.value=[]
+        dataList.value=[]
       }
       matchBarcodeList.value=[]
       scanBarcode.value=''
@@ -480,7 +584,12 @@ const getDataList = (data) => {
   });
   return groupedData
 };
-
+const rowStyle=( row )=>{
+  if(row.status!='60' ){
+    return { background: '#b3b3b3'}
+  }
+  return ''
+}
 
 //切换波次
 const _setWaveNo = () => {

+ 7 - 0
src/views/outbound/check/components/BatchPacking.vue

@@ -82,6 +82,7 @@ const recursiveRequest = async (index, dataList, item) => {
       groupDetailList: [dataList[index]],
     }
     showLoading()
+
     const res = await packingReview(data)
     if (res.data && res.data != '0000'  && res.data != '1111') {
       showNotify({ type: 'success', duration: 3000, message: res.data + '装箱成功' })
@@ -93,6 +94,12 @@ const recursiveRequest = async (index, dataList, item) => {
       } else {
         emit('print', 'PRINT_WAYBILL', res.data)
       }
+      const curQualityCheck= packingList.value.some(item => item.qualityCheck === true)
+      if(curQualityCheck){
+        setTimeout(()=>{
+          emit('print','A3014_PACK_CARTON_QC',res.data)
+        },300)
+      }
       await recursiveRequest(index + 1, dataList, item)
       closeLoading()
     } else {

+ 118 - 0
src/views/outbound/check/components/CheckBarcodeCombine.vue

@@ -0,0 +1,118 @@
+<template>
+  <div class="goods">
+    <van-dialog v-model:show="goodsTrueFalseBy"
+                :beforeClose="beforeClose"
+                title="组合商品包含"
+                show-cancel-button>
+        <div style="width:100%;max-height:150px;overflow:auto">
+          <div   v-for="(item,index) in matchedSku" :key="index" >
+            <van-cell center :title="item.matchedJson.barcode"  :label="item.matchedJson.skuName" >
+              <template #value>
+                <div>{{ item.matchedJson.quantity }}件</div>
+                <div class="goods-tips">预期数量:{{item.qty}}件</div>
+              </template>
+            </van-cell>
+          </div>
+        </div>
+        <div class="goods-number">应复核数量:{{maxCount}}</div>
+        <van-field label="实复核数" type="number" class="code-input" v-model="count" ref="countRef"  autocomplete="off" placeholder="实复核数" />
+    </van-dialog>
+  </div>
+</template>
+<script setup lang="ts">
+import { computed, ref } from 'vue'
+import { showToast } from 'vant'
+import { formatDateTime } from '@/utils/date'
+const goodsTrueFalseBy=ref(false)
+const countRef=ref(null)
+const count=ref('');
+
+const matchedSku=ref([])
+const orderList=ref([])
+const maxCount = ref(0);
+const matchedSkuList =()=> {
+  let minNumber = Infinity;
+  matchedSku.value.forEach((item) => {
+    // 计算每个 item 的 number 值
+    const number = item.qty / item.matchedJson.quantity;
+    if (number < minNumber) {
+      minNumber = number;
+    }
+  });
+  maxCount.value = Math.floor(minNumber);
+}
+const show = async (list,order) => {
+  matchedSku.value=list
+  orderList.value=order
+  matchedSkuList()
+  count.value=''
+  goodsTrueFalseBy.value = true
+  setTimeout(()=>{
+    countRef.value?.focus()
+  },200)
+}
+//输入拣货容器号
+const emit = defineEmits(['cutBarcode'])
+const  beforeClose = (action) =>
+  new Promise(async (resolve) => {
+    if (action === 'confirm') {
+      if (count.value == '') {
+        showToast('请输入复核数量')
+        return resolve(false)
+      }
+      if (count.value<=0) {
+        showToast('请输入正确复核数量')
+        return resolve(false)
+      }
+      if (count.value >maxCount.value) {
+        showToast({duration:3000,message:'复核数量不能大于应复核数量'})
+        return resolve(false)
+      }
+      const data= dataResult(matchedSku.value)
+      data.forEach((item,index) => {
+        orderList.value.forEach((items,index) => {
+          if(item.lotNum==items.lotNum){
+            emit('cutBarcode', items,item.checkNumber)
+          }
+        })
+
+      })
+    }
+    resolve(true)
+  });
+const dataResult=(data)=>{
+  const list=JSON.parse(JSON.stringify(data))
+  list.forEach((item,index)=>{
+    item.checkNumber=item.matchedJson.quantity * count.value
+    delete item.matchedJson
+  })
+  return list
+}
+defineExpose({show})
+</script>
+<style scoped lang="sass">
+.goods
+  .code-input
+    font-size: 16px
+    font-weight: bold
+    border-bottom: 2px solid #0077ff
+  :deep(.van-cell--center)
+    padding: 5px 20px
+  :deep(.van-cell__title)
+    text-align: left !important
+  :deep(.van-cell__value)
+    width: 40% !important
+    flex: 0 0 40% !important
+    color: #000
+  :deep(.van-field__control)
+    font-size: 18spx
+  .goods-number
+    text-align: left
+    font-size: 16px
+    padding-left: 20px
+    margin-top: 5px
+  .goods-tips
+    font-size: 12px
+    text-align: right
+    color: #333
+</style>

+ 6 - 0
src/views/outbound/check/components/CheckPacking.vue

@@ -76,6 +76,12 @@ const setPacking = () => {
       // 冻结
       emit('cancelOrder',orderDetail.value,'release')
     }else {
+      const curQualityCheck= packingList.value.some(item => item.qualityCheck === true)
+      if(curQualityCheck){
+        setTimeout(()=>{
+          emit('print','A3014_PACK_CARTON_QC',res.data)
+        },300)
+      }
       showNotify({ type: 'success', duration: 3000, message: res.data + '装箱成功'})
       scanSuccess()
       emit('resetPackingStatus')

+ 8 - 4
src/views/outbound/check/large/index.vue

@@ -70,7 +70,7 @@
               <th>数量</th>
               <th>剩余</th>
               <th>状态</th>
-              <th v-if="isUniqueCode">标记</th>
+              <th v-if="isUniqueCode || isQualityCheck">标记</th>
             </tr>
             </thead>
             <tbody>
@@ -83,9 +83,10 @@
               </td>
               <td>{{ item.qty }}</td>
               <td>{{ statusMap[item.status] || '' }}</td>
-              <td v-if="isUniqueCode">
+              <td v-if="isUniqueCode || isQualityCheck">
                 <van-tag v-if="item.uniqueRegExp" type="warning">唯一码</van-tag>
                 <van-tag v-if="item.imeiRegExp" type="primary">IMEI码</van-tag>
+                <van-tag v-if="item.qualityCheck" type="warning" >质检</van-tag>
               </td>
             </tr>
             <tr v-else>
@@ -162,6 +163,7 @@ const orderDetail = ref({})
 //装箱明细
 const packingDetailMap=ref({})
 const isUniqueCode = ref(false)
+const isQualityCheck=ref(false)
 const statusMap = {
   '00': '创建',
   '20': '预配',
@@ -490,9 +492,10 @@ const setBarcode = (code) => {
         cancelOrder(res.data)
         return
       }
-      if(res.data.waveType!=='M'){
+      const orderNos = [...new Set(res.data.details.map(item => item.orderNo))];
+      if(orderNos.length>1){
         scanError()
-        inputBarcodeRef.value?.show('', '请扫描订单/快递单号', '仅支持大件单复核','')
+        inputBarcodeRef.value?.show('', '请扫描订单/快递单号', '仅支持单个订单复核','')
         return
       }
       orderNo.value = code
@@ -516,6 +519,7 @@ const setBarcode = (code) => {
       res.data.details.push(...relatedMaterialList)
       dataList.value = res.data.details
       isUniqueCode.value = dataList.value.some(item => item.uniqueRegExp || item.imeiRegExp)
+      isQualityCheck.value = dataList.value.some(item => item.qualityCheck === true);
       scanBarcode.value = ''
     }
   }).catch(err => {

+ 34 - 2
src/views/outbound/picking/list/index.vue

@@ -52,6 +52,10 @@
           <div class="right">
             <div class="right-list"  v-for="(item,index) in locationList[activeIndex].list"
                  :class="activeClass(item)">
+              <div v-if="index==0 && item.locationContainer " class="location-container">
+                <span class="location-label">库位容器:</span>
+                <span class="location-value">{{item.locationContainer}}</span>
+              </div>
               <div>{{ ownerMap[item.owner] || item.owner }}</div>
               <div class="content" @click="onLotAtt(item)" >
                 <div class="c-left">
@@ -201,6 +205,8 @@ const preBarcode=ref('')
 const taskItem=ref([])
 //匹配组合商品数据
 const matchedSku=ref([])
+//库位容器
+const locationContainerMap=ref({})
 // 切换容器号
 const containerNoInputRef=ref(null)
 const onContainerNo=(type)=>{
@@ -220,6 +226,7 @@ const loadData =  async (pickingCode,type) => {
   if(taskList.value.length==0){
     return
   }
+  locationContainerMap.value=toMap(taskList.value,'locationContainer','location')
   result()
   allQuantity.value=expectedQuantity.value
   taskItem.value=taskList.value
@@ -372,9 +379,13 @@ const _handlerScan=(code)=> {
       showToast({duration:5000,message:'请先扫描容器号'})
       return
     }
-    if(taskMap.value[code.toUpperCase()]){
+    let location=code
+    if(locationContainerMap.value[barcodeToUpperCase(code)]){
+      location=locationContainerMap.value[barcodeToUpperCase(code)]
+    }
+    if(taskMap.value[barcodeToUpperCase(location)]){
       nextLocation.value=''
-      activeIndex.value = locationList.value.findIndex(item => item.location === barcodeToUpperCase(code))
+      activeIndex.value = locationList.value.findIndex(item => item.location === barcodeToUpperCase(location))
       onScan(3)
       scanSuccess()
     }else{
@@ -993,6 +1004,27 @@ const onRefresh = () => {
         margin-bottom: 5px
         border-bottom: 1px solid #D3D3D3
         border-radius: 0 8px 8px 0
+
+        .location-container
+          margin-bottom: 5px
+          padding: 2px 10px
+          background: linear-gradient(90deg, #f8f9fa 0%, #e9ecef 100%)
+          border: 1px solid #dee2e6
+          border-radius: 4px
+          border-left: 3px solid #6c757d
+
+          .location-label
+            font-size: 11px
+            color: #6c757d
+            font-weight: 500
+            letter-spacing: 0.3px
+
+          .location-value
+            font-size: 13px
+            color: #495057
+            font-weight: 600
+            margin-left: 2px
+
         .content
           display: flex
           justify-content: space-between

+ 3 - 3
src/views/outbound/picking/task/index.vue

@@ -84,8 +84,8 @@
           <table border="1" style="width: 100%;border-collapse: collapse;text-align: center;table-layout: fixed;font-size: 14px" >
             <thead>
             <tr>
-              <th>承运商</th>
-              <th style="width: 60px">数量</th>
+              <th style="width: 130px">承运商</th>
+              <th>数量</th>
               <th>操作</th>
             </tr>
             </thead>
@@ -96,7 +96,7 @@
               </td>
               <td style="word-wrap: break-word;"><span :style="item.hours>=2?'color:#ee0a24;font-weight: 500':''">{{item.residualOrderQty}}单</span></td>
               <td>
-                <van-button type="danger" size="mini" @click="remove(item)" >结束攒单</van-button>
+<!--                <van-button type="danger" size="mini" @click="remove(item)" >结束攒单</van-button>-->
                 <van-button type="primary" size="mini" @click="onSubCreateTask(item)" >获取任务</van-button>
               </td>
             </tr>

+ 183 - 0
src/views/processing/register/components/addRegisterType.vue

@@ -0,0 +1,183 @@
+<template>
+  <div class="container-no-container">
+    <van-dialog v-model:show="typeTrueFalseBy"
+                :beforeClose="beforeClose"
+                title="登记类型/数量"
+                show-cancel-button>
+      <van-field name="checkboxGroup" label="加工类型" required label-width="70px">
+        <template #input>
+          <van-checkbox-group v-model="type" direction="horizontal">
+            <div v-for="(item,index) in props.processingTypeList" :key="item.code" style="margin-bottom: 8px">
+              <van-checkbox :name="item.code" shape="square"
+                            v-if=" item.code !== 'PROFESSIONAL' && item.code !== 'DEEP' &&item.code !== 'FINE'"
+              >{{ item.name }}
+              </van-checkbox>
+            </div>
+          </van-checkbox-group>
+        </template>
+      </van-field>
+      <van-field name="radio" label="质检类型" label-width="70px" :required="type.includes('ALL_CHECK')">
+        <template #input>
+          <van-radio-group v-model="checkType" direction="horizontal" label-width="70px">
+            <div v-for="(item,index) in props.processingTypeList" :key="item.code" style="margin-bottom: 8px">
+              <van-radio v-if="item.code == 'PROFESSIONAL' || item.code == 'DEEP' || item.code == 'FINE'"
+                         :name="item.code">{{ item.name }}
+              </van-radio>
+            </div>
+          </van-radio-group>
+        </template>
+      </van-field>
+      <van-field v-model="count" type="digit" ref="countRef" @keydown.enter="onKeydown" clearable autocomplete="off"
+                 label="加工数量" placeholder="请输入加工数量" required />
+    </van-dialog>
+  </div>
+</template>
+<script setup lang="ts">
+import { ref } from 'vue'
+import { showToast } from 'vant'
+import { scanError } from '@/utils/android'
+const typeTrueFalseBy = ref(false)
+const type = ref([])
+const count = ref('')
+const countRef = ref(null)
+const checkType = ref('')
+// 接收父组件传递的数据
+const props = defineProps({
+  processingTypeList: Array
+})
+const typeList=ref([])
+const show = async (list) => {
+  typeTrueFalseBy.value = true
+  type.value = []
+  count.value = ''
+  checkType.value = ''
+  typeList.value=list
+}
+//输入拣货容器号
+const emit = defineEmits(['setProcessingType'])
+const validateData = () => {
+
+  // 验证质检类型
+  if (type.value.includes('ALL_CHECK') && !checkType.value) {
+    showToast('选择的加工类型包含质检,请选择质检类型')
+    scanError()
+    return false
+  }
+  const data=getType()
+  // 验证加工类型
+  if (data.operate.length === 0) {
+    showToast('请先选择加工类型')
+    scanError()
+    return false
+  }
+  // 验证加工数量
+  if (count.value <= 0) {
+    showToast('请输入加工数量')
+    scanError()
+    countRef.value?.focus()
+    return false
+  }
+  return true
+}
+const getType=()=>{
+  let operate = [...type.value]
+  operate = operate.filter(item => item !== 'ALL_CHECK')
+  if (checkType.value) {
+    operate.push(checkType.value)
+  }
+  return  { operate, expectAmount: count.value }
+}
+const beforeClose = (action) => new Promise(async (resolve) => {
+  if (action === 'confirm') {
+    if (!validateData()) {
+      return resolve(false)
+    }
+    const data=getType()
+    const isExist = typeList.value.some(item =>
+      item.operate.length === data.operate.length &&
+      item.operate.every(op => data.operate.includes(op)) &&
+      data.operate.every(op => item.operate.includes(op))
+    );
+    if(isExist){
+      showToast('选择的加工类型已存在,请检查')
+      scanError()
+      return resolve(false)
+    }
+    emit('setProcessingType', data)
+    resolve(true)
+  } else {
+    resolve(true)
+  }
+})
+const onKeydown = () => {
+  if (!validateData()) {
+    return
+  }
+  const data=getType()
+  const isExist = typeList.value.some(item =>
+    item.operate.length === data.operate.length && // 长度必须相同
+    item.operate.every(op => data.operate.includes(op)) && // 所有元素都在 data.operate 中
+    data.operate.every(op => item.operate.includes(op)) // 所有元素都在 item.operate 中
+  );
+  if(isExist){
+    showToast('选择的加工类型已存在,请检查')
+    scanError()
+    return
+  }
+  typeTrueFalseBy.value = false
+  emit('setProcessingType', data)
+}
+
+defineExpose({ show })
+</script>
+<style scoped lang="sass">
+.container-no-container
+  .code-input
+    font-size: 22px
+    font-weight: bold
+    border-bottom: 2px solid #0077ff
+    margin-top: 10px
+
+  .completion
+    text-align: right
+    font-size: 12px
+    padding: 5px 20px
+    cursor: pointer
+    text-decoration: underline
+
+  .container-list
+    max-height: 100px
+    font-size: 13px
+    font-weight: bold
+    display: flex
+    padding: 5px 20px
+    justify-content: space-between
+    flex-wrap: wrap
+    overflow: auto
+
+    .container-item
+      width: 50%
+      display: flex
+      justify-items: center
+      align-items: center
+      padding: 2px 0
+      cursor: pointer
+      text-decoration: underline
+
+      .container-item-line
+        width: 5px
+        height: 5px
+        background: #0077ff
+        border-radius: 50%
+        margin-right: 5px
+
+      .container-item-no
+        flex: 1
+        text-align: left
+
+.tips
+  font-size: 14px
+  color: #ff4141
+  line-height: 20px
+
+</style>

+ 236 - 0
src/views/processing/register/index.vue

@@ -0,0 +1,236 @@
+<template>
+  <div class="register">
+    <van-nav-bar
+      title="加工建单" left-arrow fixed placeholder @click-left="goBack" @click-right="onConfirm()">
+      <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>
+    <van-field v-model="date" type="text" label="登记日期" placeholder="请选择日期"
+               readonly is-link input-align="right" @click="dateTrueFalseBy=true" required
+    />
+    <van-field v-model="ownerCode" type="text" label="登记货主" placeholder="请选择货主" readonly
+               is-link @click="ownerRef.show(1)" input-align="right" required
+    />
+    <van-field v-model="code" type="textarea" label="单据信息" placeholder="请填写快递单号/收货任务号,多个用逗号/空格/换行符进行分割"
+               show-word-limit maxlength="500" rows="2" autosize required
+    />
+    <van-field v-model="note" type="textarea" label="&nbsp;&nbsp;备注信息" placeholder="请输入备注信息"
+               show-word-limit  maxlength="500" rows="1" autosize
+    />
+    <div class="register-content">
+      <div class="add-type">
+        <van-button type="primary" round size="small" @click="addType">添加类型</van-button>
+      </div>
+      <div class="type-box">
+        <div class="type-title">类型表一览</div>
+        <div class="type-list">
+          <div class="type-item">
+            <div class="type-item-item">类型</div>
+            <div class="type-item-operate">数量</div>
+            <div class="type-item-operate">操作</div>
+          </div>
+          <div  class="type-item" v-for="(item,index) in typeList" v-if="typeList.length>0">
+            <div  class="type-item-item">{{getOperate(item.operate)}}</div>
+            <div class="type-item-operate">{{item.expectAmount}}</div>
+            <div class="type-item-delete" @click="del(item,index)" >删除</div>
+          </div>
+          <div v-else>
+            <van-empty :image="<string>nodataUrl" image-size="140" >
+              <van-button round type="primary" class="bottom-button" size="small" @click="addType">添加</van-button>
+            </van-empty>
+          </div>
+        </div>
+      </div>
+    </div>
+    <owner ref="ownerRef" @onOwner="onOwner" />
+    <van-calendar first-day-of-week="1" v-model:show="dateTrueFalseBy" @confirm="dateConfirm" />
+    <add-register-type ref="addRegisterTypeRef"
+                       :processingTypeList="processingTypeList"
+                       @setProcessingType="setProcessingType"  />
+  </div>
+</template>
+<script setup lang="ts">
+import { ref } from 'vue'
+import { getHeader, goBack, scanError, scanSuccess } from '@/utils/android'
+import { showConfirmDialog, showNotify, showToast } from 'vant'
+import { useStore } from '@/store/modules/user'
+import { closeLoading, showLoading } from '@/utils/loading'
+import Owner from '@/components/Owner.vue'
+import { formatDate } from '@/utils/date'
+import { useRouter } from 'vue-router'
+import nodataUrl from '@/assets/nodata.png'
+import AddRegisterType from '@/views/processing/register/components/addRegisterType.vue'
+import { createProcessing, getProcessingTypeList } from '@/api/processing'
+import { toArray, toMap } from '@/utils/dataType'
+const router = useRouter()
+try {
+  getHeader()
+}catch (error) {
+  router.push('/login')
+}
+const store = useStore()
+const warehouse = store.warehouse
+const ownerRef = ref(null)
+const ownerItem = ref({})
+const ownerCode = ref('')
+const date = ref(formatDate(new Date()))
+const note = ref('')
+const code=ref('')
+const processingTypeList=ref([])
+const processingTypeMap=ref({})
+//添加类型
+const addRegisterTypeRef=ref(null)
+const typeList = ref([])
+const dateTrueFalseBy = ref(false)
+//选择货主
+const onOwner = (item) => {
+  ownerItem.value = item
+  ownerCode.value = item.name
+}
+//选择日期
+const dateConfirm = (e) => {
+  date.value = formatDate(e)
+  dateTrueFalseBy.value = false
+}
+//提交登记
+const onConfirm = () => {
+  if(!ownerCode.value){
+    showNotify({ type: 'warning', duration: 3000, message: '请选择货主' })
+    scanError()
+    return
+  }
+  if(typeList.value.length==0){
+    showNotify({ type: 'warning', duration: 3000, message: '请先添加单据类型' })
+    scanError()
+    return
+  }
+  if(!code.value){
+    showNotify({ type: 'warning', duration: 3000, message: '请先添加单据信息' })
+    scanError()
+    return
+  }
+
+  showConfirmDialog({
+    title: '温馨提示', message:'是否确认建单' })
+    .then(() => {
+    _createProcessing()
+    }).catch(() => {})
+}
+
+const _createProcessing=()=>{
+  const data={owner:ownerItem.value.id,ownerCode:ownerItem.value.code,note:note.value,warehouse,typeList:typeList.value,noList:toArray(code.value)}
+  showLoading()
+  createProcessing(data).then(res=>{
+    closeLoading()
+    scanSuccess()
+    showConfirmDialog({
+      title: '建单成功',
+      message:`加工单号为:${res.data}`,
+      confirmButtonText:'复制'
+    })
+      .then(() => {
+        copyText(res.data)
+      })
+      .catch(() => {});
+    reset()
+  }).catch(err=>{
+    scanError()
+    closeLoading()
+  })
+}
+const copyText=(txt) =>{
+  const textarea = document.createElement('textarea');
+  textarea.value =txt;
+  document.body.appendChild(textarea);
+  textarea.select();
+  try {
+    const successful = document.execCommand('copy');
+    if (successful) {
+      showNotify({ type: 'success', duration: 3000, message: '已复制' })
+    } else {
+      console.error('复制失败');
+    }
+  } catch (err) {
+    showNotify({ type: 'danger', duration: 3000, message: '复制失败' })
+  } finally {
+    document.body.removeChild(textarea);
+  }
+}
+const reset=()=>{
+  code.value=""
+  typeList.value=[]
+  note.value=""
+  ownerCode.value=''
+}
+//添加类型
+const addType=()=>{
+  addRegisterTypeRef.value?.show(typeList.value)
+}
+const setProcessingType=(data)=>{
+  typeList.value.push(data)
+}
+//转换类型名称
+const getOperate=(operate)=>{
+  const result = operate.map(item => processingTypeMap.value[item] || item)
+  return result.join(',')
+}
+//获取类型
+const _getProcessingTypeList=()=>{
+  getProcessingTypeList().then(res=>{
+    processingTypeList.value=res.data
+    processingTypeMap.value=toMap(res.data,'code','name')
+  })
+}
+const del=(item,index)=>{
+  typeList.value.splice(index,1)
+}
+_getProcessingTypeList()
+const loadData=()=>{
+  date.value=formatDate(new Date())
+}
+window.onRefresh = loadData
+</script>
+<style scoped lang="sass">
+.register
+  .register-content
+    background: #fff
+    padding: 10px
+    font-size: 14px
+    .add-type
+      text-align: right
+    .type-box
+      text-align: left
+      .type-title
+        line-height: 30px
+        color: #999
+      .type-list
+        border: 1px solid #e9e9e9
+        border-radius: 6px
+        padding: 5px 10px
+        .type-item
+          display: flex
+          align-items: center
+          border-bottom: 1px solid #e9e9e9
+
+          padding: 3px 0
+          .type-item-item
+            flex: 1
+          .type-item-operate
+            width: 70px
+            text-align: center
+          .type-item-delete
+            width: 70px
+            text-align: center
+            color: #ff4141
+            text-decoration: underline
+        .type-item:last-child
+          border-bottom: none
+
+
+
+</style>

+ 842 - 0
src/views/robot/takeDelivery/task/index.vue

@@ -0,0 +1,842 @@
+<template>
+  <div class="container">
+    <van-nav-bar title="智能仓收货" left-arrow fixed placeholder @click-left="goBack">
+      <template #left>
+        <van-icon name="arrow-left" size="25" />
+        <div style="color: #fff">返回</div>
+      </template>
+    </van-nav-bar>
+    <div class="take-delivery">
+      <div class="take-info">
+        <div class="take-info-no">
+          <div class="info-no-title">
+            <div>任务号:{{ taskInfo.taskNo || '--' }}</div>
+            <div>
+              <van-button type="primary" size="mini" plain @click="switchTask">切换任务</van-button>
+            </div>
+          </div>
+          <div class="info-no-tips">
+            <div>货主:<span style="color: #333;font-weight: bold;">{{ taskInfo.customerName || '--' }}</span></div>
+            <div>任务数:<span style="color: #0077ff;font-weight: bold;">{{ taskInfo.receivedQty || 0 }}/{{ taskInfo.expectedQty || 0}}</span></div>
+          </div>
+        </div>
+        <div class="take-info-number">
+          <div class="info-number-left">
+            <div class="number-left-box">
+              <div>开始时间:</div>
+              <div class="left-box-title">{{ currentTime }}</div>
+            </div>
+            <div class="number-left-box">
+              <div>已用时:</div>
+              <div class="left-box-title">{{ formattedTime }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+      <van-progress v-if="taskInfo.receivedQty/taskInfo.expectedQty>=0" :percentage="((taskInfo.receivedQty/taskInfo.expectedQty)*100).toFixed(2)" stroke-width="4" />
+      <div class="take-barcode">
+          <div class="barcode-input">
+            <van-search
+              ref="searchRef"
+              v-model.lazy="searchBarcode"
+              placeholder="请扫描商品条码"
+              @search="_handlerScan(searchBarcode)"
+              label="商品条码:"
+              left-icon=""
+              :class="[scanType===2?'search-input-barcode':'','van-hairline--bottom']"
+              @focus="scanType=2"
+              autocomplete="off"
+              @input="onAsnCancel"
+              @clear="reset"
+            >
+            </van-search>
+          </div>
+        <div class="barcode-input">
+            <van-search
+              ref="containerNoRef"
+              v-model.lazy="containerNo"
+              placeholder="请扫描库位号"
+              @search="_handlerScan(containerNo)"
+              label="&nbsp;&nbsp;&nbsp;&nbsp;库位号:"
+              left-icon=""
+              :class="[scanType===5?'search-input-barcode':'','van-hairline--bottom']"
+              @focus="scanType=5"
+              autocomplete="off"
+              @input="onAsnCancel"
+            >
+            </van-search>
+          </div>
+        <div class="barcode-input">
+          <van-search
+            ref="numberRef"
+            v-model="searchCount"
+            placeholder="请输入收货数量"
+            type="number"
+            label="收货数量:"
+            left-icon=""
+            autocomplete="off"
+            show-action
+            :min="1"
+            :max="asnInfo.asnNo?asnInfo.expectedQuantity-asnInfo.receivedQuantity:10000"
+            @search="isCheck()"
+            :class="[scanType===4?'search-input-barcode':'','van-hairline--bottom']"
+            @focus="scanType=4"
+          >
+            <template #action>
+              <div style="display: flex; align-items: center;flex-direction: column;margin-left: 20px"
+                   v-if="asnInfo.asnNo">
+                <div style="height: 20px;font-size: 12px">已收/预计</div>
+                <div style="font-size: 16px;font-weight: bold">{{ asnInfo.receivedQuantity}}/{{ asnInfo.expectedQuantity }}
+                </div>
+              </div>
+            </template>
+          </van-search>
+        </div>
+      </div>
+      <div class="take-lot" v-if="lotData.length>0">
+        <van-cell-group>
+          <div class="take-lot-title">批次信息</div>
+          <van-cell v-for="(item,i) in lotData" :key="i" :is-link="item.field!=='lotAtt05'"
+                    @click="onLot(item)">
+            <template #title>
+              <van-icon v-if="item.require" name="warning-o" color="#ed6a0c" />
+              <span class="custom-title">{{ item.label }}</span>
+            </template>
+            <template #value>
+              <div>{{ item.mapping }}</div>
+            </template>
+          </van-cell>
+        </van-cell-group>
+      </div>
+      <div class="take-button">
+        <van-button type="primary" size="large" round style="height: 36px" @click="onConfirm">完成收货</van-button>
+      </div>
+    </div>
+  </div>
+  <!-- 条码输入组件 -->
+  <input-barcode :back="back" @setBarcode="setBarcode" ref="inputBarcodeRef"  />
+  <!--  单据选择-->
+  <van-action-sheet v-model:show="asnDetailsTrueFalseBy" cancel-text="取消" description="请选择具体单据" close-on-click-action>
+    <van-cell-group>
+      <van-cell v-for="item in asnDetailsList" @click="onDetailActive(item)">
+        <template #title>
+          {{ item.asnNo }}({{ item.customerId }}-{{ item.expectedQuantity }}件)
+        </template>
+      </van-cell>
+    </van-cell-group>
+  </van-action-sheet>
+  <!--  商品物理属性-->
+  <attribute ref="attributeRef" @set-attribute="setAttribute" />
+  <!--  商品批次属性-->
+  <lot-date ref="lotDateRef" @select-lot-date="selectLotDate" />
+  <!--  唯一码-->
+  <unique-code-input ref="uniqueCodeRef"
+                     v-model:uniqueCodeList="uniqueCodeList"
+                     v-model:scanType="scanType"
+                     v-model:checkAllType="checkAllType"
+                     :searchCount="searchCount"
+                     :asnInfo="asnInfo"
+                     @setUniqueCode="onConfirm"
+
+  />
+  <van-action-sheet
+    v-model:show="lotQualityTrueFalseBy"
+    cancel-text="取消"
+    close-on-click-action
+    description="请选择质量状态"
+  >
+    <van-cell-group>
+      <van-cell v-for="(value,key) in lotQualityMap" @click="onSelectLotQuality(key)">
+        <template #title>
+          {{value}}
+        </template>
+      </van-cell>
+    </van-cell-group>
+  </van-action-sheet>
+</template>
+
+<script setup>
+import { onMounted, onUnmounted, ref } from 'vue'
+import { androidFocus, getHeader, goBack, scanError, scanSuccess } from '@/utils/android'
+import InputBarcode from '@/views/outbound/picking/components/InputBarcode.vue'
+import { closeListener, openListener, scanInit } from '@/utils/keydownListener'
+import { useRouter } from 'vue-router'
+import {
+  calculateShelfLife, getCommodityRule,
+  getIReceivingTask,
+  getProductAttribute, getProductLot,
+  getReceivingAsnDetails, setBotReceiving,
+  setProductAttribute,
+} from '@/api/takeDelivery/index'
+import { closeLoading, showLoading } from '@/utils/loading'
+import { useStore } from '@/store/modules/user'
+import { showNotify, showToast } from 'vant'
+import { isAttribute } from '@/views/inbound/takeDelivery/task/hooks/attribute'
+import Attribute from '@/views/inbound/takeDelivery/components/Attribute.vue'
+import LotDate from '@/views/inbound/takeDelivery/components/LotDate.vue'
+import UniqueCodeInput from '@/views/inbound/takeDelivery/components/UniqueCodeInput.vue'
+import { barcodeToUpperCase, toMap } from '@/utils/dataType'
+import { getCurrentTime } from '@/utils/date'
+
+const router = useRouter()
+const store = useStore()
+try {
+  getHeader()
+  androidFocus()
+} catch (error) {
+  router.push('/login')
+}
+const warehouse = store.warehouse
+//开单任务号
+const taskNo = ref('')
+//容器号
+const containerNo = ref('')
+//商品条码
+const searchBarcode = ref('')
+//收货数量
+const searchCount = ref('')
+//收货详情
+const taskInfo = ref({ receivedQty: 0, expectedQty: 0 })
+//开始时间
+const currentTime = ref('--')
+const scanType = ref(2)
+
+const type=localStorage.getItem('checkAllType')?JSON.parse(localStorage.getItem('checkAllType')):true
+const checkAllType=ref(type)
+// 页面初始化
+onMounted(() => {
+  openListener()
+  scanInit(_handlerScan)
+  loadData()
+
+})
+/**
+ * 计算时分秒
+ */
+// 时器的总秒数
+let totalSeconds = ref(0)
+//时分秒
+const formattedTime = ref('00:00:00')
+let windowTimer = null // 计时器的引用
+const updateFormattedTime = () => {
+  let hours = Math.floor(totalSeconds.value / 3600)
+  let minutes = Math.floor((totalSeconds.value % 3600) / 60)
+  let seconds = totalSeconds.value % 60
+  formattedTime.value = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
+}
+// 启动计时器
+const startTimer = () => {
+  if (!windowTimer) {
+    windowTimer = setInterval(() => {
+      totalSeconds.value++
+      updateFormattedTime()
+    }, 1000)
+  }
+}
+
+// 停止计时器
+const stopTimer = () => {
+  if (windowTimer) {
+    clearInterval(windowTimer)
+    windowTimer = null
+  }
+}
+
+const back = ref(true)
+const inputBarcodeType = ref('task')
+//输入框组件
+const inputBarcodeRef = ref(null)
+const oldSearchBarcode = ref('')
+// 设置容器号
+const setBarcode = (code, type) => {
+  if (inputBarcodeType.value === 'lot') {
+    lotData.value.forEach((lot) => {
+      if (lot.field == lotField.value) {
+        lot.mapping = code
+      }
+    })
+    return
+  }
+  showLoading()
+  if(!type){ //切换任务时初始化计时器
+    stopTimer()
+    formattedTime.value="00:00:00"
+    totalSeconds.value=0
+  }
+  getIReceivingTask({ taskNo:code,version:'V6' }).then(res=>{
+    back.value = true
+    if(res.data.receivedQty==res.data.expectedQty && res.data.expectedQty>0 ){
+      if(type){
+        reset()
+        taskNo.value=''
+        taskInfo.value={}
+        switchTask()
+      }else {
+        taskInfo.value=res.data
+        taskNo.value=code
+      }
+      containerNo.value=''
+      stopTimer()
+    }else {
+      if(!type){//切换任务成功重启计时器
+        currentTime.value=getCurrentTime()
+        startTimer()
+        containerNo.value=''
+      }
+      taskInfo.value=res.data
+      taskNo.value=code
+    }
+    scanType.value=2
+    uniqueCodeList.value=[]
+    scanSuccess()
+  }).catch(err=>{
+    inputBarcodeRef.value?.show('', '请扫描开单任务号',err.message)
+    scanError()
+  }).finally(()=>{
+    reset()
+    closeLoading()
+  })
+}
+// setBarcode('BSSH20250605000006')
+
+//切换任务
+const switchTask = () => {
+  inputBarcodeType.value = 'switchTask'
+  back.value = false
+  inputBarcodeRef.value?.show('', `请扫描开单任务号`,'')
+}
+
+//asn单多条明细展示
+const asnDetailsTrueFalseBy = ref(false)
+const asnDetailsList = ref([])
+const asnInfo = ref({})
+const reset = () => {
+  asnInfo.value = {}
+  lotData.value = []
+  searchCount.value = ''
+  searchBarcode.value = ''
+  oldSearchBarcode.value = ''
+  containerNo.value=''
+  uniqueCodeList.value = []
+}
+// 选择单据
+const onDetailActive = (item) => {
+  asnInfo.value = item
+  asnDetailsTrueFalseBy.value = false
+  searchCount.value=1
+  scanType.value=5
+  // searchCount.value = asnInfo.value.expectedQuantity - asnInfo.value.receivedQuantity
+  _getProductAttribute(item)
+  _getProductLot(item)
+  _getCommodityRule(item)
+}
+const onAsnCancel = () => {
+  if (searchBarcode.value === '' || (oldSearchBarcode.value.length != searchBarcode.value.length && oldSearchBarcode.value != '')) {
+    asnInfo.value = {}
+    lotData.value = []
+    searchCount.value = ''
+    containerNo.value=''
+  }
+}
+const uniqueCodeList = ref([])
+
+// 扫描条码监听
+const _handlerScan = (code) => {
+  if (scanType.value == 2) {
+    searchBarcode.value = code
+    oldSearchBarcode.value = code
+    const params = { warehouse, barcode: code, asnNos: taskInfo.value?.asnNos.join(',') }
+    showLoading()
+    getReceivingAsnDetails(params).then(res => {
+      uniqueCodeList.value=[]
+      asnDetailsList.value = res.data
+      if (res.data.length > 0) {
+        scanSuccess()
+        closeLoading()
+        if (res.data.length == 1) {
+          const item = res.data[0]
+          asnInfo.value = item
+          // searchCount.value = item.expectedQuantity - item.receivedQuantity
+          searchCount.value=1
+          _getProductAttribute(item)
+          _getProductLot(item)
+          _getCommodityRule(item)
+          scanType.value=5
+        }
+        if (res.data.length > 1) {
+          asnInfo.value = {}
+          lotData.value = []
+          searchCount.value = ''
+          uniqueCodeList.value = []
+          asnDetailsTrueFalseBy.value = true
+        }
+      } else {
+        scanError()
+        showNotify({ type: 'danger', duration: 3000, message: `暂未查询到条码《${code}》信息请重试` })
+        reset()
+        closeLoading()
+      }
+    }).catch(() => {
+      scanError()
+      closeLoading()
+    })
+  } else if (scanType.value == 3) {
+    if (code) {
+      const uniqueCodeScanType = uniqueCodeRef.value?.uniqueCodeScanType
+      if (checkAllType.value && uniqueCodeScanType === 'barcode') {
+        const barcode = Array.from(new Set([asnInfo.value.barcode, asnInfo.value.barcode2, asnInfo.value.sku].filter(Boolean)));
+        if (barcode.some(item => barcodeToUpperCase(item) === barcodeToUpperCase(code))) {
+          scanSuccess();
+          uniqueCodeRef.value.uniqueCodeScanType = 'unique'
+          uniqueCodeRef.value.uniqueBarcode = code
+          return;
+        } else {
+          showNotify({ type: 'danger', duration: 3000, message: `${code}-商品条码不匹配,请重新扫描` })
+          uniqueCodeRef.value.uniqueBarcode = ''
+          scanError();
+          return;
+        }
+      }
+      // 如果是通过唯一码扫描,并且没有扫描条码
+      if (uniqueCodeScanType === 'unique' && ( uniqueCodeRef.value.uniqueBarcode == '' && checkAllType.value)) {
+        showNotify({ type: 'danger', duration: 3000, message: '请先扫描商品条码' });
+        uniqueCodeRef.value.uniqueCodeScanType = 'barcode';
+        scanError();
+        return;
+      }
+      const uniqueRegExp = uniqueRuleMap.value['sku'] ? uniqueRuleMap.value['sku'] : uniqueRuleMap.value['all']
+      const isValidCode = new RegExp(uniqueRegExp).test(code)
+      if (!isValidCode) {
+        scanError()
+        showNotify({ type: 'danger', duration: 3000, message: `唯一码《${code}》不符合规则、请重新扫描` })
+        return
+      }
+      if (uniqueCodeList.value.includes(code)) {
+        scanError()
+        showNotify({ type: 'danger', duration: 3000, message: `唯一码《${code}》已存在列表内请重新扫描` })
+        return
+      }
+      scanSuccess()
+      uniqueCodeList.value.unshift(code)
+      uniqueCodeRef.value.uniqueBarcode=''
+      if(checkAllType.value){
+        uniqueCodeRef.value.uniqueCodeScanType='barcode'
+      }else {
+        uniqueCodeRef.value.uniqueCodeScanType='unique'
+      }
+
+    }
+  }else if(scanType.value==5){
+    containerNo.value=barcodeToUpperCase(code)
+    scanSuccess()
+    // isCheck()
+  }
+}
+/**
+ * 物理属性
+ */
+const attributeRef = ref(null)
+const attributeMap = ref({})
+const attributeTrueFalseBy = ref(true)
+// 获取商品物理属性
+const _getProductAttribute = (item) => {
+  const params = { warehouse: item.warehouse, owner: item.customerId, barcode: item.sku }
+  getProductAttribute(params).then(res => {
+    attributeMap.value = res.data
+    const isAttributeInfo = isAttribute(res.data)
+    if (isAttributeInfo.length > 0) {
+      attributeTrueFalseBy.value = false
+      attributeRef.value?.show(isAttributeInfo, res.data)
+    } else {
+      attributeTrueFalseBy.value = true
+    }
+  })
+}
+// 设置商品物理属性
+const setAttribute = (data) => {
+  const params = { warehouse, owner: taskInfo.value.customerId, sku: attributeMap.value.sku }
+  showLoading()
+  setProductAttribute(params, { ...attributeMap.value, ...data }).then(res => {
+    showToast({ duration: 3000, message: '商品物理属性设置成功' })
+    attributeTrueFalseBy.value = true
+    scanSuccess()
+  }).catch(err => {
+    closeLoading()
+    scanError()
+  })
+}
+/**
+ * 物理属性 end
+ */
+
+/**
+ * 商品批次属性
+ */
+// 获取商品批次属性
+const lotData = ref([])
+const _getProductLot = (item) => {
+  const params = { warehouse: item.warehouse, owner: item.customerId, barcode: item.sku }
+  getProductLot(params).then(res => {
+    res.data.forEach((lot) => {
+      const lotField = lot.field
+      if (lotField.startsWith('lotAtt') && lotField.length === 8) {
+        lot.mapping = item[lotField]
+      }
+    })
+    lotData.value = res.data
+    _calculateShelfLife(item, lotData.value)
+  })
+}
+// 计算效期
+const _calculateShelfLife = (item, lotData) => {
+  if ((item.lotAtt01 && item.lotAtt02) || (!item.lotAtt01 && !item.lotAtt02)) return
+  const params = {
+    customer: item.customerId,
+    barcode: item.sku,
+    date: item.lotAtt01 || item.lotAtt02,
+    isExpiryDate: item.lotAtt01 ? true : false,
+  }
+  calculateShelfLife(params).then(date => {
+    lotData.forEach((lot) => {
+      if ((lot.field === 'lotAtt01' && !params.isExpiryDate) ||
+        (lot.field === 'lotAtt02' && params.isExpiryDate)) {
+        lot.mapping = date.data
+      }
+    })
+  })
+}
+//日期选择
+const lotField = ref('')
+const lotDateRef = ref(null)
+const lotQualityTrueFalseBy=ref(false)
+const lotQualityMap=ref({})
+const onLot = (item) => {
+  lotField.value = item.field
+  if (item.field == 'lotAtt05' ) return
+  if ( item.field == 'lotAtt08'){
+    lotQualityMap.value=JSON.parse(item.format)
+    lotQualityTrueFalseBy.value = true
+    return
+  }
+  if (item.type == 'Date') {
+    lotDateRef.value?.show(item.label, item.mapping)
+    return
+  }
+  if (item.type == 'String') {
+    inputBarcodeType.value = 'lot'
+    back.value = false
+    inputBarcodeRef.value?.show('', `请扫描${item.label}`,'')
+    return
+  }
+}
+//设置质量状态
+const onSelectLotQuality=(key)=>{
+  lotData.value.forEach((lot) => {
+    if (lot.field == lotField.value) {
+      lot.mapping = key
+    }
+  })
+  lotQualityTrueFalseBy.value=false
+}
+
+// 设置日期
+const selectLotDate = (date) => {
+  const lotMap = toMap(lotData.value, 'field', 'mapping')
+  if ((lotField.value === 'lotAtt01' || lotField.value === 'lotAtt02') &&
+    lotMap['lotAtt01'] == '' || lotMap['lotAtt01'] == null && lotMap['lotAtt02'] == '' || lotMap['lotAtt02'] == null) {
+    const params = {
+      customer: asnInfo.value.customerId,
+      barcode: asnInfo.value.sku,
+      date,
+      isExpiryDate: lotField.value === 'lotAtt01' ? true : false,
+    }
+    calculateShelfLife(params).then(res => {
+      if(res.data){
+        lotData.value.forEach((lot) => {
+          if (lot.field === 'lotAtt01') {
+            lot.mapping = (lotField.value === 'lotAtt01') ? date : res.data
+          } else if (lot.field === 'lotAtt02') {
+            lot.mapping = (lotField.value === 'lotAtt02') ? date : res.data
+          }
+        })
+      }else {
+        lotData.value.forEach((lot) => {
+          if (lot.field === lotField.value) {
+            lot.mapping = date
+          }
+        })
+      }
+
+    })
+  }
+  lotData.value.forEach((lot) => {
+    if (lot.field === lotField.value) {
+      lot.mapping = date
+    }
+  })
+  inputBarcodeType.value = 'task'
+}
+
+/**
+ * 商品批次属性end
+ */
+/**
+ * 唯一码
+ */
+const uniqueCodeRef = ref(null)
+//规则列表
+const uniqueRuleList = ref([])
+const uniqueRuleMap = ref({})
+// 获取商品规则
+const _getCommodityRule = (item) => {
+  const params = { customer: item.customerId, sku: item.sku, input: 'RECEIVING' }
+  getCommodityRule(params).then(res => {
+    uniqueRuleList.value = res.data
+    res.data.forEach((item, index) => {
+      if (item.sku == '') {
+        item.type = 'all'
+      } else {
+        item.type = 'sku'
+      }
+    })
+    uniqueRuleMap.value = toMap(res.data, 'type', 'uniqueRegExp')
+  })
+}
+/**
+ * 唯一码end
+ */
+const containerNoRef = ref(null)
+const numberRef = ref(null)
+// 完成收货校验
+const isCheck = () => {
+  if (!asnInfo.value.asnNo) {
+    scanError()
+    showToast({ duration: 3000, message: '请先查询商品收货信息' })
+    return false
+  }
+  //商品物理属性判断
+  if (!attributeTrueFalseBy.value) {
+    const isAttributeInfo = isAttribute(attributeMap.value)
+    if (isAttributeInfo.length > 0) {
+      attributeTrueFalseBy.value = false
+      attributeRef.value?.show(isAttributeInfo, attributeMap.value)
+    }
+    return false
+  }
+  // //商品批次属性判断
+  const incompleteLot = lotData.value.find(lot => lot.require && !lot.mapping)
+  if (incompleteLot) {
+    scanError()
+    showToast({ duration: 3000, message: `请先补充${incompleteLot.label}` })
+    return false
+  }
+  const lotMap = toMap(lotData.value, 'field', 'mapping')
+  const productionDate = lotMap['lotAtt01'] ? new Date(lotMap['lotAtt01']) : null
+  const expirationDate = lotMap['lotAtt02'] ? new Date(lotMap['lotAtt02']) : null
+  const currentDate = new Date()
+// 如果存在失效日期
+  if (expirationDate) {
+    // 如果有生产日期,进行有效性检查
+    if (productionDate && expirationDate <= productionDate) {
+      scanError()
+      showToast({ duration: 3000, message: `失效日期不能小于等于生产日期` })
+      return false
+    }
+    // 检查失效日期是否小于当前日期
+    if (expirationDate <= currentDate) {
+      scanError()
+      showToast({ duration: 3000, message: `失效日期不能小于等于当前日期` })
+      return false
+    }
+  }
+  if(productionDate){
+    // 如果有生产日期,进行有效性检查
+    if (expirationDate && productionDate >= expirationDate) {
+      scanError()
+      showToast({ duration: 3000, message: `生产日期不能大于失效日期` })
+      return false
+    }
+    // 检查生产日期是否小于当前日期
+    if (productionDate >= currentDate) {
+      scanError()
+      showToast({ duration: 3000, message: `生产日期不能大于等于当前日期` })
+      return false
+    }
+  }
+  if (searchCount.value == '') {
+    numberRef.value?.focus()
+    scanError()
+    showToast({ duration: 3000, message: '请先输入收货数量' })
+    return false
+  }
+  if (containerNo.value == '') {
+    scanError()
+    containerNoRef.value?.focus()
+    showToast({ duration: 3000, message: '请先扫描库位号' })
+    scanType.value=5
+    return false
+  }
+  // 唯一码收集
+  if (uniqueRuleList.value.length > 0 && uniqueCodeList.value.length != searchCount.value) {
+      scanType.value = 3
+      uniqueCodeRef.value?.show('', '请扫描唯一码', `收货数量:${searchCount.value}`, uniqueRuleMap.value)
+      return false
+  }
+  return true
+}
+// 收货
+const onConfirm = () => {
+  if(isCheck()){
+    const lotMap = toMap(lotData.value, 'field', 'mapping')
+    const { asnLineNo, asnNo, warehouse } = asnInfo.value
+    const data = {
+      asnLineNo,
+      asnNo,
+      containerId: containerNo.value,
+      quantity: searchCount.value,
+      warehouse,
+      serialNos: uniqueCodeList.value.length > 0 ? uniqueCodeList.value : undefined,
+      ...lotMap,
+    }
+    showLoading()
+    inputBarcodeType.value='task'
+    setBotReceiving(data).then(res => {
+      scanSuccess()
+      showNotify({ type: 'success', duration: 3000, message: `${searchBarcode.value}收货完成,请继续收货!`})
+      setBarcode(taskNo.value, '2')
+      reset()
+      taskInfo.value={}
+      scanType.value=2
+      closeLoading()
+    }).catch(err => {
+      if(err.message.includes('序列号已存在')){
+        scanType.value = 3
+        uniqueCodeRef.value?.show('', '请扫描唯一码', `收货数量:${searchCount.value},${err.message}`, uniqueRuleMap.value)
+      }
+      scanError()
+      closeLoading()
+    })
+    }
+}
+
+// 数据刷新
+const loadData = () => {
+  if (!taskNo.value) {
+    inputBarcodeRef.value?.show('', '请扫描开单任务号','')
+    return
+  } else {
+    // currentTime.value=getCurrentTime()
+    // startTimer()
+  }
+}
+onUnmounted(() => {
+  closeListener()
+  stopTimer()
+})
+window.onRefresh = loadData
+
+</script>
+<style scoped lang="sass">
+.take-delivery
+  .take-info
+    padding: 6px 10px
+    background: #e0eeff
+    display: flex
+    flex-direction: column
+    text-align: left
+
+    .take-info-no
+      flex: 1
+
+      .info-no-title
+        font-size: 18px
+        display: flex
+        justify-content: space-between
+        align-items: center
+
+      .info-no-tips
+        font-size: 14px
+        color: #666666
+        display: flex
+        justify-content: space-between
+        padding: 6px 0
+
+    .take-info-number
+      flex: 1
+      border-top: 1.5px solid #ffffff
+      display: flex
+      justify-content: space-between
+      color: #666
+      font-size: 14px
+
+      .info-number-left
+        flex: 1
+        display: flex
+        justify-content: space-between
+        align-items: center
+
+        .number-left-box
+          flex: 1
+          display: flex
+          justify-content: center
+          align-items: center
+
+          .left-box-title
+            font-size: 14px
+            font-weight: bold
+            color: #000
+            line-height: 34px
+
+      .info-number-right
+        width: 0%
+        text-align: center
+
+        .van-search
+          padding: 0
+
+  .take-barcode
+    margin-top: 10px
+    text-align: left
+    background: #FFFFFF
+
+    .barcode-input
+      ::v-deep(.van-search)
+        padding: 0
+
+      ::v-deep(.van-search__field)
+        border-bottom: 2px solid #ffffff
+
+      ::v-deep(.van-search__content)
+        background: #fff
+
+      ::v-deep(.van-field__control)
+        font-size: 15px
+        font-weight: bold
+
+      ::v-deep(.van-search__label)
+        font-size: 15px
+        font-weight: bold
+
+      .search-input-barcode
+        ::v-deep(.van-search__field)
+          border-bottom: 2px solid #0077ff
+          z-index: 2
+
+  .take-lot
+    text-align: left
+    margin-top: 5px
+
+    ::v-deep(.van-cell)
+      padding: 5px 8px
+
+    .take-lot-title
+      font-size: 15px
+      font-weight: bold
+      padding: 0 5px
+      border-left: 3px solid #1989fa
+      color: #333
+      margin-bottom: 3px
+
+  .take-button
+    padding: 20px
+</style>