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