|
|
@@ -0,0 +1,1377 @@
|
|
|
+<template>
|
|
|
+ <div class="merge-container">
|
|
|
+ <!-- 扫描输入框区域 -->
|
|
|
+ <div class="scan-section">
|
|
|
+ <van-field
|
|
|
+ ref="boxCodeInputRef"
|
|
|
+ v-model="boxCode"
|
|
|
+ placeholder="请扫描料箱号"
|
|
|
+ clearable
|
|
|
+ @click="onBoxCodeClick"
|
|
|
+ @keyup.enter="onBoxCodeEnter"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 信息展示表格 -->
|
|
|
+ <div class="info-table">
|
|
|
+ <div class="table-row">
|
|
|
+ <div class="cell label">源库位</div>
|
|
|
+ <div class="cell value input-cell">
|
|
|
+ <van-field
|
|
|
+ ref="sourceLocationInputRef"
|
|
|
+ v-model="sourceLocation"
|
|
|
+ placeholder="请扫描源库位"
|
|
|
+ clearable
|
|
|
+ @click="onSourceLocationClick"
|
|
|
+ @keyup.enter="onSourceLocationEnter"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="cell label">库存数量</div>
|
|
|
+ <div class="cell value">{{ productInfo.stockQty }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="table-row">
|
|
|
+ <div class="cell label">商品名称</div>
|
|
|
+ <div class="cell value span-2">{{ productInfo.productName }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="table-row">
|
|
|
+ <div class="cell label">商品条码</div>
|
|
|
+ <div class="cell value span-2 input-cell">
|
|
|
+ <van-field
|
|
|
+ ref="barcodeInputRef"
|
|
|
+ v-model="scanBarcode"
|
|
|
+ placeholder="请扫描商品条码"
|
|
|
+ clearable
|
|
|
+ @click="onBarcodeClick"
|
|
|
+ @keyup.enter="onBarcodeEnter"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="table-row row-small">
|
|
|
+ <div class="cell label">质量状态</div>
|
|
|
+ <div class="cell value value-small">{{ productInfo.qualityStatus }}</div>
|
|
|
+ <div class="cell label label-small">属性仓</div>
|
|
|
+ <div class="cell value value-large">{{ productInfo.warehouseType }}</div>
|
|
|
+ <div class="cell label label-small">批号</div>
|
|
|
+ <div class="cell value value-small"></div>
|
|
|
+ </div>
|
|
|
+ <div class="table-row">
|
|
|
+ <div class="cell label">生产日期</div>
|
|
|
+ <div class="cell value">{{ productInfo.productionDate }}</div>
|
|
|
+ <div class="cell label">失效日期</div>
|
|
|
+ <div class="cell value">{{ productInfo.expiryDate }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="table-row">
|
|
|
+ <div class="cell label">目标库位</div>
|
|
|
+ <div class="cell value input-cell input-wide">
|
|
|
+ <van-field
|
|
|
+ ref="targetLocationInputRef"
|
|
|
+ v-model="productInfo.targetLocationNew"
|
|
|
+ placeholder="请扫描目标库位"
|
|
|
+ clearable
|
|
|
+ @click="onTargetLocationClick"
|
|
|
+ @keyup.enter="onTargetLocationEnter"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="cell label">移库数量</div>
|
|
|
+ <div class="cell value editable" @dblclick="editMoveQty">
|
|
|
+ <template v-if="isEditingMoveQty">
|
|
|
+ <van-field
|
|
|
+ v-model="productInfo.moveQty"
|
|
|
+ type="number"
|
|
|
+ autofocus
|
|
|
+ @blur="confirmMoveQty"
|
|
|
+ @keyup.enter="confirmMoveQty"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <span>{{ productInfo.moveQty }}</span>
|
|
|
+ <span v-if="!productInfo.moveQty" class="placeholder">双击编辑</span>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </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>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <!-- 普通站台或异常状态 -->
|
|
|
+ <template v-else>
|
|
|
+ <span v-if="station.label" class="box-label">{{ station.label }}</span>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 底部按钮 -->
|
|
|
+ <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 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">SKU</span>
|
|
|
+ <span class="info-value">{{ currentLocation.sku || '-' }}</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 { 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 { getInventory, inventoryMovement } from '@/api/inventory'
|
|
|
+import { showConfirmDialog } from 'vant'
|
|
|
+
|
|
|
+const store = useStore()
|
|
|
+const warehouse = store.warehouse
|
|
|
+
|
|
|
+// 扫描类型: 1=料箱号, 2=源库位, 3=商品条码, 4=目标库位
|
|
|
+const scanType = ref(1)
|
|
|
+
|
|
|
+// 输入框引用
|
|
|
+const boxCodeInputRef = ref<any>(null)
|
|
|
+const sourceLocationInputRef = ref<any>(null)
|
|
|
+const barcodeInputRef = ref<any>(null)
|
|
|
+const targetLocationInputRef = ref<any>(null)
|
|
|
+
|
|
|
+// 扫描料箱号
|
|
|
+const boxCode = ref('')
|
|
|
+// 源库位
|
|
|
+const sourceLocation = ref('')
|
|
|
+// 商品条码
|
|
|
+const scanBarcode = ref('')
|
|
|
+
|
|
|
+// 轮询定时器
|
|
|
+let pollingTimer: ReturnType<typeof setInterval> | null = null
|
|
|
+
|
|
|
+// 页面初始化
|
|
|
+onMounted(() => {
|
|
|
+ openListener()
|
|
|
+ scanInit(_handlerScan)
|
|
|
+ // 获取焦点
|
|
|
+ nextTick(() => {
|
|
|
+ focusBoxCodeInput()
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ closeListener()
|
|
|
+ stopPolling()
|
|
|
+})
|
|
|
+
|
|
|
+// 设置料箱号输入框焦点
|
|
|
+const focusBoxCodeInput = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ boxCodeInputRef.value?.focus()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 设置源库位输入框焦点
|
|
|
+const focusSourceLocationInput = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ sourceLocationInputRef.value?.focus()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 设置商品条码输入框焦点
|
|
|
+const focusBarcodeInput = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ barcodeInputRef.value?.focus()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 设置目标库位输入框焦点
|
|
|
+const focusTargetLocationInput = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ targetLocationInputRef.value?.focus()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 扫描监听
|
|
|
+const _handlerScan = (code: string) => {
|
|
|
+ if (!code) return
|
|
|
+
|
|
|
+ if (scanType.value === 1) {
|
|
|
+ // 扫描料箱号
|
|
|
+ boxCode.value = code
|
|
|
+ loadBoxData(code)
|
|
|
+ } else if (scanType.value === 2) {
|
|
|
+ // 扫描源库位
|
|
|
+ sourceLocation.value = code
|
|
|
+ onSourceLocationEnter()
|
|
|
+ } else if (scanType.value === 3) {
|
|
|
+ // 扫描商品条码
|
|
|
+ scanBarcode.value = code
|
|
|
+ onBarcodeEnter()
|
|
|
+ } else if (scanType.value === 4) {
|
|
|
+ // 扫描目标库位
|
|
|
+ productInfo.targetLocationNew = code
|
|
|
+ onTargetLocationEnter()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 料箱号输入框点击 - 重置所有数据
|
|
|
+const onBoxCodeClick = () => {
|
|
|
+ resetAllData()
|
|
|
+}
|
|
|
+
|
|
|
+// 料箱号回车
|
|
|
+const onBoxCodeEnter = () => {
|
|
|
+ if (boxCode.value && boxCode.value.length > 5) {
|
|
|
+ loadBoxData(boxCode.value)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 源库位输入框点击 - 重置除料箱号外的数据
|
|
|
+const onSourceLocationClick = () => {
|
|
|
+ resetExceptBoxCode()
|
|
|
+}
|
|
|
+
|
|
|
+// 源库位回车
|
|
|
+const onSourceLocationEnter = () => {
|
|
|
+ if (!sourceLocation.value) return
|
|
|
+
|
|
|
+ // 清空商品信息
|
|
|
+ resetProductInfo()
|
|
|
+
|
|
|
+ // 切换到扫描商品条码
|
|
|
+ scanType.value = 3
|
|
|
+ focusBarcodeInput()
|
|
|
+}
|
|
|
+
|
|
|
+// 商品条码输入框点击 - 只重置商品信息
|
|
|
+const onBarcodeClick = () => {
|
|
|
+ resetProductInfo()
|
|
|
+}
|
|
|
+
|
|
|
+// 目标库位输入框点击
|
|
|
+const onTargetLocationClick = () => {
|
|
|
+ scanType.value = 4
|
|
|
+}
|
|
|
+
|
|
|
+// 目标库位回车
|
|
|
+const onTargetLocationEnter = () => {
|
|
|
+ if (!productInfo.targetLocationNew) return
|
|
|
+ showToast(`已输入目标库位: ${productInfo.targetLocationNew}`)
|
|
|
+}
|
|
|
+
|
|
|
+// 当前选中的库存数据(用于提交移库)
|
|
|
+const currentInventoryData = ref<any>(null)
|
|
|
+
|
|
|
+// 商品条码回车 - 调用getInventory获取商品信息
|
|
|
+const onBarcodeEnter = async () => {
|
|
|
+ if (!scanBarcode.value) return
|
|
|
+ if (!sourceLocation.value) {
|
|
|
+ showToast('请先扫描源库位')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ showLoadingToast({ message: '查询中...', forbidClick: true })
|
|
|
+
|
|
|
+ const params = {
|
|
|
+ warehouse,
|
|
|
+ barcode: scanBarcode.value,
|
|
|
+ location: sourceLocation.value,
|
|
|
+ locationRegexp: '^(?!STAGE_|SORTATION_).*$'
|
|
|
+ }
|
|
|
+
|
|
|
+ const res = await getInventory(params)
|
|
|
+ closeToast()
|
|
|
+
|
|
|
+ if (res.data && res.data.length > 0) {
|
|
|
+ const inventoryData = res.data[0]
|
|
|
+ // 保存完整的库存数据
|
|
|
+ currentInventoryData.value = inventoryData
|
|
|
+ // 填充商品信息
|
|
|
+ productInfo.targetLocation = sourceLocation.value
|
|
|
+ productInfo.stockQty = inventoryData.quantityAvailable || inventoryData.quantity || ''
|
|
|
+ productInfo.productName = inventoryData.productName || inventoryData.skuName || ''
|
|
|
+ productInfo.barcode = inventoryData.barcode || scanBarcode.value
|
|
|
+ productInfo.qualityStatus = inventoryData.lotAtt08 || inventoryData.qualityStatus || ''
|
|
|
+ productInfo.warehouseType = inventoryData.lotAtt05 || inventoryData.warehouseType || ''
|
|
|
+ productInfo.batchNo = inventoryData.lotNumber || inventoryData.lotNum || ''
|
|
|
+ productInfo.productionDate = inventoryData.lotAtt01 || inventoryData.productionDate || ''
|
|
|
+ productInfo.expiryDate = inventoryData.lotAtt02 || inventoryData.expiryDate || ''
|
|
|
+ productInfo.moveQty = inventoryData.quantityAvailable || inventoryData.quantity || ''
|
|
|
+
|
|
|
+ showToast('商品信息获取成功')
|
|
|
+ // 切换到扫描目标库位
|
|
|
+ scanType.value = 4
|
|
|
+ focusTargetLocationInput()
|
|
|
+ } else {
|
|
|
+ showToast('未找到库存信息')
|
|
|
+ currentInventoryData.value = null
|
|
|
+ resetProductInfo()
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ closeToast()
|
|
|
+ showToast(error.message || '查询失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 重置所有数据(回到最开始状态)
|
|
|
+const resetAllData = () => {
|
|
|
+ boxCode.value = ''
|
|
|
+ sourceLocation.value = ''
|
|
|
+ scanBarcode.value = ''
|
|
|
+ selectedBox.value = null
|
|
|
+ mergeDataList.value = []
|
|
|
+ clickableLocationsMap.value = new Map()
|
|
|
+ resetProductInfo()
|
|
|
+ initStations()
|
|
|
+ scanType.value = 1
|
|
|
+ focusBoxCodeInput()
|
|
|
+}
|
|
|
+
|
|
|
+// 重置除料箱号外的数据
|
|
|
+const resetExceptBoxCode = () => {
|
|
|
+ sourceLocation.value = ''
|
|
|
+ scanBarcode.value = ''
|
|
|
+ selectedBox.value = null
|
|
|
+ resetProductInfo()
|
|
|
+ scanType.value = 2
|
|
|
+ focusSourceLocationInput()
|
|
|
+}
|
|
|
+
|
|
|
+// 重置商品信息
|
|
|
+const resetProductInfo = () => {
|
|
|
+ scanBarcode.value = ''
|
|
|
+ currentInventoryData.value = null
|
|
|
+ productInfo.targetLocation = ''
|
|
|
+ productInfo.stockQty = ''
|
|
|
+ productInfo.productName = ''
|
|
|
+ productInfo.barcode = ''
|
|
|
+ productInfo.qualityStatus = ''
|
|
|
+ productInfo.warehouseType = ''
|
|
|
+ productInfo.batchNo = ''
|
|
|
+ productInfo.productionDate = ''
|
|
|
+ productInfo.expiryDate = ''
|
|
|
+ productInfo.moveQty = ''
|
|
|
+}
|
|
|
+
|
|
|
+// 加载料箱数据
|
|
|
+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()
|
|
|
+
|
|
|
+ // 加载完成后检查是否需要启动轮询
|
|
|
+ if (hasWaitingBox()) {
|
|
|
+ startPolling()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 加载完成后focus到源库位输入框
|
|
|
+ scanType.value = 2
|
|
|
+ focusSourceLocationInput()
|
|
|
+ } 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 moveQty = detail.moveQty || 0
|
|
|
+
|
|
|
+ // 处理源库位(推荐清空库位)
|
|
|
+ if (sourceLocation) {
|
|
|
+ if (!map.has(sourceLocation)) {
|
|
|
+ map.set(sourceLocation, {
|
|
|
+ recommendType: 'clear',
|
|
|
+ relatedLocations: [],
|
|
|
+ sku: detail.sku || ''
|
|
|
+ })
|
|
|
+ }
|
|
|
+ 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: moveQty })
|
|
|
+ } else {
|
|
|
+ sourceInfo.relatedLocations[existingIdx].quantity += moveQty
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理目标库位(推荐保留库位)
|
|
|
+ if (targetLocation) {
|
|
|
+ if (!map.has(targetLocation)) {
|
|
|
+ map.set(targetLocation, {
|
|
|
+ recommendType: 'keep',
|
|
|
+ relatedLocations: [],
|
|
|
+ sku: detail.sku || ''
|
|
|
+ })
|
|
|
+ }
|
|
|
+ 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: moveQty })
|
|
|
+ } else {
|
|
|
+ targetInfo.relatedLocations[existingIdx].quantity += moveQty
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ clickableLocationsMap.value = map
|
|
|
+}
|
|
|
+
|
|
|
+// 商品信息
|
|
|
+const productInfo = reactive({
|
|
|
+ targetLocation: '',
|
|
|
+ stockQty: '',
|
|
|
+ productName: '',
|
|
|
+ barcode: '',
|
|
|
+ qualityStatus: '',
|
|
|
+ warehouseType: '',
|
|
|
+ batchNo: '',
|
|
|
+ productionDate: '',
|
|
|
+ expiryDate: '',
|
|
|
+ moveQty: '',
|
|
|
+ targetLocationNew: ''
|
|
|
+})
|
|
|
+
|
|
|
+// 移库数量编辑
|
|
|
+const isEditingMoveQty = ref(false)
|
|
|
+const editMoveQty = () => {
|
|
|
+ isEditingMoveQty.value = true
|
|
|
+}
|
|
|
+const confirmMoveQty = () => {
|
|
|
+ isEditingMoveQty.value = false
|
|
|
+}
|
|
|
+
|
|
|
+// 站台数据结构
|
|
|
+interface SubLocation {
|
|
|
+ id: string
|
|
|
+ status: 'empty' | 'filled' | 'selected'
|
|
|
+ locationCode: string // 实际库位编码
|
|
|
+}
|
|
|
+
|
|
|
+interface StationItem {
|
|
|
+ id: number
|
|
|
+ stationCode: string // 站台编码 RLOCHK13A01011
|
|
|
+ displayNumber: string // 显示序号 13-24
|
|
|
+ status: 'offline' | 'waiting' | 'filled' | 'emptyBox' | 'error'
|
|
|
+ label?: string
|
|
|
+ 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 = (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 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) => {
|
|
|
+ switch (splitCount) {
|
|
|
+ case 2:
|
|
|
+ // 2分割:横着分割(上下分割)
|
|
|
+ return { gridTemplateColumns: '1fr', gridTemplateRows: 'repeat(2, 1fr)' }
|
|
|
+ case 4:
|
|
|
+ return { gridTemplateColumns: 'repeat(2, 1fr)', gridTemplateRows: 'repeat(2, 1fr)' }
|
|
|
+ case 6:
|
|
|
+ return { gridTemplateColumns: 'repeat(2, 1fr)', gridTemplateRows: 'repeat(3, 1fr)' }
|
|
|
+ case 8:
|
|
|
+ return { gridTemplateColumns: 'repeat(2, 1fr)', gridTemplateRows: 'repeat(4, 1fr)' }
|
|
|
+ default:
|
|
|
+ return { gridTemplateColumns: 'repeat(2, 1fr)', gridTemplateRows: 'repeat(2, 1fr)' }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 选中的料箱/库位
|
|
|
+const selectedBox = ref<string | number | null>(null)
|
|
|
+
|
|
|
+// 可点击库位信息(来自mergeDetails的sourceLocation和targetLocation)
|
|
|
+interface LocationRecommendInfo {
|
|
|
+ location: string
|
|
|
+ quantity: number
|
|
|
+}
|
|
|
+
|
|
|
+interface ClickableLocationInfo {
|
|
|
+ recommendType: 'clear' | 'keep' // clear=推荐清空库位(源), keep=推荐保留库位(目标)
|
|
|
+ relatedLocations: LocationRecommendInfo[] // 对应的推荐库位列表
|
|
|
+ sku: string
|
|
|
+}
|
|
|
+
|
|
|
+// 可点击库位集合 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',
|
|
|
+ sku: '',
|
|
|
+ relatedLocations: [] as LocationRecommendInfo[]
|
|
|
+})
|
|
|
+
|
|
|
+// 点击站台处理
|
|
|
+const handleStationClick = (station: StationItem) => {
|
|
|
+ // 离线状态不可点击
|
|
|
+ if (station.status === 'offline') return
|
|
|
+ // 异常状态弹出重新下发确认框
|
|
|
+ if (station.status === 'error') {
|
|
|
+ showReissueConfirm(station)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 分割的料箱不处理,由子库位处理
|
|
|
+ if (station.splitCount) return
|
|
|
+ // 其他状态正常选择
|
|
|
+ selectStation(station)
|
|
|
+}
|
|
|
+
|
|
|
+// 显示重新下发确认框
|
|
|
+const showReissueConfirm = (station: StationItem) => {
|
|
|
+ showConfirmDialog({
|
|
|
+ title: '重新下发任务',
|
|
|
+ message: `站台${station.displayNumber}的料箱出现异常,是否重新下发任务?`
|
|
|
+ })
|
|
|
+ .then(() => {
|
|
|
+ doReissueTask(station)
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ // 用户取消
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 获取当前所有料箱编码列表
|
|
|
+const getBoxCodeList = (): string[] => {
|
|
|
+ return stationList.value
|
|
|
+ .filter(s => s.boxCode && s.status !== 'offline')
|
|
|
+ .map(s => s.boxCode!)
|
|
|
+}
|
|
|
+
|
|
|
+// 检查是否存在等待调箱的料箱
|
|
|
+const hasWaitingBox = (): boolean => {
|
|
|
+ return stationList.value.some(s => s.status === 'waiting')
|
|
|
+}
|
|
|
+
|
|
|
+// 局部更新料箱状态
|
|
|
+const refreshBoxStatus = async () => {
|
|
|
+ const boxCodeList = getBoxCodeList()
|
|
|
+ if (boxCodeList.length === 0) return
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await getBoxStatus(warehouse, boxCodeList)
|
|
|
+ const statusMap = res.data || {}
|
|
|
+
|
|
|
+ // 更新站台列表中的状态
|
|
|
+ stationList.value = stationList.value.map(station => {
|
|
|
+ if (!station.boxCode || !statusMap.hasOwnProperty(station.boxCode)) {
|
|
|
+ return station
|
|
|
+ }
|
|
|
+
|
|
|
+ const newBoxStatus = statusMap[station.boxCode]
|
|
|
+ const inventoryLocations = station.inventoryLocations || []
|
|
|
+
|
|
|
+ let status: StationItem['status'] = 'offline'
|
|
|
+ let label: string | undefined = undefined
|
|
|
+
|
|
|
+ if (newBoxStatus === 0 || newBoxStatus === 10) {
|
|
|
+ status = 'waiting'
|
|
|
+ label = '等待调箱'
|
|
|
+ } else if (newBoxStatus === 20) {
|
|
|
+ if (inventoryLocations.length === 0) {
|
|
|
+ status = 'emptyBox'
|
|
|
+ label = '空箱'
|
|
|
+ } else {
|
|
|
+ status = 'filled'
|
|
|
+ }
|
|
|
+ } else if (newBoxStatus === 30) {
|
|
|
+ status = 'offline'
|
|
|
+ } else if (newBoxStatus === 40) {
|
|
|
+ status = 'error'
|
|
|
+ label = '异'
|
|
|
+ }
|
|
|
+
|
|
|
+ return { ...station, status, label }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 检查是否需要继续轮询
|
|
|
+ if (hasWaitingBox()) {
|
|
|
+ startPolling()
|
|
|
+ } else {
|
|
|
+ stopPolling()
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('刷新料箱状态失败', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 启动轮询
|
|
|
+const startPolling = () => {
|
|
|
+ if (pollingTimer) return // 已经在轮询中
|
|
|
+ pollingTimer = setInterval(() => {
|
|
|
+ refreshBoxStatus()
|
|
|
+ }, 10000) // 10秒
|
|
|
+}
|
|
|
+
|
|
|
+// 停止轮询
|
|
|
+const stopPolling = () => {
|
|
|
+ if (pollingTimer) {
|
|
|
+ clearInterval(pollingTimer)
|
|
|
+ pollingTimer = null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 执行重新下发任务
|
|
|
+const doReissueTask = async (station: StationItem) => {
|
|
|
+ if (!station.boxCode) {
|
|
|
+ showToast('料箱编码不存在')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ showLoadingToast({ message: '正在重新下发...', forbidClick: true })
|
|
|
+ await reissueTask(warehouse, station.stationCode, station.boxCode)
|
|
|
+ closeToast()
|
|
|
+ showToast('重新下发成功')
|
|
|
+ // 局部刷新料箱状态
|
|
|
+ setTimeout(() => {
|
|
|
+ refreshBoxStatus()
|
|
|
+ }, 500)
|
|
|
+ } catch (error: any) {
|
|
|
+ closeToast()
|
|
|
+ showToast(error.message || '重新下发失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 选择站台(对于未分割的料箱,库位编码等于料箱编码)
|
|
|
+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.sku = locationInfo?.sku || ''
|
|
|
+ currentLocation.relatedLocations = locationInfo?.relatedLocations || []
|
|
|
+ showLocationPopup.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// 选择子库位
|
|
|
+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.sku = locationInfo?.sku || ''
|
|
|
+ currentLocation.relatedLocations = locationInfo?.relatedLocations || []
|
|
|
+ showLocationPopup.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// 确认选择库位
|
|
|
+const confirmSelectLocation = () => {
|
|
|
+ productInfo.targetLocationNew = currentLocation.id
|
|
|
+ showLocationPopup.value = false
|
|
|
+ showToast(`已选择库位: ${currentLocation.id}`)
|
|
|
+}
|
|
|
+
|
|
|
+// 呼唤机器人
|
|
|
+const callRobot = async () => {
|
|
|
+ try {
|
|
|
+ showLoadingToast({ message: '正在呼唤机器人...', forbidClick: true })
|
|
|
+ await boxInbound(warehouse)
|
|
|
+ closeToast()
|
|
|
+ showToast('呼唤机器人成功')
|
|
|
+ } catch (error: any) {
|
|
|
+ closeToast()
|
|
|
+ showToast(error.message || '呼唤机器人失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 重新输入(不重置料箱号)
|
|
|
+const resetInput = () => {
|
|
|
+ resetExceptBoxCode()
|
|
|
+}
|
|
|
+
|
|
|
+// 提交移库
|
|
|
+const submitMove = () => {
|
|
|
+ if (!boxCode.value) {
|
|
|
+ showToast('请先扫描料箱号')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!sourceLocation.value) {
|
|
|
+ showToast('请先扫描源库位')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!productInfo.barcode) {
|
|
|
+ showToast('请先扫描商品条码')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!productInfo.targetLocationNew) {
|
|
|
+ showToast('请先选择目标库位')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!productInfo.moveQty || Number(productInfo.moveQty) <= 0) {
|
|
|
+ showToast('请输入有效的移库数量')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (Number(productInfo.moveQty) > Number(productInfo.stockQty)) {
|
|
|
+ showToast('移库数量不能大于库存数量')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ showConfirmDialog({
|
|
|
+ title: '移库确认',
|
|
|
+ message: `${productInfo.barcode}从"${sourceLocation.value}"移动至"${productInfo.targetLocationNew}"共:${productInfo.moveQty}件`
|
|
|
+ })
|
|
|
+ .then(() => {
|
|
|
+ const { traceId, lotNum, lotNumber, ownerCode, owner, sku } = currentInventoryData.value || {}
|
|
|
+ const data = {
|
|
|
+ fmLocation: sourceLocation.value,
|
|
|
+ fmContainer: traceId || boxCode.value,
|
|
|
+ owner: ownerCode || owner || '',
|
|
|
+ sku: sku || productInfo.barcode,
|
|
|
+ lotNum: lotNum || lotNumber || productInfo.batchNo || '',
|
|
|
+ warehouse,
|
|
|
+ quantity: Number(productInfo.moveQty),
|
|
|
+ toLocation: productInfo.targetLocationNew
|
|
|
+ }
|
|
|
+ showLoadingToast({ message: '提交中...', forbidClick: true })
|
|
|
+ inventoryMovement(data)
|
|
|
+ .then(() => {
|
|
|
+ closeToast()
|
|
|
+ showToast('提交移库成功')
|
|
|
+ // 重置除料箱号外的数据,继续下一个移库
|
|
|
+ resetExceptBoxCode()
|
|
|
+ })
|
|
|
+ .catch((err: any) => {
|
|
|
+ closeToast()
|
|
|
+ showToast(err.message || '提交移库失败')
|
|
|
+ })
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ // 用户取消
|
|
|
+ })
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.merge-container {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: #f5f5f5;
|
|
|
+ padding: 12px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.scan-section {
|
|
|
+ margin-bottom: 12px;
|
|
|
+
|
|
|
+ :deep(.van-field) {
|
|
|
+ border-radius: 4px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.info-table {
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ margin-bottom: 12px;
|
|
|
+
|
|
|
+ .table-row {
|
|
|
+ display: flex;
|
|
|
+ border-bottom: 1px solid #ddd;
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.row-small .cell {
|
|
|
+ font-size: 11px;
|
|
|
+ padding: 6px 6px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .cell {
|
|
|
+ padding: 8px 6px;
|
|
|
+ font-size: 13px;
|
|
|
+ border-right: 1px solid #ddd;
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ border-right: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.label {
|
|
|
+ background: #f9f9f9;
|
|
|
+ color: #666;
|
|
|
+ flex: 0 0 55px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.label-small {
|
|
|
+ flex: 0 0 40px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.value {
|
|
|
+ color: #333;
|
|
|
+
|
|
|
+ &.input-cell {
|
|
|
+ padding: 0;
|
|
|
+
|
|
|
+ :deep(.van-field) {
|
|
|
+ padding: 4px 6px;
|
|
|
+
|
|
|
+ .van-field__body {
|
|
|
+ height: 24px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .van-field__control {
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.editable {
|
|
|
+ cursor: pointer;
|
|
|
+ min-height: 20px;
|
|
|
+
|
|
|
+ .placeholder {
|
|
|
+ color: #ccc;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.van-field) {
|
|
|
+ padding: 0;
|
|
|
+
|
|
|
+ .van-field__body {
|
|
|
+ height: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .van-field__control {
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.span-2 {
|
|
|
+ flex: 2;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.value-small {
|
|
|
+ flex: 0.9;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.value-large {
|
|
|
+ flex: 1.4;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.input-wide {
|
|
|
+ flex: 1.13;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.grid-section {
|
|
|
+ background: #fff;
|
|
|
+ padding: 12px;
|
|
|
+ margin-bottom: 80px;
|
|
|
+ border-radius: 4px;
|
|
|
+
|
|
|
+ .grid-container {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(6, 1fr);
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .box-wrapper {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ .box-number {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #333;
|
|
|
+ margin-bottom: 2px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .box-item {
|
|
|
+ width: 100%;
|
|
|
+ aspect-ratio: 0.8;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #333;
|
|
|
+ background: #fff;
|
|
|
+ cursor: pointer;
|
|
|
+ border-radius: 4px;
|
|
|
+ position: relative;
|
|
|
+
|
|
|
+ .box-label {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #666;
|
|
|
+ text-align: center;
|
|
|
+ line-height: 1.2;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.box-filled {
|
|
|
+ background: #e8d4f7;
|
|
|
+ border-color: #c9a0dc;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.box-empty-box {
|
|
|
+ background: #d4f7e0;
|
|
|
+ border-color: #a0dcb0;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.box-waiting {
|
|
|
+ background: #f5d4d4;
|
|
|
+ border-color: #e0b0b0;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.box-error {
|
|
|
+ background: #ffcccc;
|
|
|
+ border-color: #ff6666;
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ .box-label {
|
|
|
+ color: #cc0000;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.box-selected {
|
|
|
+ border: 2px solid #1989fa;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.box-split {
|
|
|
+ padding: 3px;
|
|
|
+ cursor: default;
|
|
|
+
|
|
|
+ .sub-grid {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: grid;
|
|
|
+ gap: 2px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sub-location {
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ background: #fff;
|
|
|
+ cursor: pointer;
|
|
|
+ border-radius: 2px;
|
|
|
+
|
|
|
+ &.sub-filled {
|
|
|
+ background: #e8d4f7;
|
|
|
+ border-color: #c9a0dc;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.sub-selected {
|
|
|
+ border: 2px solid #1989fa;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.sub-disabled {
|
|
|
+ background: #f0f0f0;
|
|
|
+ border-color: #ddd;
|
|
|
+ cursor: not-allowed;
|
|
|
+ opacity: 0.6;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.footer-buttons {
|
|
|
+ position: fixed;
|
|
|
+ bottom: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 10px 12px;
|
|
|
+ background: #fff;
|
|
|
+ box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
|
|
|
+
|
|
|
+ .btn-right {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .van-button {
|
|
|
+ height: 32px;
|
|
|
+ font-size: 13px;
|
|
|
+ padding: 0 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-robot {
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #ff9800;
|
|
|
+ color: #ff9800;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-reset {
|
|
|
+ background: #5b9bd5;
|
|
|
+ border-color: #5b9bd5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-submit {
|
|
|
+ background: #ff9800;
|
|
|
+ border-color: #ff9800;
|
|
|
+ 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>
|