zhaohuanhuan 7 месяцев назад
Родитель
Сommit
7c55eef95a

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

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

@@ -23,6 +23,11 @@ export default function() {
             icon: 'newspaper-o',
             path: "blind-receiving"
           },
+          {
+            title: '宝时快上',
+            icon: 'newspaper-o',
+            path: "putaway"
+          },
           {
             title: '海康快上',
             icon: 'newspaper-o',

+ 6 - 0
src/router/index.ts

@@ -121,6 +121,12 @@ const routes: RouteRecordRaw[] = [
     meta:{title:'加工登记'},
     component: () => import('@/views/processing/register/index.vue')
   },
+  {
+    path: '/putaway',
+    name: 'Putaway',
+    meta:{title:'宝时快上'},
+    component: () => import('@/views/inbound/putaway/task/index.vue')
+  },
 ];
 
 // 创建路由实例

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

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

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

@@ -0,0 +1,600 @@
+<template>
+  <div class="container">
+    <van-nav-bar title="宝时快上" left-arrow fixed placeholder @click-left="goBack"
+                 @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">
+        <van-button type="primary" size="large" round style="height: 36px" @click="onConfirm">上架</van-button>
+      </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 => {
+    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)
+}
+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
+      } 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
+  }
+}
+// 获取库存数据
+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: '*',
+        location: searchLocation.value,
+        quantity: takeQuantity, // 当前项扣除的数量
+      })
+      remainingQuantity -= takeQuantity
+    }
+    showLoading()
+    batchPutaway(data).then(res => {
+      if (totalQuantity.value - Number(searchCount.value) == 0) {
+        containerNo.value = ''
+        dataMap.value = {}
+        dataList.value = []
+        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
+</style>