2026-09-17 13:36:08 +08:00
< template >
2026-10-30 15:42:38 +08:00
< view v-if = "user" class="page-shell portal-page transfer-page" :class="{ 'transfer-list-page': entryMode === 'list' }" >
< PortalHeader : title = "entryMode === 'list' ? '仓储转运测量' : '转运测量作业'" :user = "user" />
2026-10-15 18:25:46 +08:00
< view v-if = "entryMode === 'work'" class="transfer-work-toolbar" >
< button class = "back-to-list" @tap ="backToCandidates" >< uni-icons type = "back" color = "#135ee8" size = "18" />< text > 返回列表 </ text ></ button >
2026-10-16 16:19:41 +08:00
< button class = "refresh-button" :disabled = "loading" aria -label =" 刷新当前转运单 " @tap ="refreshCurrentTransfer" >< uni-icons type = "refresh" color = "#135ee8" size = "18" />< text >{{ loading ? '加载中' : '刷新' }}</ text ></ button >
2026-10-15 18:25:46 +08:00
</ view >
2026-10-15 15:45:32 +08:00
< scroll-view scroll -y :scroll-into-view = "cartonScrollTarget" scroll -with-animation class = "transfer-scroll" >< view class = "portal-content transfer-content" >
2026-10-15 18:25:46 +08:00
< view v-if = "entryMode === 'list'" class="transfer-heading" >
2026-10-30 15:42:38 +08:00
< text class = "transfer-list-count" > 待测转运单 · {{ candidates . length }} 单 </ text >
< button class = "refresh-button" :disabled = "loading" aria -label =" 刷新待测转运单 " @tap ="loadCandidates" >< uni-icons type = "refresh" color = "#135ee8" size = "18" />< text >{{ loading ? '刷新中…' : '刷新' }}</ text ></ button >
2026-09-17 13:36:08 +08:00
</ view >
2026-09-24 08:55:38 +08:00
< template v-if = "!selected && entryMode === 'list'" >
2026-10-30 15:42:38 +08:00
< view v-if = "loading && !candidates.length" class="transfer-empty"><uni-load-more status="loading" />< text > 正在加载待测转运单 … </ text ></ view >
2026-09-17 13:36:08 +08:00
< view v-if = "!candidates.length && !loading" class="transfer-empty"><text class="empty-title" > 暂无待测量任务 </ text >< text > 新任务进入后可点击右上角刷新 </ text ></ view >
< view class = "candidate-list" >
2026-10-30 15:42:38 +08:00
< button v-for = "item in candidates" :key="item.transferNo" class="transfer-card" :aria-label="`${item.transferNo}, ${statusLabel(item.status, item.transferNo)}`" hover-class="transfer-card-pressed" @tap="select(item)" >
< view class = "transfer-card-head" >< text class = "transfer-no" >{{ item . transferNo }}</ text >< text class = "status-pill" : class = "{ 'status-measuring': item.status === 'PARTIALLY_RECEIVED' }" >{{ statusLabel ( item . status , item . transferNo ) }}</ text ></ view >
< view class = "task-metrics" >< view >< strong >{{ item . packageCount || 0 }}</ strong >< text > 计划箱数 </ text ></ view >< view >< strong >{{ destinationLabel ( item ) }}</ strong >< text >{{ item . destinationType === 'PRIVATE' ? '目的仓' : 'FBA 目的仓' }}</ text ></ view >< view >< strong >{{ item . needRelabel ? '需要' : '不需要' }}</ strong >< text > 换标 </ text ></ view ></ view >
< view class = "transfer-card-footer" >< text v-if = "listInboundCode(item)" class="transfer-meta">入仓码 {{ listInboundCode(item) }}</text><view class="transfer-card-action"><text>{{ item.status === 'PARTIALLY_RECEIVED' ? '继续测量' : '测量' }}</text><uni-icons type="right" color="#135ee8" size="14" /></ view ></ view >
2026-09-17 13:36:08 +08:00
</ button >
</ view >
2026-09-24 08:55:38 +08:00
</ template >
2026-10-30 15:42:38 +08:00
< view v-if = "!selected && entryMode === 'work' && !detailLoading" class="transfer-empty"><text class="empty-title" > 暂无进行中的转运测量作业 </ text >< text > 请先从 “ 仓储转运测量 ” 选择待测转运单 。 </ text ></ view >
2026-09-17 13:36:08 +08:00
< view v-if = "detailLoading" class="transfer-empty"><text class="empty-title" > 正在读取转运明细 </ text >< text > 请稍候 , 不要重复选择 </ text ></ view >
< template v-if = "selected && !detailLoading" >
2026-09-24 12:06:44 +08:00
< view v-if = "isMeasurementComplete" class="transfer-success"><text class="feedback-title" > 测量已完成 </ text >< text > 该转运单已完成 PDA 测量 , 不再重复进入测量或托盘作业 。 </ text ></ view >
< view v-else-if = "isInventoryTransfer" class="transfer-success inventory-transfer-panel"><text class="feedback-title">库存转运 · 无需测量</text><text>该单来源于仓库现有库存,不需要重复测量或上传现场照片。请进入托盘作业,按来源托盘执行整托转运、拆托重打,或与相同目的仓合托。</text><button class="transfer-primary" @tap="openPalletOperations" > 进入托盘作业 </ button ></ view >
< template v-if = "!isInventoryTransfer && !isMeasurementComplete" >
2026-10-16 16:19:41 +08:00
< view class = "order-summary" @tap ="summaryCollapsed = !summaryCollapsed" role = "button" :aria-expanded = "!summaryCollapsed" aria -label =" 展开或收起任务汇总 " >
2026-09-17 13:36:08 +08:00
< view >< text class = "summary-eyebrow" > 当前测量任务 </ text >< text class = "summary-no" >{{ selected . transferNo }}</ text ></ view >
2026-10-15 15:45:32 +08:00
< view class = "summary-count" >< strong >{{ cartonsCounted ? actualReceivedCartons : receivedCartonBoxes . length }}</ strong >< text >{{ cartonsCounted ? '实收箱' : '待确认箱' }}</ text ></ view >
2026-10-16 16:19:41 +08:00
< text class = "summary-toggle" >{{ summaryCollapsed ? '展开 ⌄' : '收起 ⌃' }}</ text >
2026-09-17 13:36:08 +08:00
</ view >
2026-10-16 16:19:41 +08:00
< view v-show = "!summaryCollapsed" class="receipt-comparison"><view><strong>{{ forecastSkuQuantity }}</strong><text>客户预报(件)</text></view><view><strong>{{ cartonsCounted ? cartonQuantity : '—' }}</strong><text>SKU实收(件)</text></view><view :class="{ difference: cartonsCounted && hasSkuReceiptDifference }" >< strong >{{ ! cartonsCounted ? '—' : hasSkuReceiptDifference ? skuReceiptDifferences . length + ' 种' : '无差异' }}</ strong >< text >{{ ! cartonsCounted ? '差异待确认' : 'SKU签收差异' }}</ text ></ view ></ view >
< view v-show = "!summaryCollapsed" class="summary-metrics" >< view >< strong >{{ pallets . length }}</ strong >< text > 托盘 </ text ></ view >< view >< strong >{{ totalVolume }}</ strong >< text > 箱子总体积 m³ </ text ></ view >< view >< strong >{{ totalWeight }}</ strong >< text > 箱子总重量 kg </ text ></ view ></ view >
2026-09-17 13:36:08 +08:00
2026-10-15 15:45:32 +08:00
< view v-for = "(pallet, index) in pallets" :key="pallet.key" class="pallet-card" :class="{ 'active-receiving-pallet': String(receivingPalletId) === String(pallet.draftId) }" >
2026-09-17 13:36:08 +08:00
< view class = "pallet-head" >
< view class = "pallet-index" >< text >{{ String ( index + 1 ). padStart ( 2 , '0' ) }}</ text ></ view >
2026-10-16 16:19:41 +08:00
< view class = "pallet-heading-copy" >< view class = "pallet-title-line" >< text class = "pallet-name" > 托盘 {{ index + 1 }}</ text >< text class = "pallet-status" : class = "{ 'is-bound': pallet.bound }" >{{ pallet . bound ? '已绑定' : pallet . generated ? '待绑定' : '待创建' }}</ text ></ view ></ view >
< button class = "remove-button" aria -label =" 删除托盘 " : disabled = "countingCartons || submitting || pallets.some(item => item.busy)" @tap.stop ="removePallet(index)" > × </ button >
2026-09-17 13:36:08 +08:00
< button v-if = "pallet.generated" class="collapse-button" :aria-label="pallet.collapsed ? '展开托盘明细' : '折叠托盘明细'" @tap.stop="pallet.collapsed = !pallet.collapsed" >{{ pallet.collapsed ? '⌄' : '⌃' }}</ button >
</ view >
2026-10-16 16:19:41 +08:00
< view v-if = "pallet.generated" class="pallet-stats" >< view >< strong >{{ palletSummary ( pallet ). cartonCount }}< text > 箱 </ text ></ strong >< text > 实收箱数 </ text ></ view >< view >< strong >{{ palletVolume ( pallet ) }}< text > m³ </ text ></ strong >< text > 体积 </ text ></ view >< view >< strong >{{ palletSummary ( pallet ). weight . toFixed ( 2 ) }}< text > kg </ text ></ strong >< text > 重量 </ text ></ view ></ view >
< view v-if = "pallet.generated" class="pallet-brief"><text class="pallet-business-code" selectable>{{ pallet.palletNo }}</text><text v-if="String(receivingPalletId) === String(pallet.draftId)" class="receiving-badge">当前签收</text><button v-else :disabled="cartonsCounted || countingCartons" @tap="useReceivingPallet(pallet)" > 设为签收托盘 </ button ></ view >
2026-09-17 13:36:08 +08:00
< view v-show = "!pallet.collapsed" >
2026-10-20 14:52:33 +08:00
< view class = "measure-section-head" >< view >< text class = "measure-title" > 本托箱子汇总 </ text ></ view ></ view >
2026-10-15 15:45:32 +08:00
< view class = "summary-metrics" >< view >< strong >{{ palletSummary ( pallet ). cartonCount }}</ strong >< text > 实收箱数 </ text ></ view >< view >< strong >{{ palletVolume ( pallet ) }}</ strong >< text > 箱子体积 m³ </ text ></ view >< view >< strong >{{ palletSummary ( pallet ). weight . toFixed ( 2 ) }}</ strong >< text > 箱子重量 kg </ text ></ view ></ view >
< button v-if = "!pallet.generated" class="generate-button" :disabled="pallet.busy || cartonsCounted || countingCartons" @tap="generatePallet(pallet)" >{{ pallet.busy ? ' 生成中 …' : ' 生成托盘号 ' }}</ button >
2026-10-29 13:37:35 +08:00
< view v-else class = "generated-card" >< view >< text class = "print-code" >{{ pallet . palletNo }}</ text >< text >{{ destinationLabel ( pallet , selected ) }}</ text ></ view >< button :disabled = "pallet.busy" @tap ="printDraftPallet(pallet)" > 打印托盘 </ button ></ view >
2026-09-17 13:36:08 +08:00
< view v-if = "pallet.generated && !pallet.bound" class="bind-panel"><ScanTargetInput :name="`transfer-pallet-${index}`" v-model="pallet.bindCode" label="确认实物托盘码" placeholder="扫描或手输新托盘标签号" :active="activePalletKey === pallet.key" @activate="activePalletKey = pallet.key" @submit="bindPallet(pallet)" /><button :disabled="pallet.busy || !String(pallet.bindCode || '').trim()" @tap="bindPallet(pallet)" >{{ pallet.busy ? ' 确认中 …' : ' 确认绑定 ' }}</ button ></ view >
< view v-if = "pallet.bound" class="bound-state" > 已绑定实物托盘 {{ pallet.palletNo }}</ view >
</ view >
</ view >
2026-10-15 15:45:32 +08:00
< button class = "add-pallet-button" : disabled = "cartonsCounted || countingCartons" @tap ="addPallet" >< uni-icons type = "plus" color = "#135ee8" size = "18" />< text > 新增一个托盘 </ text ></ button >
2026-10-15 18:25:46 +08:00
< text v-if = "!generatedPallets.length" class="carton-prerequisite" > 请先生成托盘号 , 再填写签收箱子 。 </ text >
2026-10-15 15:45:32 +08:00
< view v-if = "generatedPallets.length" class="photo-card" >
2026-10-16 16:19:41 +08:00
< view class = "photo-head" >< text id = "transfer-boxes" class = "measure-title" > 签收箱子 </ text >< button v-if = "cartonsCounted" class="remeasure-button" :disabled="countingCartons || submitting || pallets.some(item => item.busy)" @tap="remeasure" > 重新测量 </ button ></ view >
2026-10-15 15:45:32 +08:00
< view class = "carton-filter" >< button : class = "{active: !cartonPalletFilter}" @tap ="cartonPalletFilter = ''" > 全部箱子 </ button >< button v-for = "(pallet, index) in generatedPallets" :key="pallet.draftId" :class="{active: String(cartonPalletFilter) === String(pallet.draftId)}" @tap="cartonPalletFilter = pallet.draftId" > 托盘 {{ index + 1 }} · {{ palletSummary ( pallet ) .cartonCount }} 箱 </ button ></ view >
< view v-if = "!cartonsCounted" class="batch-count-panel" >
< button class = "add-pallet-button" :disabled = "countingCartons" @tap ="toggleBatchPanel" >{{ batchCountOpen ? ' 收起批量填写 ' : ' 相同箱规 ? 批量填写 ' }}</ button >
< view v-if = "batchCountOpen" >
< text class = "measure-hint" > 选择同 SKU 的箱子 , 统一填写 。 应用后可逐箱修改 ; 会覆盖所选箱的尺寸 、 重量和件数 。 </ text >
< picker :disabled = "countingCartons" :range = "batchSkuCodes" : value = "Math.max(0, batchSkuCodes.indexOf(batchSku))" @change ="changeBatchSku(Number($event.detail.value))" >< view class = "batch-sku" > 批量 SKU : {{ batchSku || '请选择' }} ▾ </ view ></ picker >
< view class = "batch-actions" >< button :disabled = "countingCartons" @tap ="selectBatchCartons(true)" > 全选该 SKU </ button >< button :disabled = "countingCartons" @tap ="selectBatchCartons(false)" > 清空选择 </ button ></ view >
2026-10-15 18:25:46 +08:00
< view class = "batch-box-options" >< template v-for = "(box, index) in cartonBoxes" :key="box.localKey || box.cartonMark"><button v-if="box.received !== false && box.items.length === 1 && box.items[0].skuCode === batchSku" :disabled="countingCartons" :class="{chosen: batchSelectedIndexes.includes(index)}" :aria-label="`选择第${index + 1}箱批量填写`" @tap="toggleBatchCarton(index)" >{{ batchSelectedIndexes.includes ( index ) ? '✓' : '+' }} 第 {{ index + 1 }} 箱 </ button ></ template ></ view >
2026-10-15 15:45:32 +08:00
< picker :disabled = "countingCartons" : range = "generatedPallets.map(pallet => pallet.palletNo)" @change ="batchPalletId = generatedPallets[Number($event.detail.value)]?.draftId || ''" >< view class = "batch-sku" > 批量所属托盘 : {{ generatedPallets . find ( pallet => String ( pallet . draftId ) === String ( batchPalletId )) ? . palletNo || '请选择' }} ▾ </ view ></ picker >
2026-10-16 16:19:41 +08:00
< CartonCountFields :box = "batchBox" volume -unit =" m3 " :sku-codes = "[batchSku]" :allow-sku-change = "false" :disabled = "countingCartons" />
2026-10-15 15:45:32 +08:00
< text class = "batch-summary" > 已选 {{ batchTargetBoxes . length }} 箱 · 每箱 {{ Number ( batchBox . items [ 0 ]. quantity ) || 0 }} 件 · 合计 {{ batchTotalQuantity }} 件 </ text >
< button class = "transfer-primary" : disabled = "!batchCountReady || countingCartons" @tap ="applyBatchCartons" > 应用到所选 {{ batchTargetBoxes.length }} 箱 </ button >
< text v-if = "batchCountMessage" class="batch-summary" role="status" >{{ batchCountMessage }}</ text >
</ view >
</ view >
2026-10-16 16:19:41 +08:00
< view v-for = "(box, index) in cartonBoxes" v-show="!cartonPalletFilter || String(box.transferPalletId) === String(cartonPalletFilter)" :id="`transfer-box-${index}`" :key="box.localKey || box.cartonMark" class="carton-count-card" :class="{ 'carton-collapsed': cartonConfirmed(box) }" >
< view v-if = "cartonConfirmed(box)" class="collapsed-carton-row"><scroll-view class="collapsed-carton-mark" scroll-x><text @tap="editCarton(box)">{{ box.cartonMark || '新增实收箱' }}</text></scroll-view><button class="collapsed-carton-expand" aria-label="展开修改箱子" :disabled="countingCartons || submitting" @tap="editCarton(box)"><uni-icons type="down" color="#7185a0" size="16" /></ button ></ view >
< template v-else >
< view class = "carton-title-row" >< text class = "measure-title" > 第 {{ index + 1 }} 箱 </ text >< view class = "carton-icon-actions" >< button v-if = "!cartonsCounted" class="delete-carton-button" aria-label="删除箱子" :disabled="countingCartons" @tap="removeCarton(index)" > × </ button ></ view ></ view >
< text class = "carton-business-code" selectable >{{ box . cartonMark || '新增实收箱' }}</ text >
2026-10-15 15:45:32 +08:00
< view v-if = "!cartonsCounted" class="batch-actions" >
< button v-if = "previousMatchingCarton(index)" :disabled="countingCartons || box.received === false || !validCartonValues(previousMatchingCarton(index))" @tap="copyPreviousCarton(index)" > 复制上一同 SKU 箱 </ button >
</ view >
< picker : disabled = "cartonsCounted || countingCartons || box.received === false" : range = "generatedPallets.map(pallet => pallet.palletNo)" : value = "Math.max(0, generatedPallets.findIndex(pallet => String(pallet.draftId) === String(box.transferPalletId)))" @change ="assignCartonPallet(box, Number($event.detail.value))" >< view class = "batch-sku" > 所属托盘 : {{ cartonPalletLabel ( box ) }} ▾ </ view ></ picker >
2026-10-16 16:53:09 +08:00
< CartonCountFields :box = "box" :sku-codes = "declaredSkuCodes" :unique-skus = "true" volume -unit =" m3 " : disabled = "cartonsCounted || countingCartons || box.received === false" >< template # sku -action >< button v-if = "!cartonsCounted && declaredSkuCodes.length > 1" class="add-sku-button" aria-label="添加 SKU" :disabled="countingCartons || box.received === false || box.items.length >= Math.min(200, declaredSkuCodes.length)" @tap="addCartonSku(box)" > + </ button ></ template ></ CartonCountFields >
2026-10-16 16:19:41 +08:00
< button class = "confirm-carton-button" : disabled = "countingCartons || submitting || !validCartonValues(box) || !generatedPallets.some(pallet => String(pallet.draftId) === String(box.transferPalletId))" @tap ="confirmSingleCarton(box)" > 确认本箱 </ button >
</ template >
2026-10-15 15:45:32 +08:00
</ view >
2026-10-29 13:37:35 +08:00
< button v-if = "cartonsCounted" class="add-pallet-button" @tap="printCartonLabels" > 打印唛头 </ button >
2026-10-16 17:43:48 +08:00
< button v-if = "restorableCartons.length" class="add-pallet-button" :disabled="countingCartons || submitting || deletingCarton" @tap="restoreDeletedCarton" > 恢复已删除箱子 ( {{ restorableCartons.length }} ) </ button >
2026-10-16 16:19:41 +08:00
< button class = "transfer-primary" : disabled = "!allCartonsConfirmed || !cartonCountReady || countingCartons || cartonsCounted" @tap ="confirmCartons" >{{ cartonsCounted ? ' 全部箱子已清点 ' : countingCartons ? ' 保存中 …' : ' 保存全部清点结果 ' }}</ button >
2026-10-15 15:45:32 +08:00
</ view >
2026-09-17 13:36:08 +08:00
< view class = "photo-card" >
< view class = "photo-head" >< view >< text class = "measure-title" > 测量现场照片 </ text >< text class = "measure-hint" > 完成测量并打托前至少拍摄 1 张 </ text ></ view >< text class = "photo-count" >{{ photoFileIds . length }} / 20 张 </ text ></ view >
< view v-if = "photoFileIds.length" class="photo-grid"><view v-for="(fileId, index) in photoFileIds" :key="fileId" class="photo-tile" >
< image class = "photo-image" :src = "photoUrls[fileId]" mode = "aspectFill" @tap ="previewPhoto(fileId)" />
< button class = "photo-remove" : disabled = "uploadingPhoto || submitting" @tap ="removePhoto(index)" > 移除 </ button >
</ view ></ view >
< button v-if = "photoFileIds.length < 20" class="take-photo-button" :disabled="uploadingPhoto || submitting" @tap="takePhoto"><uni-icons type="camera-filled" color="#135ee8" size="20" />< text >{{ uploadingPhoto ? '照片上传中…' : '拍摄测量现场照片' }}</ text ></ button >
</ view >
< view v-if = "validationMessage" class="transfer-validation" role="alert"><text class="validation-title" > 还不能提交 </ text >< text >{{ validationMessage }}</ text ></ view >
2026-10-15 15:45:32 +08:00
< view class = "submit-panel" >< view >< text > 整单测量汇总 </ text >< strong >{{ pallets . length }} 托 · 打托 {{ totalCartons }} / 已清点 {{ actualReceivedCartons }} 箱 </ strong ></ view >< button class = "transfer-primary" : disabled = "submitting || !canSubmit" @tap ="submit" >{{ submitting ? ' 提交中 …' : ' 完成测量并打托 ' }}</ button ></ view >
2026-09-23 09:06:31 +08:00
</ template >
2026-09-17 13:36:08 +08:00
</ template >
2026-11-04 19:27:34 +08:00
< view v-if = "error" class="transfer-error" role="alert"><text class="feedback-title" > 操作失败 </ text >< text >{{ simpleErrorMessage ( error ) }}</ text ></ view >
2026-09-17 13:36:08 +08:00
< view v-if = "success" class="transfer-success" role="status"><text class="feedback-title" > 提交成功 </ text >< text > 测量与托盘打托已完成 </ text ></ view >
< view v-if = "completedPallets.length" class="print-card" >
2026-09-22 17:27:56 +08:00
< view class = "photo-head" >< view >< text class = "measure-title" > 新托盘标签 </ text >< text class = "measure-hint" >{{ completedDestinationLabel }} · 逐托打开 PDF 后打印并粘贴 </ text ></ view ></ view >
2026-10-29 13:37:35 +08:00
< view v-for = "pallet in completedPallets" :key="pallet.palletNo" class="print-row"><view><text class="print-code">{{ pallet.palletNo }}</text><text>{{ pallet.cartonCount }} 箱 · {{ destinationLabel(pallet, { destinationType: completedDestinationType }) }}</text></view><button @tap="printPallet(pallet)" > 打印托盘 </ button ></ view >
2026-09-17 13:36:08 +08:00
</ view >
</ view > </ scroll -view >
</ view >
</ template >
< script >
2026-10-15 15:45:32 +08:00
import CartonCountFields from '@/components/CartonCountFields.vue'
2026-09-17 13:36:08 +08:00
import PortalHeader from '@/components/PortalHeader.vue'
import ScanTargetInput from '@/components/ScanTargetInput.vue'
import { session , requireSession } from '@/stores/session.js'
2026-09-24 08:55:38 +08:00
import { restoreWarehouseWork , resetWarehouseDomain , updateWarehouseWork , warehouseWork } from '@/stores/warehouse-work.js'
2026-09-17 13:36:08 +08:00
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
2026-10-16 16:19:41 +08:00
import { getWarehouseTransferCartonLabel , countWarehouseTransferCartons , reopenWarehouseTransferMeasurement , bindWarehouseTransferPallet , deleteWarehouseTransferPallet , downloadWarehouseTransferMeasurementPhoto , downloadWarehouseTransferPalletLabel , generateWarehouseTransferPallet , getWarehouseTransferDetail , listWarehouseTransferCandidates , listWarehouseTransferMeasurementPhotos , palletizeWarehouseTransfer , saveWarehouseTransferPalletDraft , uploadWarehouseTransferMeasurementPhoto } from '@/api/warehouse-transfers.js'
2026-09-17 13:36:08 +08:00
const newPallet = () => ({ key : ` ${ Date . now () } - ${ Math . random () } ` , cartonCount : 0 , lengthCm : null , widthCm : null , heightCm : null , grossWeightKg : null , generated : false , bound : false , collapsed : false , palletNo : '' , fbaWarehouseCode : '' , labelDownloadPath : '' , bindCode : '' , busy : false })
2026-09-28 17:26:50 +08:00
const MAX_MEASUREMENT_PHOTOS = 20
2026-10-15 15:45:32 +08:00
const newBatchBox = () => ({ length : '' , width : '' , height : '' , weight : '' , items : [{ skuCode : '' , quantity : '' }] })
2026-09-17 13:36:08 +08:00
const restoredPallet = ( value , index ) => ({
key : `server- ${ value . draftId || value . id || index } ` ,
cartonCount : Number ( value . cartonCount || 0 ), lengthCm : value . lengthCm , widthCm : value . widthCm , heightCm : value . heightCm , grossWeightKg : value . grossWeightKg ,
2026-09-24 14:43:09 +08:00
generated : Boolean ( value . palletNo ), bound : Boolean ( value . boundAt || value . status === 'BOUND' ), collapsed : Boolean ( value . boundAt || value . status === 'BOUND' ),
palletNo : String ( value . palletNo || '' ), fbaWarehouseCode : String ( value . fbaWarehouseCode || '' ), labelDownloadPath : String ( value . labelDownloadPath || '' ), bindCode : String ( value . palletNo || '' ), busy : false ,
2026-09-17 13:36:08 +08:00
draftId : value . draftId || value . id , version : value . version
})
export default {
2026-10-15 15:45:32 +08:00
components : { PortalHeader , ScanTargetInput , CartonCountFields },
2026-10-16 17:43:48 +08:00
data () { return { deletedCartonBoxes : [], deletingCarton : false , summaryCollapsed : false , receivingPalletId : '' , cartonPalletFilter : '' , batchPalletId : '' , batchCountOpen : false , batchSku : '' , batchBox : newBatchBox (), batchSelectedIndexes : [], batchCountMessage : '' , cartonScrollTarget : '' , cartonBoxes : [], countDiscrepancyReason : '' , countingCartons : false , cartonRequestKey : '' , cartonSignature : '' , user : session . currentUser , entryMode : 'work' , pendingTaskNo : '' , candidates : [], selected : null , pallets : [ newPallet ()], activePalletKey : '' , photoFileIds : [], photoNames : [], photoUrls : {}, completedPallets : [], completedTransferNo : '' , completedDestination : '' , completedDestinationType : '' , loading : false , detailLoading : false , submitting : false , uploadingPhoto : false , openingLabel : false , error : null , success : false , pendingKey : '' } },
2026-09-17 13:36:08 +08:00
computed : {
2026-10-16 17:43:48 +08:00
restorableCartons () {
const current = new Set ( this . cartonBoxes . map ( box => box . cartonMark || box . localKey ));
const removed = new Map ();
for ( const box of [...( this . selected ? . cartons || []), ... this . deletedCartonBoxes ]) {
const key = box . cartonMark || box . localKey ;
if ( key && ! current . has ( key )) removed . set ( key , box );
}
return [... removed . values ()];
},
2026-10-15 15:45:32 +08:00
generatedPallets () { return this . pallets . filter ( pallet => pallet . generated && pallet . draftId ) },
2026-10-15 18:25:46 +08:00
declaredSkuCodes () { return [... new Set (( this . selected ? . items || []). map ( item => item . skuCode ))] },
2026-10-15 15:45:32 +08:00
batchSkuCodes () { return [... new Set ( this . receivedCartonBoxes . map ( box => box . items [ 0 ] ? . skuCode ). filter ( Boolean ))] },
2026-10-15 18:25:46 +08:00
batchTargetBoxes () { return this . cartonBoxes . filter (( box , index ) => this . batchSelectedIndexes . includes ( index ) && box . received !== false && box . items . length === 1 && box . items [ 0 ] ? . skuCode === this . batchSku ) },
2026-10-15 15:45:32 +08:00
batchTotalQuantity () { const quantity = Number ( this . batchBox . items [ 0 ]. quantity ); return Number . isSafeInteger ( quantity ) && quantity > 0 ? this . batchTargetBoxes . length * quantity : 0 },
batchCountReady () { return this . batchTargetBoxes . length > 0 && this . validCartonValues ( this . batchBox ) && this . generatedPallets . some ( pallet => String ( pallet . draftId ) === String ( this . batchPalletId )) },
2026-10-16 16:19:41 +08:00
// 确认快照与当前内容一致,才允许保存整单清点结果。
// 按 SKU 分别核对件数,不能让不同 SKU 的短收与超收相互抵消。
forecastSkuQuantity () { return ( this . selected ? . items || []). reduce (( sum , item ) => sum + Number ( item . quantity || 0 ), 0 ) },
skuReceiptDifferences () {
const expected = new Map (), received = new Map ()
for ( const item of this . selected ? . items || []) expected . set ( item . skuCode , ( expected . get ( item . skuCode ) || 0 ) + Number ( item . quantity || 0 ))
for ( const box of this . receivedCartonBoxes ) for ( const line of box . items ) received . set ( line . skuCode , ( received . get ( line . skuCode ) || 0 ) + Number ( line . quantity || 0 ))
return [... new Set ([... expected . keys (), ... received . keys ()])]. filter ( sku => ( received . get ( sku ) || 0 ) !== ( expected . get ( sku ) || 0 ))
},
hasSkuReceiptDifference () { return this . skuReceiptDifferences . length > 0 },
allCartonsConfirmed () { return this . receivedCartonBoxes . length > 0 && this . receivedCartonBoxes . every ( box => this . cartonConfirmed ( box )) },
2026-10-15 15:45:32 +08:00
actualReceivedCartons () { return this . cartonsCounted ? this . receivedCartonBoxes . length : 0 },
cartonsCounted () { return Boolean ( this . selected ? . cartons ? . length ) && this . selected . cartons . every ( box => box . countedAt ) },
2026-10-16 16:19:41 +08:00
cartonVolume () { return this . receivedCartonBoxes . reduce (( sum , box ) => sum + ( Number ( box . length ) || 0 ) * ( Number ( box . width ) || 0 ) * ( Number ( box . height ) || 0 ) / 1000000 , 0 ). toFixed ( 3 ) },
2026-10-15 15:45:32 +08:00
cartonWeight () { return this . receivedCartonBoxes . reduce (( sum , box ) => sum + ( Number ( box . weight ) || 0 ), 0 ). toFixed ( 2 ) },
2026-10-15 18:25:46 +08:00
cartonQuantity () { return this . receivedCartonBoxes . reduce (( sum , box ) => sum + box . items . reduce (( total , line ) => total + ( Number ( line . quantity ) || 0 ), 0 ), 0 ) },
2026-10-15 15:45:32 +08:00
receivedCartonBoxes () { return this . cartonBoxes . filter ( box => box . received !== false ) },
2026-10-16 16:19:41 +08:00
cartonCountReady () { return this . receivedCartonBoxes . length > 0 && this . pallets . every ( pallet => pallet . generated && this . palletSummary ( pallet ). cartonCount > 0 ) && this . receivedCartonBoxes . every ( box => this . generatedPallets . some ( pallet => String ( pallet . draftId ) === String ( box . transferPalletId )) && [ box . length , box . width , box . height , box . weight ]. every ( value => Number ( value ) >= 0.01 ) && this . validCartonSkuLines ( box )) },
2026-09-22 17:27:56 +08:00
completedDestinationLabel () { return this . destinationLabel ({ fbaWarehouseCode : this . completedDestination , destinationType : this . completedDestinationType }) },
2026-10-15 15:45:32 +08:00
totalCartons () { return this . pallets . reduce (( sum , pallet ) => sum + this . palletSummary ( pallet ). cartonCount , 0 ) },
cartonDifference () { return this . actualReceivedCartons - Number ( this . selected ? . packageCount || 0 ) },
2026-09-24 14:20:26 +08:00
isInventoryTransfer () { return Boolean ( this . selected && this . selected . status === 'WAIT_PDA_RECEIVING' && this . selected . inboundCode && this . selected . inboundCode !== this . selected . transferNo ) },
isMeasurementComplete () { return Boolean ( this . selected && ! [ 'WAIT_PDA_RECEIVING' , 'PARTIALLY_RECEIVED' ]. includes ( String ( this . selected . status ))) },
2026-10-16 16:19:41 +08:00
totalVolume () { return this . pallets . reduce (( sum , pallet ) => sum + this . rawPalletVolume ( pallet ), 0 ). toFixed ( 3 ) },
2026-10-15 15:45:32 +08:00
totalWeight () { return this . pallets . reduce (( sum , pallet ) => sum + this . palletSummary ( pallet ). weight , 0 ). toFixed ( 2 ) },
2026-09-17 13:36:08 +08:00
validationMessage () {
2026-09-23 09:06:31 +08:00
if ( this . isInventoryTransfer ) return ''
2026-10-15 15:45:32 +08:00
if ( ! this . cartonsCounted ) return '请先完成全部箱子的尺寸测量和箱内清点'
if ( this . totalCartons !== this . receivedCartonBoxes . length ) return '打托箱数必须与已清点实收箱数一致'
2026-09-17 13:36:08 +08:00
if ( ! this . selected || ! this . pallets . length ) return '转运单缺少可测量的托盘'
2026-10-15 15:45:32 +08:00
if ( this . pallets . some ( pallet => this . palletSummary ( pallet ). cartonCount === 0 )) return '每个托盘必须至少分配一个实收箱'
2026-09-17 13:36:08 +08:00
if ( this . pallets . some (( pallet ) => ! pallet . bound )) return '请先生成并扫描确认绑定全部托盘'
2026-09-28 17:26:50 +08:00
if ( this . photoFileIds . length > MAX_MEASUREMENT_PHOTOS ) return `测量现场照片最多 ${ MAX_MEASUREMENT_PHOTOS } 张,请移除多余照片`
2026-09-17 13:36:08 +08:00
if ( ! this . photoFileIds . length ) return '请至少拍摄 1 张测量现场照片'
return ''
},
canSubmit () { return ! this . validationMessage }
},
watch : {
2026-10-15 15:45:32 +08:00
countDiscrepancyReason () { this . saveCartonDraft () },
2026-10-22 23:06:06 +08:00
cartonBoxes : { deep : true , handler () { if ( this . selected && ! this . cartonsCounted ) uni . setStorageSync ( this . cartonDraftKey (), { boxes : this . cartonBoxes , deletedBoxes : this . deletedCartonBoxes , forecastMarks : ( this . selected . cartons || []). map ( box => box . cartonMark ), discrepancyReason : this . countDiscrepancyReason }) } },
2026-09-17 13:36:08 +08:00
pallets : { deep : true , handler () { if ( ! this . submitting ) this . pendingKey = '' } }
},
2026-09-24 11:38:51 +08:00
onShow () { if ( requireSession ()) { this . user = session . currentUser ; restoreWarehouseWork ( ` ${ this . user . id } : ${ this . user . warehouseId } ` ); this . startScanner (); this . loadCandidates (). then (() => { const activeNo = String ( warehouseWork . warehouseTransfer . currentNo || '' ); const taskNo = this . pendingTaskNo || activeNo ; if ( this . entryMode === 'work' && taskNo ) { const active = this . candidates . find (( item ) => String ( item . transferNo ) === taskNo ); if ( active ) { const fromExplicitTask = Boolean ( this . pendingTaskNo ); this . pendingTaskNo = '' ; void this . select ( active , fromExplicitTask ) } } }) } },
2026-09-24 08:55:38 +08:00
onLoad ( options ) { this . entryMode = options ? . mode === 'list' ? 'list' : 'work' ; this . pendingTaskNo = String ( options ? . task || '' ) },
2026-09-17 13:36:08 +08:00
onHide () { this . stopScanner () },
beforeUnmount () { this . stopScanner () },
2026-09-23 09:06:31 +08:00
methods : {
2026-11-04 19:27:34 +08:00
simpleErrorMessage ( error ) { return String ( error ? . message || error || '操作失败,请重试' ). replace ( /[( (]\s*(?:错误码|code|requestId|traceId)\s*[:: =][\s\S]*$/i , '' ). replace ( /(?:requestId|traceId)\s*[:: =][\s\S]*$/i , '' ). trim () || '操作失败,请重试' },
2026-10-16 16:19:41 +08:00
// 快照只包含业务数据;复制、批量修改或改托后会自动失效并重新展开。
cartonConfirmationSignature ( box ) {
return JSON . stringify ({ pallet : String ( box . transferPalletId || '' ), dimensions : [ box . length , box . width , box . height , box . weight ]. map ( Number ), items : box . items . map ( line => [ line . skuCode , Number ( line . quantity )]) })
},
cartonConfirmed ( box ) { return Boolean ( box . confirmedSignature && box . confirmedSignature === this . cartonConfirmationSignature ( box )) },
// 单箱先保存设备草稿,最后一箱确认后统一提交后端已有的整单清点事务。
async confirmSingleCarton ( box ) {
if ( this . countingCartons || this . submitting || this . cartonsCounted || ! this . validCartonValues ( box ) || ! this . generatedPallets . some ( pallet => String ( pallet . draftId ) === String ( box . transferPalletId ))) return
box . confirmedSignature = this . cartonConfirmationSignature ( box )
this . error = null ; this . saveCartonDraft ()
if ( this . allCartonsConfirmed && this . cartonCountReady ) await this . confirmCartons ()
},
// 已保存清点时先请求服务端重开;成功后只展开当前箱,其他箱继续保留确认状态。
async editCarton ( box ) {
if ( this . countingCartons || this . submitting || this . pallets . some ( pallet => pallet . busy )) return
if ( ! this . cartonsCounted ) { box . confirmedSignature = '' ; this . saveCartonDraft (); return }
const mark = box . cartonMark
this . countingCartons = true ; this . error = null
try {
const detail = await reopenWarehouseTransferMeasurement ( this . selected . transferNo )
2026-10-16 17:43:48 +08:00
uni . setStorageSync ( this . cartonDraftKey (), { deletedBoxes : this . deletedCartonBoxes }); this . selected = detail ; this . restoreCartons ()
2026-10-16 16:19:41 +08:00
this . cartonBoxes . forEach ( current => { current . confirmedSignature = current . cartonMark === mark ? '' : this . cartonConfirmationSignature ( current ) })
this . pendingKey = '' ; this . saveCartonDraft ()
} catch ( error ) { this . error = error } finally { this . countingCartons = false }
},
2026-10-15 15:45:32 +08:00
useReceivingPallet ( pallet ) {
if ( this . cartonsCounted || this . countingCartons || ! pallet . generated ) return ;
this . receivingPalletId = pallet . draftId ; this . batchPalletId = pallet . draftId ; this . cartonPalletFilter = pallet . draftId ;
this . activePalletKey = '' ; this . cartonScrollTarget = 'transfer-boxes' ;
},
toggleBatchPanel () {
if ( this . countingCartons ) return ;
this . batchCountOpen = ! this . batchCountOpen ;
if ( this . batchCountOpen && ! this . batchSelectedIndexes . length ) this . selectBatchCartons ( true );
},
2026-10-15 18:25:46 +08:00
validCartonValues ( box ) { return Boolean ( box && [ box . length , box . width , box . height , box . weight ]. every ( value => Number ( value ) >= 0.01 && Number . isFinite ( Number ( value ))) && this . validCartonSkuLines ( box )) },
2026-10-15 15:45:32 +08:00
changeBatchSku ( index ) {
if ( this . countingCartons || this . cartonsCounted ) return ;
this . batchSku = this . batchSkuCodes [ index ] || '' ; this . batchSelectedIndexes = []; this . batchCountMessage = '' ; this . selectBatchCartons ( true );
this . batchBox = newBatchBox (); this . batchBox . items [ 0 ]. skuCode = this . batchSku ;
},
selectBatchCartons ( selected ) {
if ( this . countingCartons || this . cartonsCounted ) return ;
2026-10-15 18:25:46 +08:00
this . batchSelectedIndexes = selected ? this . cartonBoxes . flatMap (( box , index ) => box . received !== false && box . items . length === 1 && box . items [ 0 ] ? . skuCode === this . batchSku ? [ index ] : []) : [];
2026-10-15 15:45:32 +08:00
this . batchCountMessage = '' ;
},
toggleBatchCarton ( index ) {
if ( this . countingCartons || this . cartonsCounted ) return ;
const box = this . cartonBoxes [ index ];
2026-10-15 18:25:46 +08:00
if ( ! box || box . received === false || box . items . length !== 1 || box . items [ 0 ] ? . skuCode !== this . batchSku ) return ;
2026-10-15 15:45:32 +08:00
this . batchSelectedIndexes = this . batchSelectedIndexes . includes ( index ) ? this . batchSelectedIndexes . filter ( value => value !== index ) : [... this . batchSelectedIndexes , index ]; this . batchCountMessage = '' ;
},
copyCartonValues ( source , target ) {
for ( const key of [ 'length' , 'width' , 'height' , 'weight' ]) target [ key ] = source [ key ];
2026-10-15 18:25:46 +08:00
target . items = source . items . map ( line => ({ ... line }));
2026-10-15 15:45:32 +08:00
},
applyBatchCartons () {
if ( this . cartonsCounted || this . countingCartons || ! this . batchCountReady ) return ;
const targets = this . batchTargetBoxes ;
targets . forEach ( box => { this . copyCartonValues ( this . batchBox , box ); box . transferPalletId = this . batchPalletId });
this . cartonPalletFilter = '' ; this . saveCartonDraft (); this . batchCountMessage = `已应用到 ${ targets . length } 箱,共 ${ this . batchTotalQuantity } 件,可继续逐箱调整` ;
},
previousMatchingCarton ( index ) {
2026-10-15 18:25:46 +08:00
if ( this . cartonBoxes [ index ] ? . items . length !== 1 ) return null ;
2026-10-15 15:45:32 +08:00
const sku = this . cartonBoxes [ index ] ? . items [ 0 ] ? . skuCode ;
2026-10-15 18:25:46 +08:00
return this . cartonBoxes . slice ( 0 , index ). reverse (). find ( box => box . received !== false && box . items . length === 1 && box . items [ 0 ] ? . skuCode === sku );
2026-10-15 15:45:32 +08:00
},
copyPreviousCarton ( index ) {
if ( this . cartonsCounted || this . countingCartons ) return ;
const source = this . previousMatchingCarton ( index ), target = this . cartonBoxes [ index ];
if ( ! target || target . received === false || ! this . validCartonValues ( source )) return ;
this . copyCartonValues ( source , target ); this . saveCartonDraft ();
},
2026-10-22 23:06:06 +08:00
saveCartonDraft () { if ( this . selected && ! this . cartonsCounted ) uni . setStorageSync ( this . cartonDraftKey (), { boxes : this . cartonBoxes , deletedBoxes : this . deletedCartonBoxes , forecastMarks : ( this . selected . cartons || []). map ( box => box . cartonMark ), discrepancyReason : this . countDiscrepancyReason }) },
2026-10-15 15:45:32 +08:00
cartonDraftKey () { return `transfer-carton-count: ${ session . apiBase } : ${ this . user ? . id } : ${ this . user ? . warehouseId } : ${ this . selected ? . transferNo } ` },
restoreCartons () {
2026-10-16 16:53:09 +08:00
const rows = ( this . selected ? . cartons || []). filter ( box => box . status !== 'NOT_RECEIVED' ). map ( box => ({ ... box , transferPalletId : box . transferPalletId || ( this . generatedPallets . length === 1 ? this . generatedPallets [ 0 ]. draftId : '' ), received : box . status !== 'NOT_RECEIVED' , items : box . items ? . length ? box . items . map ( line => ({ skuCode : line . skuCode , quantity : line . quantity })) : [{ skuCode : this . selected . items . find ( item => String ( item . id ) === String ( box . transferItemId )) ? . skuCode || this . declaredSkuCodes [ 0 ] || '' , quantity : box . quantity ?? '' }] }));
2026-10-15 15:45:32 +08:00
const draft = uni . getStorageSync ( this . cartonDraftKey ());
2026-10-16 17:43:48 +08:00
this . deletedCartonBoxes = Array . isArray ( draft ? . deletedBoxes ) ? draft . deletedBoxes . filter ( box => ! box . cartonMark || ( this . selected ? . cartons || []). some ( row => row . cartonMark === box . cartonMark )) : [];
2026-10-15 18:25:46 +08:00
this . countDiscrepancyReason = this . selected ? . cartons ? . find ( box => box . discrepancyReason ) ? . discrepancyReason || '' ; this . cartonRequestKey = '' ; this . cartonSignature = '' ;
2026-10-16 17:43:48 +08:00
if ( ! this . cartonsCounted && Array . isArray ( draft ? . boxes ) && draft . boxes . every ( box => box . items ? . length && box . items . every ( line => this . declaredSkuCodes . includes ( line . skuCode )) && ( box . cartonMark || this . selected . items . some ( item => String ( item . id ) === String ( box . transferItemId )))) && draft . boxes . every ( box => ! box . cartonMark || ( this . selected ? . cartons || []). some ( row => row . cartonMark === box . cartonMark ))) {
2026-10-16 16:19:41 +08:00
const knownMarks = new Set ( draft . forecastMarks || draft . boxes . map ( box => box . cartonMark ));
2026-10-22 23:06:06 +08:00
this . cartonBoxes = [... draft . boxes , ... rows . filter ( box => ! knownMarks . has ( box . cartonMark ))]; this . countDiscrepancyReason = draft . discrepancyReason || '' ; this . cartonRequestKey = '' ; this . cartonSignature = '' ;
2026-10-15 15:45:32 +08:00
} else this . cartonBoxes = rows ;
2026-10-16 16:19:41 +08:00
this . cartonBoxes . forEach ( box => { if ( this . cartonsCounted || ( box . status === 'COUNTED' && this . validCartonValues ( box ))) box . confirmedSignature = this . cartonConfirmationSignature ( box ) });
2026-10-15 15:45:32 +08:00
this . receivingPalletId = this . generatedPallets [ 0 ] ? . draftId || '' ; this . cartonPalletFilter = '' ; this . batchPalletId = this . receivingPalletId ; this . batchCountOpen = false ; this . batchSelectedIndexes = []; this . batchCountMessage = '' ; this . batchSku = this . batchSkuCodes [ 0 ] || '' ; this . batchBox = newBatchBox (); this . batchBox . items [ 0 ]. skuCode = this . batchSku ;
},
async printCartonLabels () {
// #ifdef H5
const view = window . open ( '' , '_blank' );
if ( ! view ) { this . error = new Error ( '请允许打开标签打印窗口' ); return }
try { const label = await getWarehouseTransferCartonLabel ( this . selected . transferNo ); view . document . write ( label . printableHtml ); view . document . close (); view . focus (); view . print () }
catch ( error ) { view . close (); this . error = error }
// #endif
// #ifndef H5
this . error = new Error ( '当前设备未配置箱标签打印服务' );
// #endif
},
2026-10-16 17:43:48 +08:00
async removeCarton ( index ) {
if ( this . cartonsCounted || this . countingCartons || this . submitting || this . deletingCarton ) return ;
const box = this . cartonBoxes [ index ], transferNo = this . selected ? . transferNo ;
if ( ! box ) return ;
this . deletingCarton = true ;
try {
const answer = await new Promise (( resolve , reject ) => uni . showModal ({ title : '确认删除箱子' , content : `确定删除 ${ box . cartonMark || `第 ${ index + 1 } 箱` } 吗?删除后本箱不计入本次实收,保存清点时会记为未收到。误删可通过“恢复已删除箱子”找回。` , confirmText : '确认删除' , confirmColor : '#b74545' , cancelText : '取消' , success : resolve , fail : reject }));
if ( ! answer . confirm || transferNo !== this . selected ? . transferNo || this . cartonsCounted || this . countingCartons || this . submitting ) return ;
const position = this . cartonBoxes . indexOf ( box );
if ( position < 0 ) return ;
if ( ! box . cartonMark && ! box . localKey ) box . localKey = `removed- ${ Date . now () } - ${ Math . random () } ` ;
this . deletedCartonBoxes . push ( box ); this . cartonBoxes . splice ( position , 1 );
this . batchSelectedIndexes = []; this . batchCountMessage = '' ; this . saveCartonDraft ();
} catch ( error ) { this . error = error } finally { this . deletingCarton = false }
},
async restoreDeletedCarton () {
if ( this . countingCartons || this . submitting || this . deletingCarton || ! this . restorableCartons . length ) return ;
const candidates = this . restorableCartons , transferNo = this . selected ? . transferNo ;
try {
const choice = candidates . length === 1 ? 0 : await new Promise (( resolve , reject ) => uni . showActionSheet ({ itemList : candidates . map (( box , index ) => box . cartonMark || `已删除实收箱 ${ index + 1 } ` ), success : result => resolve ( result . tapIndex ), fail : () => resolve ( - 1 ) }));
if ( choice < 0 || transferNo !== this . selected ? . transferNo ) return ;
const removed = candidates [ choice ], key = removed . cartonMark || removed . localKey ;
if ( this . cartonsCounted ) { await this . remeasure (); if ( this . cartonsCounted || transferNo !== this . selected ? . transferNo ) return }
if ( this . cartonBoxes . some ( box => ( box . cartonMark || box . localKey ) === key )) return ;
const box = { ... removed , received : true , status : 'EXPECTED' , countedAt : null , confirmedSignature : '' ,
items : removed . items ? . length ? removed . items . map ( line => ({ ... line })) : [{ skuCode : this . selected . items . find ( item => String ( item . id ) === String ( removed . transferItemId )) ? . skuCode || this . declaredSkuCodes [ 0 ] || '' , quantity : removed . quantity ?? '' }] };
if ( ! this . generatedPallets . some ( pallet => String ( pallet . draftId ) === String ( box . transferPalletId ))) box . transferPalletId = this . receivingPalletId || this . generatedPallets [ 0 ] ? . draftId || '' ;
this . deletedCartonBoxes = this . deletedCartonBoxes . filter ( item => ( item . cartonMark || item . localKey ) !== key );
this . cartonBoxes . push ( box ); this . cartonPalletFilter = '' ; this . cartonRequestKey = '' ; this . cartonSignature = '' ; this . saveCartonDraft ();
} catch ( error ) { this . error = error }
2026-10-15 18:25:46 +08:00
},
2026-10-15 15:45:32 +08:00
addExtraCarton () {
if ( this . cartonsCounted || this . countingCartons || ! this . selected ? . items ? . length ) return ;
const item = this . selected . items [ 0 ];
this . cartonBoxes . push ({ localKey : `extra- ${ Date . now () } - ${ Math . random () } ` , cartonMark : '' , transferItemId : item . id , transferPalletId : this . receivingPalletId || this . generatedPallets [ 0 ] ? . draftId || '' , received : true , length : '' , width : '' , height : '' , weight : '' , items : [{ skuCode : item . skuCode , quantity : '' }] });
},
2026-10-15 18:25:46 +08:00
addCartonSku ( box ) {
if ( this . cartonsCounted || this . countingCartons || box . received === false ) return ;
const skuCode = this . declaredSkuCodes . find ( sku => ! box . items . some ( line => line . skuCode === sku ));
if ( skuCode ) box . items . push ({ skuCode , quantity : '' });
},
validCartonSkuLines ( box ) {
2026-10-16 16:53:09 +08:00
return box . items . length > 0 && box . items . length <= 200 && new Set ( box . items . map ( line => line . skuCode )). size === box . items . length
2026-10-15 18:25:46 +08:00
&& box . items . every ( line => this . declaredSkuCodes . includes ( line . skuCode ) && Number . isSafeInteger ( Number ( line . quantity )) && Number ( line . quantity ) > 0 && Number ( line . quantity ) <= 2147483647 );
2026-10-15 15:45:32 +08:00
},
async confirmCartons () {
if ( ! this . cartonCountReady || this . countingCartons || this . cartonsCounted ) return ;
this . countingCartons = true ; this . error = null ;
try {
2026-10-15 18:25:46 +08:00
const boxes = this . receivedCartonBoxes . map ( box => ({ cartonMark : box . cartonMark || null , transferItemId : box . cartonMark ? box . transferItemId : this . selected . items . find ( item => item . skuCode === box . items [ 0 ] ? . skuCode ) ? . id , transferPalletId : box . transferPalletId , length : Number ( box . length ), width : Number ( box . width ), height : Number ( box . height ), weight : Number ( box . weight ), items : box . items . map ( line => ({ skuCode : line . skuCode , quantity : Number ( line . quantity ) })) }));
2026-10-16 16:19:41 +08:00
const signature = JSON . stringify ({ boxes , discrepancyReason : this . countDiscrepancyReason , forecastPackageCount : Number ( this . selected . packageCount ) });
2026-10-27 17:39:44 +08:00
if ( ! this . cartonRequestKey || this . cartonSignature !== signature ) {
this . cartonRequestKey = `transfer-count- ${ Date . now () } - ${ Math . random (). toString ( 36 ). slice ( 2 ) } `
this . cartonSignature = signature
}
2026-10-22 23:06:06 +08:00
uni . setStorageSync ( this . cartonDraftKey (), { boxes : this . cartonBoxes , deletedBoxes : this . deletedCartonBoxes , forecastMarks : ( this . selected . cartons || []). map ( box => box . cartonMark ), discrepancyReason : this . countDiscrepancyReason });
2026-10-16 16:19:41 +08:00
const detail = await countWarehouseTransferCartons ( this . selected . transferNo , { idempotencyKey : this . cartonRequestKey , boxes , discrepancyReason : this . countDiscrepancyReason , forecastPackageCount : Number ( this . selected . packageCount ) });
2026-10-15 15:45:32 +08:00
if ( ! detail . cartons ? . length || ! detail . cartons . every ( box => box . countedAt )) throw new Error ( '清点结果未确认,请刷新核实' );
2026-10-16 17:43:48 +08:00
uni . setStorageSync ( this . cartonDraftKey (), { deletedBoxes : this . deletedCartonBoxes }); this . selected = detail ; this . pallets = detail . pallets . map ( restoredPallet ); this . restoreCartons ();
2026-10-15 15:45:32 +08:00
} catch ( error ) { this . error = error } finally { this . countingCartons = false }
},
2026-09-23 09:06:31 +08:00
openPalletOperations () { uni . navigateTo ({ url : '/pages/pallet/pallet' }) },
2026-09-22 17:27:56 +08:00
destinationLabel ( value , fallback = {}) { const code = String ( value ? . fbaWarehouseCode || fallback ? . fbaWarehouseCode || '' ). trim (); if ( code ) return code ; const type = value ? . destinationType || fallback ? . destinationType ; return type === 'PRIVATE' ? '私人仓' : '目的仓待定' },
2026-10-30 15:42:38 +08:00
listInboundCode ( item ) { const code = String ( item ? . inboundCode || '' ). trim (); return code !== String ( item ? . transferNo || '' ). trim () ? code : '' },
2026-09-17 13:36:08 +08:00
startScanner () { this . stopScanner (); this . _stopScanner = startScanBroadcast ({ onScan : ( value ) => this . handlePalletScan ( value ), onError : ( error ) => { this . error = error } }) },
stopScanner () { if ( this . _stopScanner ) this . _stopScanner (); this . _stopScanner = null },
2026-10-15 18:25:46 +08:00
handlePalletScan ( value ) { const pallet = this . pallets . find (( item ) => item . key === this . activePalletKey && item . generated && ! item . bound ); if ( ! pallet ) return ; pallet . bindCode = String ( value || '' ). trim (); this . bindPallet ( pallet ) },
2026-09-24 08:55:38 +08:00
statusLabel ( status , transferNo = '' ) { if ( String ( status ) === 'WAIT_PDA_RECEIVING' && String ( warehouseWork . warehouseTransfer . currentNo ) === String ( transferNo )) return '测量中' ; return ({ WAIT_PDA_RECEIVING : '待测量' , APPROVED : '待测量' , PARTIALLY_RECEIVED : '测量中' })[ status ] || '待测量' },
2026-10-15 15:45:32 +08:00
palletSummary ( pallet ) {
const boxes = this . receivedCartonBoxes . filter ( box => pallet . draftId && String ( box . transferPalletId ) === String ( pallet . draftId ));
return { cartonCount : boxes . length , volume : boxes . reduce (( sum , box ) => sum + ( Number ( box . length ) || 0 ) * ( Number ( box . width ) || 0 ) * ( Number ( box . height ) || 0 ) / 1000000 , 0 ), weight : boxes . reduce (( sum , box ) => sum + ( Number ( box . weight ) || 0 ), 0 ) };
},
2026-10-16 16:19:41 +08:00
// 逐托汇总后取三位,与服务端落库精度一致。
rawPalletVolume ( pallet ) { return Number ( this . palletSummary ( pallet ). volume . toFixed ( 3 )) },
palletVolume ( pallet ) { return this . rawPalletVolume ( pallet ). toFixed ( 3 ) },
2026-10-15 15:45:32 +08:00
cartonPalletLabel ( box ) { return this . generatedPallets . find ( pallet => String ( pallet . draftId ) === String ( box . transferPalletId )) ? . palletNo || '请选择托盘' },
assignCartonPallet ( box , index ) { if ( ! this . cartonsCounted && ! this . countingCartons && box . received !== false ) box . transferPalletId = this . generatedPallets [ index ] ? . draftId || '' },
2026-09-17 13:36:08 +08:00
applyPallet ( target , value ) { Object . assign ( target , restoredPallet ( value , this . pallets . indexOf ( target )), { key : target . key , busy : false , collapsed : false }) },
async generatePallet ( pallet ) {
2026-10-15 15:45:32 +08:00
if ( ! this . selected || pallet . busy || pallet . generated || this . cartonsCounted || this . countingCartons ) return
2026-09-17 13:36:08 +08:00
pallet . busy = true ; this . error = null
try {
2026-10-15 15:45:32 +08:00
const measured = { palletId : pallet . draftId || null , idempotencyKey : `pda-transfer-draft- ${ this . selected . transferNo } - ${ pallet . key } ` }
2026-09-17 13:36:08 +08:00
const draftDetail = await saveWarehouseTransferPalletDraft ( this . selected . transferNo , measured )
const draft = pallet . draftId ? ( draftDetail . pallets || []). find (( item ) => String ( item . id ) === String ( pallet . draftId )) : [...( draftDetail . pallets || [])]. sort (( a , b ) => Number ( b . sequenceNo || 0 ) - Number ( a . sequenceNo || 0 ))[ 0 ]
if ( ! draft ? . id ) throw new Error ( '服务端未返回已保存的托盘草稿' )
const generatedDetail = await generateWarehouseTransferPallet ( this . selected . transferNo , draft . id , { idempotencyKey : `pda-transfer-generate- ${ this . selected . transferNo } - ${ draft . id } ` })
const generated = ( generatedDetail . pallets || []). find (( item ) => String ( item . id ) === String ( draft . id ))
if ( ! generated ? . palletNo ) throw new Error ( '服务端未返回新托盘号' )
2026-10-15 15:45:32 +08:00
this . applyPallet ( pallet , generated ); this . pendingKey = '' ; this . receivingPalletId = pallet . draftId ; this . batchPalletId = pallet . draftId ; if ( this . generatedPallets . length === 1 ) this . receivedCartonBoxes . forEach ( box => { if ( ! box . transferPalletId ) box . transferPalletId = pallet . draftId }); this . saveCartonDraft ()
2026-09-17 13:36:08 +08:00
} catch ( error ) { this . error = error ; pallet . busy = false }
},
async bindPallet ( pallet ) {
const code = String ( pallet . bindCode || '' ). trim ()
if ( ! this . selected || pallet . busy || ! pallet . generated || pallet . bound || ! code ) return
pallet . busy = true ; this . error = null
try { const detail = await bindWarehouseTransferPallet ( this . selected . transferNo , pallet . draftId , { palletCode : code , idempotencyKey : `pda-transfer-bind- ${ this . selected . transferNo } - ${ pallet . draftId } - ${ code } ` }); const value = ( detail . pallets || []). find (( item ) => String ( item . id ) === String ( pallet . draftId )); if ( ! value ? . boundAt && value ? . status !== 'BOUND' ) throw new Error ( '服务端未确认托盘绑定' ); this . applyPallet ( pallet , value ); pallet . collapsed = true ; this . pendingKey = '' }
catch ( error ) { this . error = error ; pallet . busy = false }
},
async printDraftPallet ( pallet ) { if ( ! pallet . labelDownloadPath ) { this . error = new Error ( '服务端未返回托盘标签下载地址' ); return }; await this . openLabelPdf (() => downloadWarehouseTransferPalletLabel ( pallet . labelDownloadPath )) },
2026-10-16 16:19:41 +08:00
// 刷新本单时合并预报新增箱号,保留本机已填写的测量草稿。
async refreshCurrentTransfer () {
if ( ! this . selected || this . loading || this . submitting || this . countingCartons || this . pallets . some ( pallet => pallet . busy )) return
this . saveCartonDraft (); this . loading = true ; this . error = null
try {
const detail = await getWarehouseTransferDetail ( this . selected . transferNo )
this . selected = detail ; this . pallets = detail . pallets ? . length ? detail . pallets . map ( restoredPallet ) : [ newPallet ()]
this . restoreCartons (); this . pendingKey = '' ; await this . restorePhotos ( detail . transferNo , detail . measurementPhotos )
} catch ( error ) { this . error = error } finally { this . loading = false }
},
2026-09-24 11:38:51 +08:00
async loadCandidates () {
this . loading = true ; this . error = null
try {
const activeNo = String ( warehouseWork . warehouseTransfer . currentNo || '' )
2026-09-24 14:20:26 +08:00
const measuredStatuses = new Set ([ 'WAIT_PDA_RECEIVING' , 'PARTIALLY_RECEIVED' ])
2026-09-24 11:38:51 +08:00
this . candidates = ( await listWarehouseTransferCandidates ()). map (( item ) => String ( item . transferNo ) === activeNo && item . status === 'WAIT_PDA_RECEIVING' ? { ... item , status : 'PARTIALLY_RECEIVED' } : item )
2026-09-24 14:20:26 +08:00
this . candidates = this . candidates . filter (( item ) => measuredStatuses . has ( String ( item . status )))
2026-09-24 11:38:51 +08:00
// 服务端已完成当前单但设备在提交响应前断网时,本地 currentNo 会遗留。
// 候选已成功读取且当前单不再可测,清掉本地锁,允许继续下一票。
if ( activeNo && ! this . candidates . some (( item ) => String ( item . transferNo ) === activeNo && measuredStatuses . has ( String ( item . status )))) {
resetWarehouseDomain ( 'warehouseTransfer' )
}
2026-09-24 14:20:26 +08:00
if ( this . selected && ! this . candidates . some (( item ) => String ( item . transferNo ) === String ( this . selected . transferNo ))) {
this . selected = null ; this . pallets = [ newPallet ()]; this . photoFileIds = []; this . photoUrls = {}; this . photoNames = []
}
2026-09-24 11:38:51 +08:00
} catch ( e ) { this . error = e } finally { this . loading = false }
},
async select ( item , fromExplicitTask = false ) {
2026-10-15 15:45:32 +08:00
if ( this . detailLoading || this . submitting || this . countingCartons || this . pallets . some ( pallet => pallet . busy )) return
2026-09-24 08:55:38 +08:00
const requestedNo = String ( item ? . transferNo || '' )
const activeNo = String ( warehouseWork . warehouseTransfer . currentNo || '' )
2026-09-24 11:38:51 +08:00
if ( ! activeNo && ! fromExplicitTask ) {
2026-09-24 08:55:38 +08:00
const result = await new Promise (( resolve ) => uni . showModal ({ title : '开始测量' , content : `确认开始测量转运单 ${ requestedNo } 吗?开始后本设备需先完成该单,才能选择下一单。` , confirmText : '确认开始' , cancelText : '取消' , success : resolve , fail : () => resolve ({ confirm : false }) }))
if ( ! result . confirm ) return
}
if ( activeNo && activeNo !== requestedNo ) {
const result = await new Promise (( resolve ) => uni . showModal ({ title : '存在未完结的测量单' , content : `转运单 ${ activeNo } 尚未测量完成,是否继续进入?` , confirmText : '去继续测量' , cancelText : '取消' , success : resolve , fail : () => resolve ({ confirm : false }) }))
if ( ! result . confirm ) return
item = this . candidates . find (( candidate ) => String ( candidate . transferNo ) === activeNo ) || item
}
updateWarehouseWork ( 'warehouseTransfer' , { currentNo : String ( item . transferNo ) })
this . candidates = this . candidates . map (( candidate ) => String ( candidate . transferNo ) === String ( item . transferNo ) ? { ... candidate , status : 'PARTIALLY_RECEIVED' } : candidate )
if ( this . entryMode === 'list' ) { uni . navigateTo ({ url : `/pages/warehouse-transfer/warehouse-transfer?mode=work&task= ${ encodeURIComponent ( String ( item . transferNo )) } ` }); return }
2026-09-22 17:27:56 +08:00
this . detailLoading = true ; this . error = null ; this . success = false ; this . completedPallets = []; this . completedTransferNo = '' ; this . completedDestination = '' ; this . completedDestinationType = ''
2026-10-16 16:19:41 +08:00
try { this . selected = await getWarehouseTransferDetail ( item . transferNo ); this . summaryCollapsed = false ; this . pendingKey = '' ; this . pallets = this . selected . pallets ? . length ? this . selected . pallets . map ( restoredPallet ) : [ newPallet ()]; this . restoreCartons (); await this . restorePhotos ( item . transferNo , this . selected . measurementPhotos ) }
2026-09-17 13:36:08 +08:00
catch ( e ) { this . selected = null ; this . error = e }
finally { this . detailLoading = false }
},
2026-10-15 15:45:32 +08:00
backToCandidates () { if ( this . submitting || this . detailLoading || this . countingCartons || this . pallets . some ( pallet => pallet . busy )) return ; this . entryMode = 'list' ; this . selected = null ; this . error = null ; this . pallets = [ newPallet ()]; this . photoFileIds = []; this . photoUrls = {}; this . photoNames = [] },
addPallet () { if ( this . cartonsCounted || this . countingCartons ) return ; this . pendingKey = '' ; this . pallets . push ( newPallet ()) },
2026-10-16 16:19:41 +08:00
async remeasure () {
if ( ! this . selected || this . countingCartons || this . submitting || this . pallets . some ( pallet => pallet . busy )) return ;
this . countingCartons = true ; this . error = null ;
try {
const detail = await reopenWarehouseTransferMeasurement ( this . selected . transferNo );
2026-10-16 17:43:48 +08:00
uni . setStorageSync ( this . cartonDraftKey (), { deletedBoxes : this . deletedCartonBoxes }); this . selected = detail ; this . restoreCartons (); this . pendingKey = '' ;
2026-10-16 16:19:41 +08:00
this . cartonScrollTarget = 'transfer-boxes' ;
} catch ( error ) { this . error = error } finally { this . countingCartons = false }
},
async removePallet ( index ) {
const pallet = this . pallets [ index ];
if ( ! pallet || this . countingCartons || this . submitting || this . pallets . some ( item => item . busy )) return ;
pallet . busy = true ;
try {
const answer = await new Promise (( resolve , reject ) => uni . showModal ({ title : '删除托盘' , content : '删除后保留箱子和测量数据,本托箱子需重新选择托盘并确认清点。是否继续?' , success : resolve , fail : reject }));
if ( ! answer . confirm ) return ;
this . error = null ;
if ( pallet . draftId ) this . selected = await deleteWarehouseTransferPallet ( this . selected . transferNo , pallet . draftId );
this . cartonBoxes . forEach ( box => { if ( pallet . draftId && String ( box . transferPalletId ) === String ( pallet . draftId )) box . transferPalletId = '' });
this . pallets . splice ( index , 1 ); if ( ! this . pallets . length ) this . pallets = [ newPallet ()];
this . receivingPalletId = this . generatedPallets [ 0 ] ? . draftId || '' ; this . batchPalletId = this . receivingPalletId ; this . cartonPalletFilter = '' ; this . activePalletKey = '' ;
this . cartonRequestKey = '' ; this . cartonSignature = '' ; this . pendingKey = '' ; this . batchSelectedIndexes = []; this . saveCartonDraft ();
} catch ( error ) { this . error = error } finally { pallet . busy = false }
},
2026-09-17 13:36:08 +08:00
async restorePhotos ( transferNo , knownPhotos ) {
2026-09-28 17:26:50 +08:00
let photos
2026-09-17 13:36:08 +08:00
try {
2026-09-28 17:26:50 +08:00
// Detail responses contain submitted photos only. The PDA list endpoint
// is the authoritative source for uploads that have not been submitted.
photos = await listWarehouseTransferMeasurementPhotos ( transferNo )
} catch ( error ) {
if ( ! Array . isArray ( knownPhotos )) { this . error = error ; return false }
photos = knownPhotos
}
const nextUrls = {}
await Promise . all ( photos . map ( async ( photo ) => { try { nextUrls [ String ( photo . fileId )] = await downloadWarehouseTransferMeasurementPhoto ( transferNo , photo . fileId ) } catch ( _ ) { /* 单张恢复失败不阻断补拍 */ } }))
this . photoFileIds = photos . map (( photo ) => String ( photo . fileId )); this . photoNames = photos . map (( photo ) => photo . fileName || '测量现场照片' ); this . photoUrls = nextUrls
return true
2026-09-17 13:36:08 +08:00
},
takePhoto () {
2026-09-28 17:26:50 +08:00
if ( ! this . selected || this . uploadingPhoto || this . submitting || this . photoFileIds . length >= MAX_MEASUREMENT_PHOTOS ) {
if ( this . photoFileIds . length >= MAX_MEASUREMENT_PHOTOS ) this . error = new Error ( `测量现场照片最多 ${ MAX_MEASUREMENT_PHOTOS } 张` )
return
}
2026-09-17 13:36:08 +08:00
const transferNo = this . selected . transferNo
2026-09-28 17:26:50 +08:00
uni . chooseImage ({ count : Math . min ( 9 , MAX_MEASUREMENT_PHOTOS - this . photoFileIds . length ), sourceType : [ 'camera' , 'album' ], success : async ({ tempFilePaths , tempFiles }) => {
2026-09-17 13:36:08 +08:00
const paths = ( tempFilePaths || []). length ? tempFilePaths : ( tempFiles || []). map (( file ) => file . path || file . tempFilePath ). filter ( Boolean )
if ( this . selected ? . transferNo !== transferNo ) { this . error = new Error ( '所选转运单已切换,请重新拍摄' ); return }
if ( ! paths . length ) { this . error = new Error ( '未获取到可上传的照片路径,请重新拍摄' ); return }
2026-09-28 17:26:50 +08:00
const remaining = MAX_MEASUREMENT_PHOTOS - this . photoFileIds . length
const uploadPaths = paths . slice ( 0 , remaining )
const skipped = paths . length - uploadPaths . length
2026-09-17 13:36:08 +08:00
this . uploadingPhoto = true ; this . error = null
try {
2026-09-28 17:26:50 +08:00
for ( const [ index , path ] of uploadPaths . entries ()) {
if ( this . photoFileIds . length >= MAX_MEASUREMENT_PHOTOS ) throw new Error ( `测量现场照片最多 ${ MAX_MEASUREMENT_PHOTOS } 张` )
2026-09-17 13:36:08 +08:00
if ( Number ( tempFiles ? .[ index ] ? . size || 0 ) > 10 * 1024 * 1024 ) throw new Error ( '照片不能超过 10MB' )
const file = await uploadWarehouseTransferMeasurementPhoto ( transferNo , path )
if ( this . selected ? . transferNo !== transferNo ) throw new Error ( '所选转运单已切换,请重新拍摄' )
const fileId = String ( file . fileId ); this . photoUrls [ fileId ] = path ; this . photoFileIds . push ( fileId ); this . photoNames . push ( file . fileName || '测量现场照片' )
}
2026-09-28 17:26:50 +08:00
if ( skipped > 0 ) this . error = new Error ( `最多上传 ${ MAX_MEASUREMENT_PHOTOS } 张测量现场照片,已忽略超出的 ${ skipped } 张` )
2026-09-17 13:36:08 +08:00
this . pendingKey = ''
2026-09-28 17:26:50 +08:00
} catch ( error ) {
this . error = error
// Another window/device may have filled the remaining slots. Re-read
// server state so a refresh cannot reset the UI back to 0/20.
if ( /at most 20|最多.?20|supports at most/i . test ( String ( error ? . message || '' ))) {
await this . restorePhotos ( transferNo )
this . error = error
}
} finally { this . uploadingPhoto = false }
2026-09-17 13:36:08 +08:00
}, fail : ( cause ) => { if ( ! /cancel/i . test ( String ( cause ? . errMsg || '' ))) this . error = new Error ( cause ? . errMsg || '选择照片失败,请重试' ) } })
},
previewPhoto ( fileId ) { const urls = this . photoFileIds . map (( id ) => this . photoUrls [ id ]). filter ( Boolean ); if ( this . photoUrls [ fileId ]) uni . previewImage ({ current : this . photoUrls [ fileId ], urls }) },
removePhoto ( index ) { if ( this . uploadingPhoto || this . submitting ) return ; const [ fileId ] = this . photoFileIds . splice ( index , 1 ); this . photoNames . splice ( index , 1 ); if ( fileId ) delete this . photoUrls [ fileId ]; this . pendingKey = '' },
async submit () {
if ( ! this . selected || this . submitting ) return
if ( this . validationMessage ) { this . error = new Error ( this . validationMessage ); return }
this . submitting = true ; this . error = null
try {
2026-10-16 16:19:41 +08:00
// 清点、绑托和拍照完成后,仅 SKU 件数差异要求填写原因。
if ( this . hasSkuReceiptDifference ) {
const answer = await new Promise (( resolve , reject ) => uni . showModal ({
title : 'SKU签收数量存在差异' , content : this . countDiscrepancyReason || '' , editable : true ,
placeholderText : '请填写SKU签收差异原因(必填)' , confirmText : '确认完成' ,
success : resolve , fail : reject
}))
if ( ! answer . confirm ) return
const reason = String ( answer . content || '' ). trim ()
if ( ! reason ) { this . error = new Error ( '请填写差异原因后再完成测量' ); return }
if ( reason . length > 1000 ) { this . error = new Error ( '差异原因不能超过1000字' ); return }
if ( reason !== this . countDiscrepancyReason ) this . pendingKey = ''
this . countDiscrepancyReason = reason
this . saveCartonDraft ()
}
2026-10-30 15:42:38 +08:00
this . pendingKey = `pda-transfer- ${ this . selected . transferNo } - ${ Date . now () } - ${ Math . random (). toString ( 36 ). slice ( 2 , 10 ) } `
2026-10-16 16:19:41 +08:00
const payload = { idempotencyKey : this . pendingKey , photoFileIds : [... this . photoFileIds ], discrepancyReason : this . hasSkuReceiptDifference ? this . countDiscrepancyReason : null }
2026-09-24 08:55:38 +08:00
const transferNo = this . selected . transferNo ; const result = await palletizeWarehouseTransfer ( transferNo , payload ); this . completedTransferNo = transferNo ; this . completedDestination = result ? . fbaWarehouseCode || this . selected . fbaWarehouseCode || '' ; this . completedDestinationType = result ? . destinationType || this . selected . destinationType || '' ; this . completedPallets = ( result ? . pallets || []). filter (( pallet ) => pallet ? . palletNo ); this . success = true ; resetWarehouseDomain ( 'warehouseTransfer' ); this . selected = null ; this . pendingKey = '' ; this . pallets = [ newPallet ()]; this . photoFileIds = []; this . photoUrls = {}; this . photoNames = []; await this . loadCandidates ()
2026-09-17 13:36:08 +08:00
} catch ( e ) { this . error = e } finally { this . submitting = false }
},
async printPallet ( pallet ) { if ( ! pallet ? . labelDownloadPath ) { this . error = new Error ( '服务端未返回托盘标签下载地址,请刷新后重试' ); return }; await this . openLabelPdf (() => downloadWarehouseTransferPalletLabel ( pallet . labelDownloadPath )) },
async openLabelPdf ( download ) {
if ( this . openingLabel ) return
this . openingLabel = true ; this . error = null
let h5View = null
// #ifdef H5
h5View = window . open ( '' , '_blank' ); if ( ! h5View ) { this . error = new Error ( '请允许打开托盘标签 PDF 窗口' ); this . openingLabel = false ; return }
// #endif
try {
const filePath = await download ()
// #ifdef H5
h5View . location . href = filePath
// #endif
// #ifndef H5
await new Promise (( resolve , reject ) => uni . openDocument ({ filePath , fileType : 'pdf' , showMenu : true , success : resolve , fail : ( cause ) => reject ( new Error ( cause ? . errMsg || '托盘标签 PDF 打开失败' )) }))
// #endif
} catch ( error ) { if ( h5View ) h5View . close (); this . error = error } finally { this . openingLabel = false }
}
}
}
</ script >
< style scoped >
2026-10-16 16:19:41 +08:00
. collapsed - carton - row { position : relative ; min - height : 40 px ; padding : 0 36 px 0 0 ; color : # 415 a78 ; font - size : 12 px ; line - height : 40 px }. collapsed - carton - mark { width : 100 % ; white - space : nowrap }. collapsed - carton - mark text { display : inline - block ; white - space : nowrap ; word - break : normal ; overflow - wrap : normal }. collapsed - carton - expand { position : absolute ; top : 0 ; right : 0 ; display : flex ; align - items : center ; justify - content : center ; width : 32 px ; height : 32 px ; margin : 0 ; padding : 0 ; border : 0 ; border - radius : 6 px ; background : transparent ; line - height : 32 px }. collapsed - carton - expand :: after { border : 0 }. transfer - content . carton - count - card . carton - collapsed { padding : 6 px 12 px }
. confirm - carton - button { margin : 12 px 0 0 ; border : 1 px solid # cbdcff ; border - radius : 8 px ; color : # 135 ee8 ; background : # eff5ff ; font - size : 13 px ; line - height : 38 px }
. pallet - title - line { display : flex ; align - items : center ; flex - wrap : wrap ; gap : 5 px 10 px }. pallet - inline - metrics { display : flex ; flex - wrap : wrap ; gap : 4 px 8 px }. transfer - content . pallet - inline - metrics text { margin : 0 ; color : # 3 f5671 ; font - size : 10 px ; font - weight : 500 ; white - space : nowrap }. pallet - index { flex - shrink : 0 }. remeasure - button { width : auto ; margin : 0 ; padding : 0 12 px ; border : 1 px solid # b9d3f4 ; border - radius : 8 px ; color : # 135 ee8 ; font - size : 12 px ; line - height : 36 px ; background : # f0f6ff }
. carton - title - row { display : flex ; align - items : flex - start ; gap : 8 px }. carton - title - row { flex - wrap : wrap }. carton - title - row > . measure - title { flex : 1 ; min - width : 150 px }. delete - carton - button { flex : none ; width : auto ; margin : 0 ; padding : 0 8 px ; border : 1 px solid # f0caca ; border - radius : 7 px ; color : # b74545 ; background : # fff7f7 ; font - size : 12 px ; line - height : 30 px }. add - sku - button { display : flex ; align - items : center ; justify - content : center ; flex : none ; box - sizing : border - box ; width : 24 px ; height : 24 px ; margin : 0 ; padding : 0 ; border : 1 px solid # cbdcff ; border - radius : 7 px ; color : # 135 ee8 ; background : # f0f6ff ; font - size : 12 px ; line - height : 30 px }. add - sku - button [ disabled ]{ color : # a0adbe ; background : # f5f7fa }
2026-10-15 18:25:46 +08:00
. pallet - index { display : flex ; align - items : center ; justify - content : center ; width : 34 px ; height : 34 px ; border - radius : 10 px ; color : # fff ; font - size : 13 px ; font - weight : 850 ; background : # 135 ee8 ; box - shadow : 0 4 px 10 px rgba ( 19 , 94 , 232 , .2 )}
. transfer - page { display : flex ; flex - direction : column }. transfer - page . transfer - scroll { flex : 1 ; min - height : 0 ; height : 0 }
. transfer - work - toolbar { display : flex ; flex - shrink : 0 ; align - items : center ; justify - content : space - between ; gap : 12 px ; padding : 8 px 14 px ; border - bottom : 1 px solid # e1e7ef ; background : # fff }
. transfer - work - toolbar . back - to - list ,. transfer - work - toolbar . refresh - button { display : flex ; align - items : center ; justify - content : center ; gap : 4 px ; width : auto ; min - height : 40 px ; margin : 0 ; padding : 0 12 px ; border : 1 px solid # d6e2f1 ; border - radius : 8 px ; color : # 135 ee8 ; font - size : 13 px ; line - height : 40 px ; background : # f8fbff }
. carton - prerequisite { display : block ; margin : 0 0 12 px ; color : # 718095 ; font - size : 12 px ; line - height : 18 px }
. active - receiving - pallet { border - color : # 79 a7ec ! important }. pallet - brief { display : flex ; align - items : center ; flex - wrap : wrap ; gap : 10 px ; margin :- 3 px 0 12 px ; color : # 3 f5671 ; font - size : 12 px }. pallet - brief button { margin : 0 0 0 auto ; padding : 0 10 px ; height : 36 px ; line - height : 36 px ; border - radius : 8 px ; background : # eaf2ff ; color : # 135 ee8 ; font - size : 11 px }. carton - filter ,. batch - box - options { display : flex ; flex - wrap : wrap ; gap : 7 px ; margin : 12 px 0 }. carton - filter button ,. batch - box - options button { width : auto ; margin : 0 ; padding : 0 11 px ; height : 36 px ; line - height : 36 px ; border : 1 px solid # d4dfed ; border - radius : 9 px ; background : # fff ; color : # 5 b6e84 ; font - size : 12 px }. carton - filter . active ,. batch - box - options . chosen { border - color : # 135 ee8 ; background : # eaf2ff ; color : # 135 ee8 }. batch - count - panel . add - pallet - button { margin : 0 }. carton - count - card > . measure - title { overflow - wrap : anywhere ; font - size : 12 px ; line - height : 19 px }. batch - sku { padding : 10 px ; border : 1 px solid # cbdcf2 ; border - radius : 8 px ; background : # fff }. summary - no { overflow - wrap : anywhere }
2026-10-15 15:45:32 +08:00
. batch - count - panel { margin : 12 px 0 ; padding : 10 px ; border : 1 px solid # b9d3f4 ; border - radius : 10 px ; background : # f4f8ff }. batch - sku { margin : 10 px 0 ; color : # 17314 f ; font - size : 12 px ; word - break : break - all }. batch - actions { display : flex ; flex - wrap : wrap ; gap : 8 px ; margin : 8 px 0 }. batch - actions button { width : auto ; margin : 0 ; padding : 0 10 px ; min - height : 34 px ; line - height : 34 px ; font - size : 11 px ; color : # 135 ee8 ; border : 1 px solid # b9d3f4 ; border - radius : 7 px ; background : # fff }. batch - actions button [ disabled ]{ opacity : .45 }. batch - summary { display : block ; margin : 10 px 0 ; color : # 245981 ; font - size : 11 px ; line - height : 18 px }
2026-10-15 18:25:46 +08:00
. carton - count - card { padding : 14 px 10 px ; margin : 10 px 0 ; border : 1 px solid # d8e3f1 ; border - radius : 12 px }
2026-10-15 15:45:32 +08:00
2026-10-30 15:42:38 +08:00
. transfer - page { height : 100 vh ; overflow : hidden ; background : # f2f5f9 }. transfer - scroll { height : calc ( 100 vh - 48 px - var ( -- status - bar - height , 0 px ))}. transfer - content { padding - bottom : 52 px }. transfer - list - page { background : # f1f5fd }
. transfer - list - page . transfer - content { padding - top : 14 px ; padding - bottom : calc ( 24 px + env ( safe - area - inset - bottom , 0 px ))}
. transfer - heading { display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px ; margin : 0 0 11 px }
. transfer - list - count { min - width : 0 ; color : # 526174 ; font - size : 14 px ; font - weight : 700 ; line - height : 20 px }
. transfer - no ,. transfer - meta ,. summary - eyebrow ,. summary - no ,. pallet - heading - copy text ,. measure - title ,. measure - hint ,. sku - copy text ,. validation - title ,. transfer - validation > text ,. feedback - title ,. transfer - error > text ,. transfer - success > text ,. empty - title ,. transfer - empty > text { display : block }
. refresh - button { display : flex ; align - items : center ; justify - content : center ; gap : 4 px ; width : auto ; height : 34 px ; margin : 0 ; padding : 0 11 px ; border : 1 px solid # b8d1f5 ; border - radius : 17 px ; color : # 135 ee8 ; font - size : 10 px ; line - height : 34 px ; background : # f8fbff }
. transfer - heading . refresh - button { box - sizing : border - box ; flex - shrink : 0 ; gap : 5 px ; min - width : 72 px ; height : 36 px ; padding : 0 14 px ; border - radius : 20 px ; font - size : 14 px ; font - weight : 700 ; line - height : normal ; background : # fff }
. refresh - button [ disabled ]{ opacity : .55 }
. candidate - list { display : flex ; flex - direction : column ; gap : 11 px ; margin - bottom : 16 px }
. transfer - card { display : block ; box - sizing : border - box ; width : 100 % ; margin : 0 ; padding : 14 px ; border : 1 px solid # dce5f0 ; border - radius : 13 px ; color : # 1 f2937 ; line - height : normal ; text - align : left ; white - space : normal ; background : # fff ; box - shadow : none ; cursor : pointer ; transition : background - color .12 s ease , border - color .12 s ease }
. transfer - card - pressed { border - color : # 93 baf0 ; background : # f4f8ff }
. transfer - card - head { display : flex ; align - items : center ; justify - content : space - between ; gap : 10 px }
. transfer - no { flex : 1 ; min - width : 0 ; color : # 1 f2937 ; font - size : 18 px ; font - weight : 750 ; line - height : 26 px ; overflow - wrap : anywhere ; word - break : break - all }
. status - pill { flex - shrink : 0 ; padding : 6 px 10 px ; border - radius : 9 px ; color : # 135 ee8 ; font - size : 13 px ; font - weight : 700 ; line - height : 16 px ; white - space : nowrap ; background : # e5efff }
. status - measuring { color : # 08705 f ; background : # e2f6f0 }
. task - metrics { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) minmax ( 0 , 1.3 fr ) minmax ( 0 , 1 fr ); margin - top : 12 px ; padding : 7 px 0 ; border - top : 1 px solid # e1e7f0 ; border - bottom : 1 px solid # e1e7f0 }
. task - metrics view { min - width : 0 ; padding : 0 6 px ; border - right : 1 px solid # e1e7f0 ; text - align : center }
. task - metrics view : last - child { border - right : 0 }
. task - metrics strong { display : block ; color : # 1 f2937 ; font - size : 22 px ; font - weight : 750 ; line - height : 26 px ; overflow - wrap : anywhere }
. task - metrics text { display : block ; margin - top : 2 px ; color : # 6 f8194 ; font - size : 12 px ; line - height : 18 px }
. transfer - card - footer { display : flex ; align - items : center ; justify - content : flex - end ; gap : 10 px ; min - height : 28 px ; margin - top : 10 px }
. transfer - meta { flex : 1 ; min - width : 0 ; color : # 718095 ; font - size : 12 px ; line - height : 20 px ; overflow - wrap : anywhere }
. transfer - card - action { display : flex ; flex - shrink : 0 ; align - items : center ; gap : 2 px ; color : # 135 ee8 ; font - size : 13 px ; font - weight : 700 ; line - height : 22 px ; white - space : nowrap }
. transfer - list - page . transfer - empty { font - size : 12 px ; line - height : 20 px }
. transfer - list - page . empty - title { font - size : 14 px }
. transfer - empty { margin - bottom : 12 px ; padding : 26 px 14 px ; border : 1 px dashed # ccd7e4 ; border - radius : 13 px ; color : # 8996 a6 ; font - size : 10 px ; line - height : 18 px ; text - align : center ; background : rgba ( 255 , 255 , 255 , .65 )}. empty - title { color : # 56677 b ; font - size : 12 px ; font - weight : 800 }. order - summary { display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px ; padding : 14 px 15 px ; border - radius : 14 px 14 px 0 0 ; color : # fff ; background : linear - gradient ( 135 deg , # 123 d73 , # 135 ee8 )}. summary - eyebrow { font - size : 9 px ; opacity : .72 }. summary - no { margin - top : 3 px ; font - size : 15 px ; font - weight : 850 }. summary - count { display : flex ; align - items : baseline ; gap : 3 px ; flex - shrink : 0 }. summary - count strong { font - size : 24 px }. summary - count text { font - size : 10 px ; opacity : .8 }. summary - metrics { display : grid ; grid - template - columns : repeat ( 3 , 1 fr ); margin - bottom : 12 px ; padding : 11 px 8 px ; border - radius : 0 0 14 px 14 px ; background : # fff ; box - shadow : 0 5 px 17 px rgba ( 32 , 67 , 111 , .07 )}. summary - metrics view { position : relative ; text - align : center }. summary - metrics view + view : before { position : absolute ; top : 3 px ; bottom : 3 px ; left : 0 ; width : 1 px ; background : # e7edf4 ; content : '' }. summary - metrics strong { display : block ; color : # 17314 f ; font - size : 14 px }. summary - metrics text { display : block ; margin - top : 2 px ; color : # 8390 a1 ; font - size : 8 px }. pallet - card { box - sizing : border - box ; margin - bottom : 12 px ; padding : 14 px ; border : 1 px solid # d9e3ee ; border - radius : 15 px ; background : # fff ; box - shadow : 0 5 px 18 px rgba ( 32 , 67 , 111 , .065 )}. pallet - head { display : flex ; align - items : center ; gap : 9 px ; margin :- 2 px - 2 px 14 px ; padding - bottom : 11 px ; border - bottom : 1 px solid # edf1f5 }. pallet - heading - copy { min - width : 0 ; flex : 1 }. pallet - heading - copy text : first - child { color : # 17314 f ; font - size : 13 px ; font - weight : 850 }. pallet - heading - copy > text : last - child { margin - top : 2 px ; color : # 8995 a5 ; font - size : 8 px }. remove - button { width : auto ; height : 28 px ; margin : 0 ; padding : 0 9 px ; border - radius : 7 px ; color : # b33030 ; font - size : 9 px ; line - height : 28 px ; background : # fff0f0 }. field - label { display : block ; margin - bottom : 5 px ; color : # 65768 b ; font - size : 9 px ; font - weight : 700 }. field - label small { margin - left : 3 px ; color : # 9 aa5b3 ; font - size : 8 px ; font - weight : 500 }. measure - input ,. input - with - unit { box - sizing : border - box ; width : 100 % ; height : 42 px ; border : 1 px solid # cad7e5 ; border - radius : 9 px ; color : # 203750 ; background : # fafcfe }. measure - input { padding : 0 11 px ; font - size : 12 px }. pallet - code - field { display : block ; margin - bottom : 14 px }. measure - section - head ,. sku - section - head { display : flex ; align - items : center ; justify - content : space - between ; gap : 10 px ; margin - bottom : 9 px }. measure - title { color : # 243 a53 ; font - size : 11 px ; font - weight : 850 }. measure - hint { margin - top : 2 px ; color : # 8 a96a5 ; font - size : 8 px }. volume - chip { flex - shrink : 0 ; padding : 5 px 8 px ; border - radius : 8 px ; text - align : right ; background : # edf5ff }. volume - chip text { display : block ; color : # 7189 a7 ; font - size : 7 px }. volume - chip strong { display : block ; margin - top : 1 px ; color : # 135 ee8 ; font - size : 10 px }. dimension - grid { display : grid ; grid - template - columns : repeat ( 3 , 1 fr ); gap : 7 px }. measure - grid { display : grid ; grid - template - columns : repeat ( 2 , 1 fr ); gap : 9 px ; margin - top : 10 px }. input - with - unit { display : flex ; align - items : center ; overflow : hidden }. input - with - unit input { box - sizing : border - box ; min - width : 0 ; height : 40 px ; padding : 0 4 px 0 10 px ; flex : 1 ; color : # 203750 ; font - size : 16 px ; font - weight : 800 }. input - with - unit > text { padding - right : 8 px ; color : # 8491 a1 ; font - size : 8 px }. input - with - unit . important { border - color : # 9 ebff0 ; background : # f8fbff }. sku - section { margin : 15 px - 2 px - 2 px ; padding : 12 px ; border - radius : 11 px ; background : # f5f8fb }. sku - total { flex - shrink : 0 ; padding : 5 px 7 px ; border - radius : 7 px ; color : # 9 a5c13 ; font - size : 9 px ; font - weight : 800 ; background : # fff1dd }. sku - total . matched { color : # 08705 f ; background : # dff5ed }. sku - row { display : flex ; align - items : center ; gap : 9 px ; min - height : 48 px ; border - top : 1 px solid # e3eaf2 }. sku - copy { min - width : 0 ; flex : 1 }. sku - code { overflow : hidden ; color : # 233 b56 ; font - size : 11 px ; font - weight : 800 ; text - overflow : ellipsis ; white - space : nowrap }. sku - copy text : last - child { margin - top : 2 px ; color : # 8693 a3 ; font - size : 8 px }. sku - input { display : flex ; align - items : center ; gap : 4 px ; color : # 718095 ; font - size : 9 px }. sku - input input { box - sizing : border - box ; width : 64 px ; height : 34 px ; padding : 0 7 px ; border : 1 px solid # abc4e8 ; border - radius : 8 px ; color : # 17314 f ; font - size : 14 px ; font - weight : 800 ; text - align : center ; background : # f
2026-09-17 13:36:08 +08:00
. photo - card { box - sizing : border - box ; margin - bottom : 11 px ; padding : 14 px ; border : 1 px solid # d9e3ee ; border - radius : 13 px ; background : # fff ; box - shadow : 0 5 px 18 px rgba ( 32 , 67 , 111 , .06 )}
. photo - head { display : flex ; align - items : center ; justify - content : space - between ; gap : 10 px ; margin - bottom : 9 px }
. photo - count { flex - shrink : 0 ; padding : 5 px 8 px ; border - radius : 7 px ; color : # 135 ee8 ; font - size : 9 px ; font - weight : 800 ; background : # eaf2ff }
. photo - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : 9 px ; margin : 11 px 0 }
. photo - tile { position : relative ; overflow : hidden ; border - radius : 10 px ; background : # eef2f6 }
. photo - image { display : block ; width : 100 % ; height : 112 px }
. photo - remove { position : absolute ; right : 5 px ; bottom : 5 px ; width : auto ; height : 26 px ; margin : 0 ; padding : 0 8 px ; border - radius : 6 px ; color : # fff ; font - size : 9 px ; line - height : 26 px ; background : rgba ( 152 , 35 , 35 , .88 )}
. take - photo - button { display : flex ; align - items : center ; justify - content : center ; gap : 6 px ; box - sizing : border - box ; width : 100 % ; height : 42 px ; margin : 9 px 0 0 ; border : 1 px solid # 9 fc0ef ; border - radius : 9 px ; color : # 135 ee8 ; font - size : 10 px ; font - weight : 800 ; background : # f8fbff }
. take - photo - button [ disabled ]{ color : # 9 aa7b5 ; background : # e7ecf2 ; opacity : 1 }
. print - card { box - sizing : border - box ; margin - bottom : 11 px ; padding : 14 px ; border : 1 px solid # b9d3f4 ; border - radius : 13 px ; background : # f8fbff }. print - row button { width : auto ; margin : 0 ; padding : 0 10 px ; color : # 135 ee8 ; border : 1 px solid # 9 fc0ef ; border - radius : 8 px ; background : # fff }. print - row { display : flex ; align - items : center ; justify - content : space - between ; padding : 10 px 0 ; border - top : 1 px solid # dce8f5 ; color : # 728196 ; font - size : 9 px }. print - code { display : block ; color : # 17314 f ; font - size : 13 px ; font - weight : 850 }
. generate - button { width : 100 % ; margin - top : 12 px ; color : # fff ; background : # 135 ee8 }. generated - card { display : flex ; align - items : center ; justify - content : space - between ; margin - top : 12 px ; padding : 10 px ; border - radius : 9 px ; background : # edf5ff }. generated - card button ,. bind - panel > button { width : auto ; margin : 0 ; color : # 135 ee8 ; background : # fff }. bind - panel { margin - top : 10 px }. bound - state { margin - top : 10 px ; padding : 9 px ; border - radius : 8 px ; color : # 08705 f ; font - weight : 800 ; background : # dff5ed }
2026-10-11 21:48:44 +08:00
. generated - card ,. print - row { gap : 10 px }. generated - card > view ,. print - row > view { flex : 1 ; min - width : 0 ; overflow - wrap : anywhere }. generated - card button ,. print - row button { flex - shrink : 0 ; white - space : nowrap ; padding : 0 10 px }
2026-10-15 18:25:46 +08:00
. back - to - list { width : auto ; height : 30 px ; margin : 0 ; padding : 0 9 px ; border : 1 px solid # c8d8ec ; border - radius : 8 px ; color : # 135 ee8 ; font - size : 10 px ; line - height : 30 px ; background : # f8fbff }
2026-09-17 13:36:08 +08:00
. collapse - button { width : 30 px ; height : 28 px ; margin : 0 ; padding : 0 ; color : # 135 ee8 ; font - size : 16 px ; line - height : 28 px ; background : # edf5ff }
. receipt - comparison { display : grid ; grid - template - columns : repeat ( 3 , 1 fr ); gap : 7 px ; margin - bottom : 9 px }. receipt - comparison view { padding : 9 px ; border - radius : 9 px ; text - align : center ; background : # fff }. receipt - comparison strong ,. receipt - comparison text { display : block }. receipt - comparison strong { color : # 17314 f ; font - size : 15 px }. receipt - comparison text { margin - top : 2 px ; color : # 8390 a1 ; font - size : 8 px }. receipt - comparison . difference { border : 1 px solid # e3a044 ; background : # fff4df }. receipt - comparison . difference strong ,. receipt - comparison . difference text { color : # a35b0c ; font - weight : 800 }
2026-10-16 16:19:41 +08:00
. transfer - content . measure - hint { font - size : 11 px ; line - height : 17 px }. transfer - content . measure - title { font - size : 13 px }. transfer - content . summary - metrics text { font - size : 10 px }. transfer - content . transfer - primary { height : 46 px ; line - height : 46 px ; font - size : 14 px }. transfer - content . photo - card { padding : 14 px }. transfer - content . pallet - heading - copy > text : last - child { margin - top : 2 px ; color : # 8995 a5 ; font - size : 8 px }. transfer - content . summary - no { font - size : 13 px }. transfer - content . batch - summary { font - size : 12 px }
/* 测量任务默认展开,收起后仍保留单号和实收箱数,方便现场核对。 */
. order - summary { position : relative ; margin - bottom : 10 px ; border - radius : 12 px ; cursor : pointer ; padding : 12 px ; gap : 8 px }
. order - summary > view : first - child { flex : 1 ; min - width : 0 }. summary - toggle { flex : none ; font - size : 11 px ; white - space : nowrap ; opacity : .9 }. transfer - content . summary - no { font - size : 12 px ; line - height : 18 px ; word - break : break - all }. summary - count strong { font - size : 20 px }
. receipt - comparison { margin - top : 0 }. summary - metrics { box - shadow : none }
/* 托盘卡片按身份、实测指标、业务码分层,所有信息沿左侧基线排列。 */
. transfer - content . pallet - card { padding : 12 px ; border - radius : 12 px ; border - color : # dce5f0 ; box - shadow : 0 2 px 8 px rgba ( 32 , 67 , 111 , .04 ); text - align : left }
. transfer - content . pallet - card . active - receiving - pallet { border - color : # 91 b7ef ! important }
. pallet - card . pallet - head { display : flex ; align - items : center ; gap : 9 px ; margin : 0 ; padding : 0 0 10 px ; border - bottom : 0 ; text - align : left }
. pallet - card . pallet - index { width : 30 px ; height : 30 px ; min - width : 30 px ; border - radius : 8 px ; font - size : 12 px ; box - shadow : none }
. pallet - card . pallet - heading - copy { flex : 1 ; min - width : 0 ; text - align : left }
. pallet - card . pallet - title - line { display : flex ; flex - direction : row ; align - items : center ; justify - content : flex - start ; gap : 8 px ; flex - wrap : wrap }
. pallet - card . pallet - title - line . pallet - name { margin : 0 ; font - size : 14 px ; font - weight : 750 ; color : # 18324 f }
. pallet - card . pallet - title - line . pallet - status { margin : 0 ; padding : 3 px 6 px ; border - radius : 5 px ; color : # a47320 ; background : # fff6e5 ; font - size : 10 px ; font - weight : 500 ; line - height : 15 px }
. pallet - card . pallet - title - line . pallet - status . is - bound { color : # 278365 ; background : # edf8f2 }
. pallet - card . remove - button ,. pallet - card . collapse - button { flex : none ; box - sizing : border - box ; width : 28 px ; height : 28 px ; margin : 0 ; padding : 0 ; line - height : 26 px ; border - radius : 7 px ; font - size : 18 px }
. pallet - card . remove - button { color : # c46868 ; background : # fff5f5 }. pallet - card . collapse - button { color : # 6180 a6 ; background : # f2f6fc }
. pallet - stats { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr )); gap : 8 px ; padding : 10 px ; border - radius : 8 px ; background : # f5f8fc }
. pallet - stats > view { min - width : 0 ; text - align : left }. pallet - stats strong { display : block ; color : # 213 b58 ; font - size : 15 px ; font - weight : 750 ; line - height : 21 px }. pallet - stats strong text { font - size : 10 px ; font - weight : 400 ; color : # 73859 b }. pallet - stats > view > text { display : block ; margin - top : 3 px ; color : # 8090 a3 ; font - size : 10 px }
. pallet - card . pallet - brief { display : flex ; align - items : center ; justify - content : space - between ; gap : 10 px ; margin : 10 px 0 0 ; padding : 0 ; flex - wrap : nowrap }
. pallet - business - code { flex : 1 ; min - width : 0 ; color : # 7 c8ca0 ; font - size : 9 px ; line - height : 15 px ; overflow - wrap : anywhere ; word - break : break - all ; text - align : left }
. receiving - badge { flex : none ; display : flex ; align - items : center ; gap : 5 px ; color : # 3273 c7 ; font - size : 10 px ; white - space : nowrap }. receiving - badge : before { content : '' ; width : 5 px ; height : 5 px ; border - radius : 50 % ; background : # 4184 e1 }
. pallet - card . pallet - brief button { flex : none ; width : auto ; height : 28 px ; line - height : 28 px ; margin : 0 ; padding : 0 8 px ; font - size : 10 px ; border - radius : 6 px ; background : # edf4ff ; color : # 246 ac6 }
. pallet - card . measure - section - head { margin - top : 12 px }. pallet - card . generated - card { margin - top : 10 px }
/* 箱号独占一行,操作按钮固定在标题右侧,长业务编号不会挤压按钮。 */
. transfer - content . carton - count - card { padding : 12 px ; margin : 12 px 0 ; border - color : # dce5f0 ; border - radius : 12 px ; background : # fff }
. carton - title - row { flex - wrap : nowrap ; align - items : center ; justify - content : space - between ; gap : 8 px }. carton - title - row > . measure - title { min - width : 0 ; font - size : 14 px ! important }. carton - icon - actions { display : flex ; flex : none ; gap : 6 px }
. carton - icon - actions button { box - sizing : border - box ; width : 30 px ; height : 30 px ; padding : 0 ; margin : 0 ; line - height : 28 px ; font - size : 20 px ; border - radius : 8 px }
. carton - business - code { display : block ; margin : 5 px 0 10 px ; color : # 7 c8da2 ; font - size : 10 px ; line - height : 15 px ; overflow - wrap : anywhere ; word - break : break - all }
. carton - count - card . batch - sku { padding : 9 px 10 px ; margin : 8 px 0 ; font - size : 11 px ; background : # f7faff ; border - color : # dce5f0 }. carton - count - card . batch - actions : empty { display : none }
2026-09-17 13:36:08 +08:00
</ style >