| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865 |
- <template>
- <div class="merge-container">
- <van-nav-bar
- 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">
- <van-search
- ref="boxCodeInputRef"
- v-model.lazy="boxCode"
- placeholder="请扫描料箱号"
- left-icon=""
- :class="['scan-input', scanType === 1 ? 'scan-input-active' : '']"
- @search="onBoxCodeEnter"
- @focus="onBoxCodeClick"
- autocomplete="off"
- />
- </div>
- <!-- 信息展示表格 -->
- <div class="info-table">
- <div class="table-row">
- <div class="cell label">源库位</div>
- <div class="cell value input-cell source-location-cell">
- <van-search
- ref="sourceLocationInputRef"
- v-model.lazy="sourceLocation"
- placeholder="请扫描源库位"
- left-icon=""
- class="table-search-input"
- @search="onSourceLocationEnter"
- @focus="onSourceLocationClick"
- autocomplete="off"
- />
- </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-search
- ref="barcodeInputRef"
- v-model.lazy="scanBarcode"
- placeholder="请扫描商品条码"
- left-icon=""
- class="table-search-input"
- @search="onBarcodeEnter"
- @focus="onBarcodeClick"
- autocomplete="off"
- />
- </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">{{ productInfo.lotNumber }}</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 source-location-cell">
- <van-search
- ref="targetLocationInputRef"
- v-model.lazy="productInfo.targetLocationNew"
- placeholder="请扫描目标库位"
- left-icon=""
- class="table-search-input"
- @search="onTargetLocationEnter"
- @focus="onTargetLocationClick"
- autocomplete="off"
- />
- </div>
- <div class="cell label">移库数量</div>
- <div class="cell value editable" @dblclick="editMoveQty">
- <template v-if="isEditingMoveQty">
- <van-field
- v-model="productInfo.actualMoveQty"
- type="number"
- autofocus
- @blur="confirmMoveQty"
- @keyup.enter="confirmMoveQty"
- />
- </template>
- <template v-else>
- <span>{{ productInfo.actualMoveQty }}</span>
- <span class="placeholder">双击编辑</span>
- </template>
- </div>
- </div>
- </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>
- </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="resetAllData">重新输入</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-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>
- </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>
- <!-- 并库任务详情弹框 -->
- <MergeTaskDetailsDialog ref="taskDetailsDialogRef"/>
- <!-- 料箱选择弹框 -->
- <BoxSelectionDialog ref="boxSelectionDialogRef" :box-list="currentBoxList" :warehouse="warehouse" @success="onBoxSelectionSuccess" />
- </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, boxAndStationUnbindTask, reissueTask, getBoxStatus, forceCompleteBoxTask, type BoxRelatedMergeDetailsVO, type LocationMergeDetails } from '@/api/location/merge'
- import { getInventory, inventoryMovement } from '@/api/inventory'
- 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()
- androidFocus()
- } catch (error) {
- }
- 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('')
- // 选中的站点类型:'return'(退货缓存站点)或 'shelf'(上架站点)
- const selectedStationType = ref('return')
- // 轮询定时器
- let pollingTimer: ReturnType<typeof setInterval> | null = null
- // 页面初始化
- onMounted(() => {
- // 检查仓库是否已选择
- if (!warehouse) {
- showToast('请先选择仓库')
- return
- }
- openListener()
- scanInit(_handlerScan)
- // 获取焦点
- nextTick(() => {
- focusBoxCodeInput()
- })
- // 防止键盘弹出时底部按钮上移
- if (window.visualViewport) {
- window.visualViewport.addEventListener('resize', handleViewportResize)
- }
- })
- onUnmounted(() => {
- closeListener()
- stopPolling()
- // 移除viewport监听
- if (window.visualViewport) {
- window.visualViewport.removeEventListener('resize', handleViewportResize)
- }
- })
- // 处理viewport变化(键盘弹出/收起)
- const handleViewportResize = () => {
- // 保持页面滚动位置,防止键盘弹出时页面跳动
- const scrollTop = document.documentElement.scrollTop || document.body.scrollTop
- requestAnimationFrame(() => {
- window.scrollTo(0, scrollTop)
- })
- }
- // 设置料箱号输入框焦点
- 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 = () => {
- scanType.value = 1
- }
- // 料箱号回车
- const onBoxCodeEnter = () => {
- scanType.value = 1
- if (boxCode.value && boxCode.value.length > 5) {
- loadBoxData(boxCode.value)
- }
- }
- // 源库位输入框点击 - 重置除料箱号外的数据
- const onSourceLocationClick = () => {
- scanType.value = 2
- }
- // 源库位回车
- const onSourceLocationEnter = () => {
- if (!sourceLocation.value) return
- // 清空商品信息
- resetProductInfo()
- // 切换到扫描商品条码
- scanType.value = 3
- focusBarcodeInput()
- }
- // 商品条码输入框点击 - 只重置商品信息
- const onBarcodeClick = () => {
- scanType.value = 3
- }
- // 目标库位输入框点击
- const onTargetLocationClick = () => {
- scanType.value = 4
- }
- // 目标库位回车
- const onTargetLocationEnter = () => {
- if (!productInfo.targetLocationNew) return
- showToast(`已输入目标库位: ${productInfo.targetLocationNew}`)
- }
- // 处理站点类型切换
- const handleStationTypeChange = () => {
- // 重新初始化站点(显示不同类型的站点)
- initStations()
- // 重新加载站点数据
- if (boxCode.value) {
- refreshBoxData()
- }
- }
- // 当前选中的库存数据(用于提交移库)
- 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
- }
- 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.quantity
- productInfo.productName = inventoryData.productName
- productInfo.barcode = inventoryData.barcode || inventoryData.barcode2
- productInfo.qualityStatus = inventoryData.lotAtt08
- productInfo.warehouseType = inventoryData.lotAtt05
- productInfo.lotNumber = inventoryData.lotAtt04 || ''
- productInfo.productionDate = inventoryData.lotAtt01
- productInfo.expiryDate = inventoryData.lotAtt02
- // 可移库数量
- const availableQty = inventoryData.quantityAvailable + inventoryData.quantityVirtual
- productInfo.moveQty = availableQty
- // 计算推荐移库数量:取可移库数量和任务推荐数量的最小值
- const taskRecommendQty = getTaskRecommendQty(sourceLocation.value)
- productInfo.recommendMoveQty = taskRecommendQty > 0 ? Math.min(availableQty, taskRecommendQty) : availableQty
- productInfo.actualMoveQty = '' // 用户实际填写数量初始为空
- scanSuccess()
- showToast('商品信息获取成功')
- // 切换到扫描目标库位
- scanType.value = 4
- focusTargetLocationInput()
- } else {
- scanError()
- showToast('未找到库存信息')
- currentInventoryData.value = null
- resetProductInfo()
- }
- } catch (error: any) {
- closeToast()
- scanError()
- showToast(error.message || '查询失败')
- }
- }
- // 重置所有数据(回到最开始状态)
- const resetAllData = () => {
- boxCode.value = ''
- sourceLocation.value = ''
- scanBarcode.value = ''
- selectedBox.value = null
- mergeDataList.value = []
- clickableLocationsMap.value = new Map()
- resetProductInfo()
- productInfo.targetLocationNew = '' // 清空目标库位
- initStations()
- scanType.value = 1
- focusBoxCodeInput()
- }
- // 重置除料箱号外的数据
- const resetExceptBoxCode = () => {
- sourceLocation.value = ''
- scanBarcode.value = ''
- selectedBox.value = null
- resetProductInfo()
- productInfo.targetLocationNew = '' // 清空目标库位
- scanType.value = 2
- focusSourceLocationInput()
- }
- // 重置商品信息
- const resetProductInfo = () => {
- scanBarcode.value = ''
- currentInventoryData.value = null
- productInfo.targetLocation = ''
- productInfo.stockQty = ''
- productInfo.productName = ''
- productInfo.barcode = ''
- productInfo.qualityStatus = ''
- productInfo.warehouseType = ''
- productInfo.lotNumber = ''
- productInfo.productionDate = ''
- productInfo.expiryDate = ''
- productInfo.moveQty = ''
- productInfo.recommendMoveQty = ''
- productInfo.actualMoveQty = ''
- }
- // 加载料箱数据
- const loadBoxData = async (code: string) => {
- if (!code) return
- // 检查是否为站台编码(以RLOCHK开头)
- if (code.startsWith('RLOCHK')) {
- handleStationCodeForceComplete(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()
- scanError()
- 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()
- scanSuccess()
- // 加载完成后检查是否需要启动轮询
- if (hasWaitingBox()) {
- startPolling()
- }
- // 加载完成后focus到源库位输入框
- scanType.value = 2
- focusSourceLocationInput()
- } catch (error: any) {
- closeToast()
- scanError()
- showToast(error.message || '加载失败')
- }
- }
- // 合并数据列表
- const mergeDataList = ref<BoxRelatedMergeDetailsVO[]>([])
- // 构建可点击库位信息Map
- const buildClickableLocationsMap = (boxDetailsList: BoxRelatedMergeDetailsVO[]) => {
- const map = new Map<string, ClickableLocationInfo>()
- // 先对所有移库任务去重,避免同一条任务被处理多次导致数量翻倍
- const uniqueTaskMap = new Map<string, LocationMergeDetails>()
- boxDetailsList.forEach(boxDetail => {
- boxDetail.mergeDetails?.forEach((detail: LocationMergeDetails) => {
- // 使用 sourceLocation + targetLocation + sku 作为唯一键
- const key = `${detail.sourceLocation || ''}_${detail.targetLocation || ''}_${detail.sku || ''}_${detail.lotNum || ''}`
- if (!uniqueTaskMap.has(key)) {
- uniqueTaskMap.set(key, detail)
- }
- })
- })
- // 遍历去重后的任务
- uniqueTaskMap.forEach((detail) => {
- 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 })
- }
- }
- }
- // 处理目标库位(推荐保留库位)
- 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 })
- }
- }
- }
- })
- clickableLocationsMap.value = map
- }
- // 商品信息
- const productInfo = reactive({
- targetLocation: '',
- stockQty: '',
- productName: '',
- barcode: '',
- qualityStatus: '',
- warehouseType: '',
- lotNumber: '', // 批号,取lotAtt04
- productionDate: '',
- expiryDate: '',
- moveQty: '', // 可移库数量(库存可用数量)
- recommendMoveQty: '', // 推荐移库数量(取可移库数量和任务推荐数量的最小值)
- actualMoveQty: '', // 用户实际填写的移库数量
- 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[]>([])
- // 初始化站台列表(根据站点类型显示不同范围的站台)
- const initStations = () => {
- 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: start + i,
- 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.status === 'emptyBox') {
- showUnbindConfirm(station)
- return
- }
- // 分割的料箱不处理,由子库位处理
- if (station.splitCount) return
- // 其他状态正常选择
- selectStation(station)
- }
- // 处理站台编码强制完成任务
- const handleStationCodeForceComplete = async (stationCode: string) => {
- showConfirmDialog({
- title: '强制完成任务',
- message: `检测到站台编码:${stationCode},是否强制完成该站台料箱任务?`
- }).then(async () => {
- try {
- showLoadingToast({ message: '正在强制完成任务...', forbidClick: true })
- await forceCompleteBoxTask(warehouse, stationCode, null)
- closeToast()
- scanSuccess()
- showToast('强制完成任务成功')
- // 重新加载数据
- refreshBoxData()
- } catch (error: any) {
- closeToast()
- scanError()
- showToast(error.message || '强制完成任务失败')
- }
- }).catch(() => {
- // 用户取消
- })
- }
- // 显示重新下发确认框
- const showReissueConfirm = (station: StationItem) => {
- showConfirmDialog({
- title: '重新下发任务',
- message: `料箱${station.boxCode}调用海康异常,是否重新下发任务?`
- })
- .then(() => {
- doReissueTask(station)
- })
- .catch(() => {
- // 用户取消
- })
- }
- // 显示解绑确认框
- const showUnbindConfirm = (station: StationItem) => {
- showConfirmDialog({
- title: '解绑确认',
- message: '您正在进行解绑操作,是否继续?'
- })
- .then(() => {
- doUnbindTask(station)
- })
- .catch(() => {
- // 用户取消
- })
- }
- // 执行解绑任务
- const doUnbindTask = async (station: StationItem) => {
- try {
- showLoadingToast({ message: '正在解绑...', forbidClick: true })
- const data = {
- warehouse,
- boxCode: station.boxCode,
- stationCode: station.stationCode
- }
- await boxAndStationUnbindTask(data)
- closeToast()
- scanSuccess()
- showToast('料箱解绑成功')
- // 局部刷新料箱状态
- setTimeout(() => {
- refreshBoxStatus()
- }, 500)
- } catch (error: any) {
- closeToast()
- scanError()
- showToast(error.message || '解绑失败')
- }
- }
- // 获取当前所有料箱编码列表
- 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
- }
- }
- // 静默刷新盒子数据(调用getWorkingDetailsByBox更新库存信息)
- const refreshBoxData = async () => {
- if (!boxCode.value) return
- try {
- // 调用 getWorkingDetailsByBox 获取最新任务详情
- const res = await getWorkingDetailsByBox(boxCode.value, warehouse)
- const boxDetailsList: BoxRelatedMergeDetailsVO[] = res.data || []
- if (boxDetailsList.length === 0) {
- return
- }
- // 保存返回的数据
- mergeDataList.value = boxDetailsList
- // 遍历 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) {
- // 调用 getBoxSplitCode 获取库位分割信息
- const splitRes = await getBoxSplitCode(warehouse, locations)
- const splitMap: Record<string, number> = splitRes.data || {}
- // 构建可点击库位信息
- buildClickableLocationsMap(boxDetailsList)
- // 更新站台区域
- updateStationList(boxDetailsList, splitMap)
- }
- // 检查是否需要启动轮询
- if (hasWaitingBox()) {
- startPolling()
- } else {
- stopPolling()
- }
- } catch (error) {
- console.error('刷新盒子数据失败', error)
- }
- }
- // 执行重新下发任务
- const doReissueTask = async (station: StationItem) => {
- if (!station.boxCode) {
- scanError()
- showToast('料箱编码不存在')
- return
- }
- try {
- showLoadingToast({ message: '正在重新下发...', forbidClick: true })
- await reissueTask(warehouse, station.stationCode, station.boxCode)
- closeToast()
- scanSuccess()
- showToast('重新下发成功')
- // 局部刷新料箱状态
- setTimeout(() => {
- refreshBoxStatus()
- }, 500)
- } catch (error: any) {
- closeToast()
- scanError()
- 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)) {
- scanError()
- 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
- }
- if (station.status === 'emptyBox') {
- showUnbindConfirm(station)
- 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 = async () => {
- if (currentLocation.recommendType === 'clear') {
- // 推荐清空库位,填入源库位
- sourceLocation.value = currentLocation.id
- showLocationPopup.value = false
- showToast(`已选择源库位: ${currentLocation.id}`)
- // 如果有SKU,自动查询库存信息
- if (currentLocation.sku) {
- scanBarcode.value = currentLocation.sku
- await queryInventoryBySku(currentLocation.sku, currentLocation.id)
- } else {
- // 切换到扫描商品条码
- scanType.value = 3
- focusBarcodeInput()
- }
- } else {
- // 推荐保留库位,填入目标库位
- productInfo.targetLocationNew = currentLocation.id
- showLocationPopup.value = false
- showToast(`已选择目标库位: ${currentLocation.id}`)
- }
- }
- // 并库任务详情弹框
- 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 {
- showLoadingToast({ message: '查询中...', forbidClick: true })
- const params = {
- warehouse,
- barcode: sku,
- location: location
- }
- const res = await getInventory(params)
- closeToast()
- if (res.data && res.data.length > 0) {
- const inventoryData = res.data[0]
- // 保存完整的库存数据
- currentInventoryData.value = inventoryData
- // 填充商品信息
- productInfo.targetLocation = location
- productInfo.stockQty = inventoryData.quantity
- productInfo.productName = inventoryData.productName
- productInfo.barcode = inventoryData.barcode || inventoryData.barcode2
- productInfo.qualityStatus = inventoryData.lotAtt08
- productInfo.warehouseType = inventoryData.lotAtt05
- productInfo.lotNumber = inventoryData.lotAtt04 || ''
- productInfo.productionDate = inventoryData.lotAtt01
- productInfo.expiryDate = inventoryData.lotAtt02
- // 可移库数量
- const availableQty = inventoryData.quantityAvailable + inventoryData.quantityVirtual
- productInfo.moveQty = availableQty
- // 计算推荐移库数量:取可移库数量和任务推荐数量的最小值
- const taskRecommendQty = getTaskRecommendQty(location)
- productInfo.recommendMoveQty = taskRecommendQty > 0 ? Math.min(availableQty, taskRecommendQty) : availableQty
- productInfo.actualMoveQty = '' // 用户实际填写数量初始为空
- scanSuccess()
- showToast('商品信息获取成功')
- // 切换到扫描目标库位
- scanType.value = 4
- focusTargetLocationInput()
- } else {
- scanError()
- showToast('未找到库存信息')
- currentInventoryData.value = null
- resetProductInfo()
- // 切换到扫描商品条码
- scanType.value = 3
- focusBarcodeInput()
- }
- } catch (error: any) {
- closeToast()
- scanError()
- showToast(error.message || '查询失败')
- // 切换到扫描商品条码
- scanType.value = 3
- focusBarcodeInput()
- }
- }
- // 获取任务推荐移库数量(从mergeDetails中获取对应源库位的moveQty)
- const getTaskRecommendQty = (location: string): number => {
- let totalQty = 0
- mergeDataList.value.forEach(boxDetail => {
- boxDetail.mergeDetails?.forEach((detail: LocationMergeDetails) => {
- if (detail.sourceLocation === location && detail.moveQty) {
- totalQty += detail.moveQty
- }
- })
- })
- return totalQty
- }
- // 呼唤机器人
- const callRobot = () => {
- // 获取当前所有料箱列表
- currentBoxList.value = getBoxCodeList()
- if (currentBoxList.value.length === 0) {
- showToast('暂无需要回库的料箱')
- return
- }
- // 显示选择Dialog
- boxSelectionDialogRef.value?.showDialog()
- }
- // 提交移库
- const submitMove = () => {
- if (!boxCode.value) {
- scanError()
- showToast('请先扫描料箱号')
- return
- }
- if (!sourceLocation.value) {
- scanError()
- showToast('请先扫描源库位')
- return
- }
- if (!productInfo.barcode) {
- scanError()
- showToast('请先扫描商品条码')
- return
- }
- if (!productInfo.targetLocationNew) {
- scanError()
- showToast('请先选择目标库位')
- return
- }
- if (!productInfo.actualMoveQty || Number(productInfo.actualMoveQty) <= 0) {
- scanError()
- showToast('请输入有效的移库数量')
- return
- }
- if (Number(productInfo.actualMoveQty) > Number(productInfo.moveQty)) {
- scanError()
- showToast('移库数量不能大于可移库数量')
- return
- }
- showConfirmDialog({
- title: '移库确认',
- message: `${productInfo.barcode}从"${sourceLocation.value}"移动至"${productInfo.targetLocationNew}"共:${productInfo.actualMoveQty}件`
- })
- .then(() => {
- const { traceId, lotNumber, ownerCode, owner, sku } = currentInventoryData.value || {}
- console.log(currentInventoryData.value)
- const data = {
- fmLocation: sourceLocation.value,
- fmContainer: traceId || boxCode.value,
- owner: ownerCode || owner || '',
- sku: sku || productInfo.barcode,
- lotNum: lotNumber,
- warehouse,
- quantity: Number(productInfo.actualMoveQty),
- toLocation: productInfo.targetLocationNew,
- transactionType: 'MERGE_TRANSFER'
- }
- showLoadingToast({ message: '提交中...', forbidClick: true })
- // 保存移库前的数据用于判断是否清空
- const movedQty = Number(productInfo.actualMoveQty)
- const availableQty = Number(productInfo.moveQty)
- inventoryMovement(data)
- .then(() => {
- closeToast()
- scanSuccess()
- showToast('提交移库成功')
- // 如果源库位被清空,更新盒子信息
- if (movedQty >= availableQty) {
- setTimeout(() => {
- refreshBoxData()
- }, 500)
- }
- // 重置除料箱号外的数据,继续下一个移库
- resetExceptBoxCode()
- })
- .catch((err: any) => {
- closeToast()
- scanError()
- 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;
- .scan-input {
- padding: 0;
- background: #fff;
- border-radius: 4px;
- :deep(.van-search__content) {
- background: #fff;
- padding-left: 12px;
- }
- :deep(.van-field__control) {
- font-size: 14px;
- }
- &.scan-input-active {
- :deep(.van-search__content) {
- border-bottom: 2px solid #1989fa;
- }
- }
- }
- }
- .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;
- 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;
- .table-search-input {
- padding: 0;
- background: transparent;
- :deep(.van-search__content) {
- background: transparent;
- padding-left: 6px;
- }
- :deep(.van-field__control) {
- font-size: 13px;
- }
- }
- }
- &.source-location-cell {
- flex: 2;
- }
- &.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: 12px;
- 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%;
- border: 1px solid #ddd;
- font-size: 14px;
- color: #333;
- background: #fff;
- cursor: pointer;
- border-radius: 4px;
- position: relative;
- box-sizing: border-box;
- overflow: hidden;
- /* 使用伪元素实现宽高比,兼容安卓WebView */
- &::before {
- content: '';
- display: block;
- padding-top: 125%; /* 0.8的宽高比 = 1/0.8 = 125% */
- }
- .box-label {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- 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 {
- border-color: #ff6666;
- cursor: pointer;
- .box-label {
- color: #ff6666;
- }
- }
- &.box-selected {
- border: 2px solid #1989fa;
- }
- &.box-split {
- padding: 3px;
- cursor: default;
- .sub-grid {
- position: absolute;
- top: 3px;
- left: 3px;
- right: 3px;
- bottom: 3px;
- 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;
- padding-bottom: calc(10px + env(safe-area-inset-bottom));
- background: #fff;
- box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
- z-index: 100;
- /* 防止键盘弹出时上移 */
- transform: translate3d(0, 0, 0);
- backface-visibility: hidden;
- -webkit-backface-visibility: hidden;
- .btn-right {
- display: flex;
- /* 使用 margin 替代 gap,兼容安卓 WebView */
- .van-button + .van-button {
- margin-left: 12px;
- }
- }
- .van-button {
- height: 32px;
- font-size: 13px;
- padding: 0 14px;
- min-width: 75px;
- }
- .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: 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;
- 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: 6px;
- padding-top: 6px;
- .recommend-title {
- font-size: 14px;
- font-weight: 500;
- color: #333;
- margin-bottom: 8px;
- text-align: left;
- }
- .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>
|