|
|
@@ -12,7 +12,7 @@
|
|
|
<!-- 信息展示表格 -->
|
|
|
<div class="info-table">
|
|
|
<div class="table-row">
|
|
|
- <div class="cell label">目标库位</div>
|
|
|
+ <div class="cell label">源库位</div>
|
|
|
<div class="cell value">{{ productInfo.targetLocation }}</div>
|
|
|
<div class="cell label">库存数量</div>
|
|
|
<div class="cell value">{{ productInfo.stockQty }}</div>
|
|
|
@@ -28,9 +28,9 @@
|
|
|
<div class="table-row row-small">
|
|
|
<div class="cell label">质量状态</div>
|
|
|
<div class="cell value">{{ productInfo.qualityStatus }}</div>
|
|
|
- <div class="cell label">属性仓</div>
|
|
|
+ <div class="cell label label-small">属性仓</div>
|
|
|
<div class="cell value">{{ productInfo.warehouseType }}</div>
|
|
|
- <div class="cell label">批号</div>
|
|
|
+ <div class="cell label label-small">批号</div>
|
|
|
<div class="cell value"></div>
|
|
|
</div>
|
|
|
<div class="table-row">
|
|
|
@@ -65,41 +65,43 @@
|
|
|
<div class="grid-section">
|
|
|
<div class="grid-container">
|
|
|
<div
|
|
|
- v-for="box in boxList"
|
|
|
- :key="box.id"
|
|
|
+ v-for="station in stationList"
|
|
|
+ :key="station.id"
|
|
|
class="box-wrapper"
|
|
|
>
|
|
|
- <!-- 序号在料箱上方 -->
|
|
|
- <div class="box-number">{{ box.id }}</div>
|
|
|
+ <!-- 站台序号在上方 -->
|
|
|
+ <div class="box-number">{{ station.displayNumber }}</div>
|
|
|
<div
|
|
|
class="box-item"
|
|
|
:class="{
|
|
|
- 'box-filled': box.status === 'filled' && !box.splitCount,
|
|
|
- 'box-empty-box': box.status === 'emptyBox',
|
|
|
- 'box-waiting': box.status === 'waiting',
|
|
|
- 'box-selected': selectedBox === box.id,
|
|
|
- 'box-split': box.splitCount
|
|
|
+ '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
|
|
|
}"
|
|
|
- @click="!box.splitCount && selectBox(box)"
|
|
|
+ @click="!station.splitCount && station.status !== 'offline' && selectStation(station)"
|
|
|
>
|
|
|
<!-- 分割的料箱 -->
|
|
|
- <template v-if="box.splitCount && box.subLocations">
|
|
|
- <div class="sub-grid" :style="getSubGridStyle(box.splitCount)">
|
|
|
+ <template v-if="station.splitCount && station.subLocations">
|
|
|
+ <div class="sub-grid" :style="getSubGridStyle(station.splitCount)">
|
|
|
<div
|
|
|
- v-for="sub in box.subLocations"
|
|
|
+ v-for="sub in station.subLocations"
|
|
|
:key="sub.id"
|
|
|
class="sub-location"
|
|
|
:class="{
|
|
|
'sub-filled': sub.status === 'filled',
|
|
|
- 'sub-selected': selectedBox === sub.id
|
|
|
+ 'sub-selected': selectedBox === sub.locationCode,
|
|
|
+ 'sub-disabled': !isLocationClickable(sub.locationCode)
|
|
|
}"
|
|
|
- @click.stop="selectSubLocation(box, sub)"
|
|
|
+ @click.stop="isLocationClickable(sub.locationCode) && selectSubLocation(station, sub)"
|
|
|
></div>
|
|
|
</div>
|
|
|
</template>
|
|
|
- <!-- 普通料箱 -->
|
|
|
+ <!-- 普通站台 -->
|
|
|
<template v-else>
|
|
|
- <span v-if="box.label" class="box-label">{{ box.label }}</span>
|
|
|
+ <span v-if="station.label" class="box-label">{{ station.label }}</span>
|
|
|
</template>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -114,24 +116,236 @@
|
|
|
<van-button class="btn-submit" size="small" @click="submitMove">提交移库</van-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ <!-- 库位信息弹窗 -->
|
|
|
+ <van-popup
|
|
|
+ v-model:show="showLocationPopup"
|
|
|
+ position="bottom"
|
|
|
+ round
|
|
|
+ :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="info-row">
|
|
|
+ <span class="info-label">库位编号</span>
|
|
|
+ <span class="info-value">{{ currentLocation.id }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-row">
|
|
|
+ <span class="info-label">商品名称</span>
|
|
|
+ <span class="info-value">{{ currentLocation.productName || '-' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-row">
|
|
|
+ <span class="info-label">商品条码</span>
|
|
|
+ <span class="info-value">{{ currentLocation.barcode || '-' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-row">
|
|
|
+ <span class="info-label">库存数量</span>
|
|
|
+ <span class="info-value">{{ currentLocation.qty || 0 }}</span>
|
|
|
+ </div>
|
|
|
+ <!-- 推荐库位列表 -->
|
|
|
+ <div class="recommend-section">
|
|
|
+ <div class="recommend-title">
|
|
|
+ {{ currentLocation.recommendType === 'clear' ? '推荐清空' : '推荐保留' }}
|
|
|
+ </div>
|
|
|
+ <div v-if="currentLocation.relatedLocations.length > 0" class="recommend-list">
|
|
|
+ <div
|
|
|
+ v-for="(item, index) in currentLocation.relatedLocations"
|
|
|
+ :key="index"
|
|
|
+ class="recommend-item"
|
|
|
+ >
|
|
|
+ <span class="recommend-location">{{ currentLocation.recommendType === 'clear' ? '保留库位' : '清空库位' }}: {{ item.location }}</span>
|
|
|
+ <span class="recommend-qty">推荐移库数量: {{ item.quantity }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-else class="recommend-empty">暂无推荐库位</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="popup-footer">
|
|
|
+ <van-button type="primary" block @click="confirmSelectLocation">选择此库位</van-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </van-popup>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, reactive } from 'vue'
|
|
|
-import { showToast } from 'vant'
|
|
|
+import { closeListener, openListener, scanInit } from '@/utils/keydownListener'
|
|
|
+import { onMounted, onUnmounted, ref, reactive, watch, nextTick } from 'vue'
|
|
|
+import { showToast, showLoadingToast, closeToast } from 'vant'
|
|
|
+import { useStore } from '@/store/modules/user'
|
|
|
+import { getWorkingDetailsByBox, getBoxSplitCode, type BoxRelatedMergeDetailsVO, type LocationMergeDetails } from '@/api/location/merge'
|
|
|
+
|
|
|
+const store = useStore()
|
|
|
+const warehouse = store.warehouse
|
|
|
+
|
|
|
+
|
|
|
+// 页面初始化
|
|
|
+onMounted(() => {
|
|
|
+ openListener()
|
|
|
+ scanInit(_handlerScan)
|
|
|
+ // 获取焦点
|
|
|
+ nextTick(() => {
|
|
|
+ focusScanInput()
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ closeListener()
|
|
|
+})
|
|
|
+
|
|
|
+// 设置扫描输入框焦点
|
|
|
+const focusScanInput = () => {
|
|
|
+ const input = document.querySelector('.scan-section input') as HTMLInputElement
|
|
|
+ if (input) {
|
|
|
+ input.focus()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 扫描条码监听
|
|
|
+const _handlerScan = (code: string) => {
|
|
|
+ if (code) {
|
|
|
+ boxCode.value = code
|
|
|
+ loadBoxData(code)
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
// 扫描料箱号
|
|
|
const boxCode = ref('')
|
|
|
|
|
|
+// 监听 boxCode 变化,当有值时调用接口
|
|
|
+watch(boxCode, (newVal) => {
|
|
|
+ if (newVal && newVal.length > 5) {
|
|
|
+ // 防抖处理,避免输入过程中频繁调用
|
|
|
+ loadBoxData(newVal)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// 加载料箱数据
|
|
|
+const loadBoxData = async (code: string) => {
|
|
|
+ if (!code) return
|
|
|
+
|
|
|
+ try {
|
|
|
+ showLoadingToast({ message: '加载中...', forbidClick: true })
|
|
|
+
|
|
|
+ // 1. 调用 getWorkingDetailsByBox 获取任务详情
|
|
|
+ const res = await getWorkingDetailsByBox(code, warehouse)
|
|
|
+ const boxDetailsList: BoxRelatedMergeDetailsVO[] = res.data || []
|
|
|
+
|
|
|
+ if (boxDetailsList.length === 0) {
|
|
|
+ closeToast()
|
|
|
+ showToast('未找到相关任务')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存返回的数据
|
|
|
+ mergeDataList.value = boxDetailsList
|
|
|
+
|
|
|
+ // 2. 遍历 mergeDetails 取源库位和目标库位,去重
|
|
|
+ const locationSet = new Set<string>()
|
|
|
+ boxDetailsList.forEach(boxDetail => {
|
|
|
+ boxDetail.mergeDetails?.forEach((detail: LocationMergeDetails) => {
|
|
|
+ if (detail.sourceLocation) locationSet.add(detail.sourceLocation)
|
|
|
+ if (detail.targetLocation) locationSet.add(detail.targetLocation)
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ const locations = Array.from(locationSet)
|
|
|
+
|
|
|
+ if (locations.length > 0) {
|
|
|
+ // 3. 调用 getBoxSplitCode 获取库位分割信息
|
|
|
+ const splitRes = await getBoxSplitCode(warehouse, locations)
|
|
|
+ const splitMap: Record<string, number> = splitRes.data || {}
|
|
|
+
|
|
|
+ // 4. 构建可点击库位信息
|
|
|
+ buildClickableLocationsMap(boxDetailsList)
|
|
|
+
|
|
|
+ // 5. 初始化站台区域
|
|
|
+ updateStationList(boxDetailsList, splitMap)
|
|
|
+ }
|
|
|
+
|
|
|
+ closeToast()
|
|
|
+ } catch (error: any) {
|
|
|
+ closeToast()
|
|
|
+ showToast(error.message || '加载失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 合并数据列表
|
|
|
+const mergeDataList = ref<BoxRelatedMergeDetailsVO[]>([])
|
|
|
+
|
|
|
+// 构建可点击库位信息Map
|
|
|
+const buildClickableLocationsMap = (boxDetailsList: BoxRelatedMergeDetailsVO[]) => {
|
|
|
+ const map = new Map<string, ClickableLocationInfo>()
|
|
|
+
|
|
|
+ boxDetailsList.forEach(boxDetail => {
|
|
|
+ boxDetail.mergeDetails?.forEach((detail: LocationMergeDetails) => {
|
|
|
+ const sourceLocation = detail.sourceLocation
|
|
|
+ const targetLocation = detail.targetLocation
|
|
|
+ const quantity = detail.moveQty || 0
|
|
|
+
|
|
|
+ // 处理源库位(推荐清空库位)
|
|
|
+ if (sourceLocation) {
|
|
|
+ if (!map.has(sourceLocation)) {
|
|
|
+ map.set(sourceLocation, {
|
|
|
+ recommendType: 'clear',
|
|
|
+ relatedLocations: [],
|
|
|
+ productName: detail.productName || '',
|
|
|
+ barcode: detail.barcode || '',
|
|
|
+ qty: quantity
|
|
|
+ })
|
|
|
+ }
|
|
|
+ const sourceInfo = map.get(sourceLocation)!
|
|
|
+ // 添加对应的保留库位
|
|
|
+ if (targetLocation) {
|
|
|
+ const existingIdx = sourceInfo.relatedLocations.findIndex(r => r.location === targetLocation)
|
|
|
+ if (existingIdx === -1) {
|
|
|
+ sourceInfo.relatedLocations.push({ location: targetLocation, quantity })
|
|
|
+ } else {
|
|
|
+ sourceInfo.relatedLocations[existingIdx].quantity += quantity
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理目标库位(推荐保留库位)
|
|
|
+ if (targetLocation) {
|
|
|
+ if (!map.has(targetLocation)) {
|
|
|
+ map.set(targetLocation, {
|
|
|
+ recommendType: 'keep',
|
|
|
+ relatedLocations: [],
|
|
|
+ productName: detail.productName || '',
|
|
|
+ barcode: detail.barcode || '',
|
|
|
+ qty: 0
|
|
|
+ })
|
|
|
+ }
|
|
|
+ const targetInfo = map.get(targetLocation)!
|
|
|
+ // 添加对应的清空库位
|
|
|
+ if (sourceLocation) {
|
|
|
+ const existingIdx = targetInfo.relatedLocations.findIndex(r => r.location === sourceLocation)
|
|
|
+ if (existingIdx === -1) {
|
|
|
+ targetInfo.relatedLocations.push({ location: sourceLocation, quantity })
|
|
|
+ } else {
|
|
|
+ targetInfo.relatedLocations[existingIdx].quantity += quantity
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ clickableLocationsMap.value = map
|
|
|
+}
|
|
|
+
|
|
|
// 商品信息
|
|
|
const productInfo = reactive({
|
|
|
- targetLocation: 'HK000001',
|
|
|
+ targetLocation: '',
|
|
|
stockQty: '',
|
|
|
productName: '',
|
|
|
barcode: '',
|
|
|
- qualityStatus: 'ZP',
|
|
|
- warehouseType: 'CHZP',
|
|
|
+ qualityStatus: '',
|
|
|
+ warehouseType: '',
|
|
|
batchNo: '',
|
|
|
productionDate: '',
|
|
|
expiryDate: '',
|
|
|
@@ -148,62 +362,126 @@ const confirmMoveQty = () => {
|
|
|
isEditingMoveQty.value = false
|
|
|
}
|
|
|
|
|
|
-// 料箱数据 - 支持分割成多个库位
|
|
|
+// 站台数据结构
|
|
|
interface SubLocation {
|
|
|
id: string
|
|
|
status: 'empty' | 'filled' | 'selected'
|
|
|
+ locationCode: string // 实际库位编码
|
|
|
}
|
|
|
|
|
|
-interface BoxItem {
|
|
|
+interface StationItem {
|
|
|
id: number
|
|
|
- status: 'empty' | 'filled' | 'emptyBox' | 'waiting'
|
|
|
+ stationCode: string // 站台编码 RLOCHK13A01011
|
|
|
+ displayNumber: string // 显示序号 13-24
|
|
|
+ status: 'offline' | 'waiting' | 'filled' | 'emptyBox' | 'error'
|
|
|
label?: string
|
|
|
- splitCount?: number // 分割数量: 2, 4, 6, 8
|
|
|
- subLocations?: SubLocation[] // 子库位
|
|
|
+ splitCount?: number
|
|
|
+ subLocations?: SubLocation[]
|
|
|
+ boxCode?: string
|
|
|
+ inventoryLocations?: string[]
|
|
|
+}
|
|
|
+
|
|
|
+// 站台列表
|
|
|
+const stationList = ref<StationItem[]>([])
|
|
|
+
|
|
|
+// 初始化站台列表(12个站台,编号13-24)
|
|
|
+const initStations = () => {
|
|
|
+ stationList.value = Array.from({ length: 12 }, (_, i) => {
|
|
|
+ const num = String(i + 13).padStart(2, '0')
|
|
|
+ return {
|
|
|
+ id: i + 1,
|
|
|
+ stationCode: `RLOCHK${num}A01011`,
|
|
|
+ displayNumber: num,
|
|
|
+ status: 'offline' as const
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 判断库位是否可点击
|
|
|
+const isLocationClickable = (locationCode: string): boolean => {
|
|
|
+ return clickableLocationsMap.value.has(locationCode)
|
|
|
}
|
|
|
|
|
|
// 生成子库位
|
|
|
-const generateSubLocations = (boxId: number, count: number, filledIndexes: number[] = []): SubLocation[] => {
|
|
|
- return Array.from({ length: count }, (_, i) => ({
|
|
|
- id: `${boxId}-${i + 1}`,
|
|
|
- status: filledIndexes.includes(i) ? 'filled' : 'empty'
|
|
|
- }))
|
|
|
+const generateSubLocations = (boxCode: string, splitCount: number, inventoryLocations: string[]): SubLocation[] => {
|
|
|
+ return Array.from({ length: splitCount }, (_, i) => {
|
|
|
+ // 库位编码: 分割数为1时库位=料箱编码;否则为 料箱编码-序号
|
|
|
+ const locationCode = splitCount === 1 ? boxCode : `${boxCode}-${i + 1}`
|
|
|
+ return {
|
|
|
+ id: `${boxCode}-${i + 1}`,
|
|
|
+ locationCode: locationCode,
|
|
|
+ status: inventoryLocations.includes(locationCode) ? 'filled' : 'empty'
|
|
|
+ }
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
-const boxList = ref<BoxItem[]>([
|
|
|
- { id: 1, status: 'filled' },
|
|
|
- {
|
|
|
- id: 2,
|
|
|
- status: 'empty',
|
|
|
- splitCount: 2,
|
|
|
- subLocations: generateSubLocations(2, 2, [0])
|
|
|
- },
|
|
|
- {
|
|
|
- id: 3,
|
|
|
- status: 'empty',
|
|
|
- splitCount: 4,
|
|
|
- subLocations: generateSubLocations(3, 4, [0, 1])
|
|
|
- },
|
|
|
- {
|
|
|
- id: 4,
|
|
|
- status: 'empty',
|
|
|
- splitCount: 6,
|
|
|
- subLocations: generateSubLocations(4, 6, [0, 2])
|
|
|
- },
|
|
|
- {
|
|
|
- id: 5,
|
|
|
- status: 'empty',
|
|
|
- splitCount: 8,
|
|
|
- subLocations: generateSubLocations(5, 8, [0, 3, 5])
|
|
|
- },
|
|
|
- { id: 6, status: 'empty' },
|
|
|
- { id: 7, status: 'filled' },
|
|
|
- { id: 8, status: 'empty' },
|
|
|
- { id: 9, status: 'emptyBox', label: '空箱' },
|
|
|
- { id: 10, status: 'waiting', label: '等待调箱' },
|
|
|
- { id: 11, status: 'empty' },
|
|
|
- { id: 12, status: 'empty' }
|
|
|
-])
|
|
|
+// 根据接口数据更新站台列表
|
|
|
+const updateStationList = (boxDetailsList: BoxRelatedMergeDetailsVO[], splitMap: Record<string, number>) => {
|
|
|
+ const stationToBoxMap = new Map<string, BoxRelatedMergeDetailsVO>()
|
|
|
+ boxDetailsList.forEach(boxDetail => {
|
|
|
+ if (boxDetail.station) {
|
|
|
+ stationToBoxMap.set(boxDetail.station, boxDetail)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ stationList.value = stationList.value.map(station => {
|
|
|
+ const boxDetail = stationToBoxMap.get(station.stationCode)
|
|
|
+
|
|
|
+ if (!boxDetail) {
|
|
|
+ return { ...station, status: 'offline' as const, label: undefined, splitCount: undefined, subLocations: undefined, boxCode: undefined }
|
|
|
+ }
|
|
|
+
|
|
|
+ const boxCode = boxDetail.boxCode
|
|
|
+ const boxStatus = boxDetail.boxStatus
|
|
|
+ const inventoryLocations = boxDetail.inventoryLocations || []
|
|
|
+
|
|
|
+ let status: StationItem['status'] = 'offline'
|
|
|
+ let label: string | undefined = undefined
|
|
|
+
|
|
|
+ if (boxStatus === 0 || boxStatus === 10) {
|
|
|
+ status = 'waiting'
|
|
|
+ label = '等待调箱'
|
|
|
+ } else if (boxStatus === 20) {
|
|
|
+ if (inventoryLocations.length === 0) {
|
|
|
+ status = 'emptyBox'
|
|
|
+ label = '空箱'
|
|
|
+ } else {
|
|
|
+ status = 'filled'
|
|
|
+ }
|
|
|
+ } else if (boxStatus === 30) {
|
|
|
+ status = 'offline'
|
|
|
+ } else if (boxStatus === 40) {
|
|
|
+ status = 'error'
|
|
|
+ label = '异'
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取分割数量
|
|
|
+ let splitCount = 1
|
|
|
+ if (splitMap[boxCode]) {
|
|
|
+ splitCount = splitMap[boxCode]
|
|
|
+ } else {
|
|
|
+ boxDetail.mergeDetails?.forEach(detail => {
|
|
|
+ if (detail.sourceLocation && splitMap[detail.sourceLocation]) {
|
|
|
+ splitCount = Math.max(splitCount, splitMap[detail.sourceLocation])
|
|
|
+ }
|
|
|
+ if (detail.targetLocation && splitMap[detail.targetLocation]) {
|
|
|
+ splitCount = Math.max(splitCount, splitMap[detail.targetLocation])
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ const updatedStation: StationItem = { ...station, status, label, boxCode, inventoryLocations }
|
|
|
+
|
|
|
+ if (splitCount > 1) {
|
|
|
+ updatedStation.splitCount = splitCount
|
|
|
+ updatedStation.subLocations = generateSubLocations(boxCode, splitCount, inventoryLocations)
|
|
|
+ }
|
|
|
+
|
|
|
+ return updatedStation
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+initStations()
|
|
|
|
|
|
// 获取子库位的grid样式
|
|
|
const getSubGridStyle = (splitCount: number) => {
|
|
|
@@ -225,15 +503,94 @@ const getSubGridStyle = (splitCount: number) => {
|
|
|
// 选中的料箱/库位
|
|
|
const selectedBox = ref<string | number | null>(null)
|
|
|
|
|
|
-// 选择料箱或子库位
|
|
|
-const selectBox = (box: BoxItem, subId?: string) => {
|
|
|
- if (box.status === 'waiting' || box.status === 'emptyBox') return
|
|
|
- selectedBox.value = subId || box.id
|
|
|
+// 可点击库位信息(来自mergeDetails的sourceLocation和targetLocation)
|
|
|
+interface LocationRecommendInfo {
|
|
|
+ location: string
|
|
|
+ quantity: number
|
|
|
+}
|
|
|
+
|
|
|
+interface ClickableLocationInfo {
|
|
|
+ recommendType: 'clear' | 'keep' // clear=推荐清空库位(源), keep=推荐保留库位(目标)
|
|
|
+ relatedLocations: LocationRecommendInfo[] // 对应的推荐库位列表
|
|
|
+ productName: string
|
|
|
+ barcode: string
|
|
|
+ qty: number
|
|
|
+}
|
|
|
+
|
|
|
+// 可点击库位集合 Map<库位编码, 库位推荐信息>
|
|
|
+const clickableLocationsMap = ref<Map<string, ClickableLocationInfo>>(new Map())
|
|
|
+
|
|
|
+// 库位信息弹窗
|
|
|
+const showLocationPopup = ref(false)
|
|
|
+const currentLocation = reactive({
|
|
|
+ id: '',
|
|
|
+ boxId: '',
|
|
|
+ status: '' as 'empty' | 'filled',
|
|
|
+ recommendType: '' as 'clear' | 'keep',
|
|
|
+ productName: '',
|
|
|
+ barcode: '',
|
|
|
+ qty: 0,
|
|
|
+ relatedLocations: [] as LocationRecommendInfo[]
|
|
|
+})
|
|
|
+
|
|
|
+// 选择站台(对于未分割的料箱,库位编码等于料箱编码)
|
|
|
+const selectStation = (station: StationItem) => {
|
|
|
+ if (station.status === 'waiting' || station.status === 'emptyBox' || station.status === 'offline') return
|
|
|
+
|
|
|
+ const locationCode = station.boxCode || ''
|
|
|
+
|
|
|
+ // 检查是否可点击
|
|
|
+ if (!isLocationClickable(locationCode)) {
|
|
|
+ showToast('该库位不在合并任务中')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ selectedBox.value = station.stationCode
|
|
|
+
|
|
|
+ // 获取库位推荐信息
|
|
|
+ const locationInfo = clickableLocationsMap.value.get(locationCode)
|
|
|
+
|
|
|
+ // 显示库位信息弹窗
|
|
|
+ currentLocation.id = locationCode
|
|
|
+ currentLocation.boxId = locationCode
|
|
|
+ currentLocation.status = station.status === 'filled' ? 'filled' : 'empty'
|
|
|
+ currentLocation.recommendType = locationInfo?.recommendType || 'keep'
|
|
|
+ currentLocation.productName = locationInfo?.productName || ''
|
|
|
+ currentLocation.barcode = locationInfo?.barcode || ''
|
|
|
+ currentLocation.qty = locationInfo?.qty || 0
|
|
|
+ currentLocation.relatedLocations = locationInfo?.relatedLocations || []
|
|
|
+ showLocationPopup.value = true
|
|
|
}
|
|
|
|
|
|
// 选择子库位
|
|
|
-const selectSubLocation = (box: BoxItem, sub: SubLocation) => {
|
|
|
- selectedBox.value = sub.id
|
|
|
+const selectSubLocation = (station: StationItem, sub: SubLocation) => {
|
|
|
+ // 检查是否可点击
|
|
|
+ if (!isLocationClickable(sub.locationCode)) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ selectedBox.value = sub.locationCode
|
|
|
+
|
|
|
+ // 获取库位推荐信息
|
|
|
+ const locationInfo = clickableLocationsMap.value.get(sub.locationCode)
|
|
|
+
|
|
|
+ // 显示库位信息弹窗
|
|
|
+ currentLocation.id = sub.locationCode
|
|
|
+ currentLocation.boxId = station.boxCode || ''
|
|
|
+ currentLocation.status = sub.status === 'filled' ? 'filled' : 'empty'
|
|
|
+ currentLocation.recommendType = locationInfo?.recommendType || 'keep'
|
|
|
+ currentLocation.productName = locationInfo?.productName || ''
|
|
|
+ currentLocation.barcode = locationInfo?.barcode || ''
|
|
|
+ currentLocation.qty = locationInfo?.qty || 0
|
|
|
+ currentLocation.relatedLocations = locationInfo?.relatedLocations || []
|
|
|
+ showLocationPopup.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// 确认选择库位
|
|
|
+const confirmSelectLocation = () => {
|
|
|
+ productInfo.targetLocationNew = currentLocation.id
|
|
|
+ showLocationPopup.value = false
|
|
|
+ showToast(`已选择库位: ${currentLocation.id}`)
|
|
|
}
|
|
|
|
|
|
// 呼唤机器人
|
|
|
@@ -245,6 +602,12 @@ const callRobot = () => {
|
|
|
const resetInput = () => {
|
|
|
boxCode.value = ''
|
|
|
selectedBox.value = null
|
|
|
+ mergeDataList.value = []
|
|
|
+ clickableLocationsMap.value = new Map()
|
|
|
+ initStations()
|
|
|
+ nextTick(() => {
|
|
|
+ focusScanInput()
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
// 提交移库
|
|
|
@@ -288,7 +651,7 @@ const submitMove = () => {
|
|
|
|
|
|
&.row-small .cell {
|
|
|
font-size: 11px;
|
|
|
- padding: 6px 4px;
|
|
|
+ padding: 6px 6px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -310,6 +673,10 @@ const submitMove = () => {
|
|
|
flex: 0 0 55px;
|
|
|
}
|
|
|
|
|
|
+ &.label-small {
|
|
|
+ flex: 0 0 40px;
|
|
|
+ }
|
|
|
+
|
|
|
&.value {
|
|
|
color: #333;
|
|
|
|
|
|
@@ -403,6 +770,16 @@ const submitMove = () => {
|
|
|
border-color: #e0b0b0;
|
|
|
}
|
|
|
|
|
|
+ &.box-error {
|
|
|
+ background: #ffcccc;
|
|
|
+ border-color: #ff6666;
|
|
|
+
|
|
|
+ .box-label {
|
|
|
+ color: #ff0000;
|
|
|
+ font-weight: bold;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
&.box-selected {
|
|
|
border: 2px solid #1989fa;
|
|
|
}
|
|
|
@@ -432,6 +809,13 @@ const submitMove = () => {
|
|
|
&.sub-selected {
|
|
|
border: 2px solid #1989fa;
|
|
|
}
|
|
|
+
|
|
|
+ &.sub-disabled {
|
|
|
+ background: #f0f0f0;
|
|
|
+ border-color: #ddd;
|
|
|
+ cursor: not-allowed;
|
|
|
+ opacity: 0.6;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -477,4 +861,102 @@ const submitMove = () => {
|
|
|
color: #fff;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+.location-popup {
|
|
|
+ padding: 16px;
|
|
|
+
|
|
|
+ .popup-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding-bottom: 12px;
|
|
|
+ border-bottom: 1px solid #eee;
|
|
|
+
|
|
|
+ .popup-title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #333;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .popup-content {
|
|
|
+ padding: 12px 0;
|
|
|
+
|
|
|
+ .info-row {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 10px 0;
|
|
|
+ border-bottom: 1px solid #f5f5f5;
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .info-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #666;
|
|
|
+ }
|
|
|
+
|
|
|
+ .info-value {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #333;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .popup-footer {
|
|
|
+ padding-top: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .recommend-section {
|
|
|
+ margin-top: 12px;
|
|
|
+ padding-top: 12px;
|
|
|
+ border-top: 1px solid #eee;
|
|
|
+
|
|
|
+ .recommend-title {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #333;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .recommend-list {
|
|
|
+ max-height: 150px;
|
|
|
+ overflow-y: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .recommend-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 8px 12px;
|
|
|
+ background: #f9f9f9;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-bottom: 6px;
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .recommend-location {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #333;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .recommend-qty {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #666;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .recommend-empty {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #999;
|
|
|
+ text-align: center;
|
|
|
+ padding: 12px 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
</style>
|