2026-09-17 13:36:08 +08:00
< template >
< view v-if = "user" class="page-shell portal-page transfer-page" >
2026-09-24 08:55:38 +08:00
< PortalHeader : title = "entryMode === 'list' ? '仓储转运测量列表' : '转运测量作业'" :user = "user" fallback -url ="/ pages / module / module ? key = inbound " />
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-09-17 13:36:08 +08:00
< view class = "transfer-heading" >
2026-10-15 15:45:32 +08:00
< view >< text class = "transfer-title" >{{ entryMode === 'list' ? '仓储转运测量列表' : '转运测量作业' }}</ text >< text class = "transfer-subtitle" >{{ entryMode === 'list' ? '选择待测转运单后进入作业详情' : '先创建托盘号,再逐箱测量清点并分配入托' }}</ text ></ view >
2026-09-17 13:36:08 +08:00
< button class = "refresh-button" :disabled = "loading" aria -label =" 刷新待测转运单 " @tap ="loadCandidates" >< uni-icons type = "refresh" color = "#135ee8" size = "18" />< text >{{ loading ? '加载中' : '刷新' }}</ text ></ button >
</ view >
2026-09-24 08:55:38 +08:00
< template v-if = "!selected && entryMode === 'list'" >
2026-09-17 13:36:08 +08:00
< view class = "section-heading" >< text > 待测转运单 </ text >< text >{{ candidates . length }} 单 </ text ></ view >
< view v-if = "!candidates.length && !loading" class="transfer-empty"><text class="empty-title" > 暂无待测量任务 </ text >< text > 新任务进入后可点击右上角刷新 </ text ></ view >
< view class = "candidate-list" >
< button v-for = "item in candidates" :key="item.transferNo" class="transfer-card" :class="{ selected: selected?.transferNo === item.transferNo }" @tap="select(item)" >
2026-09-24 08:55:38 +08:00
< view class = "transfer-card-head" >< text class = "transfer-no" >{{ item . transferNo }}</ text >< text class = "status-pill" >{{ statusLabel ( item . status , item . transferNo ) }}</ text ></ view >
2026-09-22 17:27:56 +08:00
< view class = "task-metrics" >< view >< text > 计划箱数 </ text >< strong >{{ item . packageCount || 0 }}</ strong ></ view >< view >< text >{{ item . destinationType === 'PRIVATE' ? '目的仓' : 'FBA 目的仓' }}</ text >< strong >{{ destinationLabel ( item ) }}</ strong ></ view >< view >< text > 换标 </ text >< strong >{{ item . needRelabel ? '需要' : '不需要' }}</ strong ></ view ></ view >
2026-09-17 13:36:08 +08:00
< text class = "transfer-meta" > 入仓码 {{ item . inboundCode || '—' }}</ text >
</ button >
</ view >
2026-09-24 08:55:38 +08:00
</ template >
< 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 08:55:38 +08:00
< view class = "operation-detail-heading" >< button class = "back-to-list" @tap ="backToCandidates" > ‹ 返回待测列表 </ button >< view >< text class = "operation-detail-title" > 作业详情 </ text >< text class = "operation-detail-subtitle" > 进入后完成当前转运单的测量 、 打托和拍照 </ text ></ view ></ view >
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-09-17 13:36:08 +08:00
< view class = "order-summary" >
< 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-09-17 13:36:08 +08:00
</ view >
2026-10-15 15:45:32 +08:00
< view class = "receipt-comparison" >< view >< strong >{{ selected . packageCount || 0 }}</ strong >< text > 客户预报 </ text ></ view >< view >< strong >{{ cartonsCounted ? actualReceivedCartons : '—' }}</ strong >< text >{{ cartonsCounted ? '当前实收' : '清点后确认' }}</ text ></ view >< view : class = "{ difference: cartonsCounted && cartonDifference !== 0 }" >< strong >{{ ! cartonsCounted ? '—' : cartonDifference > 0 ? `+ ${ cartonDifference } ` : cartonDifference }}</ strong >< text >{{ ! cartonsCounted ? '差异待确认' : cartonDifference === 0 ? '无差异' : cartonDifference > 0 ? '超收差异' : '短收差异' }}</ text ></ view ></ view >
< view 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 class = "workflow-step" >< text class = "step-number" > 1 </ text >< view >< text class = "step-title" > 创建签收托盘 </ text >< text class = "step-caption" > 支持多托盘 , 先生成标签 , 再分配箱子 </ text ></ view ></ view >
< 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-15 15:45:32 +08:00
< view class = "pallet-heading-copy" >< text > 托盘 {{ index + 1 }} · {{ pallet . bound ? '已绑定' : pallet . generated ? '待绑定' : '待创建' }}</ text >< text >{{ pallet . collapsed && pallet . palletNo ? pallet . palletNo : '先创建托盘号,再录入并分配签收箱子' }}</ text ></ view >
2026-09-17 13:36:08 +08:00
< button v-if = "pallets.length > 1 && !pallet.generated" class="remove-button" @tap.stop="removePallet(index)" > 移除 </ button >
< button v-if = "pallet.generated" class="collapse-button" :aria-label="pallet.collapsed ? '展开托盘明细' : '折叠托盘明细'" @tap.stop="pallet.collapsed = !pallet.collapsed" >{{ pallet.collapsed ? '⌄' : '⌃' }}</ button >
</ view >
2026-10-15 15:45:32 +08:00
< view v-if = "pallet.generated" class="pallet-brief"><text>{{ palletSummary(pallet).cartonCount }} 箱</text><text>{{ palletVolume(pallet) }} m³</text><text>{{ palletSummary(pallet).weight.toFixed(2) }} kg</text><button :disabled="cartonsCounted || countingCartons" @tap="useReceivingPallet(pallet)" >{{ String ( receivingPalletId ) === String ( pallet.draftId ) ? ' 当前签收托盘 ' : ' 在此托盘签收 ' }}</ button ></ view >
2026-09-17 13:36:08 +08:00
< view v-show = "!pallet.collapsed" >
2026-10-15 15:45:32 +08:00
< view class = "measure-section-head" >< view >< text class = "measure-title" > 本托箱子汇总 </ text >< text class = "measure-hint" > 由分配到本托的实收箱自动汇总 , 无需填写托盘尺寸和重量 </ text ></ view ></ view >
< 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-09-22 17:27:56 +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 >
< view v-if = "!generatedPallets.length" class="transfer-validation"><view class="workflow-step"><text class="step-number">2</text><view><text class="step-title">填写签收箱子</text><text class="step-caption" > 请先生成托盘号 , 再填写签收箱子 。 </ text ></ view ></ view ></ view >
< view v-if = "generatedPallets.length" class="photo-card" >
< view id = "transfer-boxes" class = "workflow-step" >< text class = "step-number" > 2 </ text >< view >< text class = "step-title" > 填写签收箱子 </ text >< text class = "step-caption" > 相同箱规批量填 , 差异箱单独改 </ text ></ view ></ view >
< 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 >
< text class = "measure-hint" > 扫描本单箱号定位 ; 填写每箱尺寸 、 重量和箱内实点件数 。 申报未提供件数 , 按现场实点记录 。 </ text >
< ScanTargetInput name = "transfer-carton" v-model = "cartonScan" label="箱号" placeholder="扫描系统箱号或客户箱唛" :active="!cartonsCounted" @activate="activePalletKey = ''" @submit="locateCarton" />
< text class = "measure-title" >{{ receivedCartonBoxes . length }} 箱 · {{ cartonVolume }} m³ · {{ cartonWeight }} kg · {{ cartonQuantity }} 件 </ text >
< 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 >
< 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[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 >
< 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 >
< CartonCountFields :box = "batchBox" :sku-codes = "[batchSku]" :allow-sku-change = "false" :disabled = "countingCartons" />
< 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 >
< 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-selected': box.cartonMark === selectedCartonMark }" >
< text class = "measure-title" > 第 {{ index + 1 }} 箱 · {{ box . cartonMark || '新增实收箱(确认后生成箱号)' }}</ text >
< 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 >
< text v-if = "box.customerCartonMark" class="measure-hint" > 客户箱唛 : {{ box.customerCartonMark }}</ text >
< text class = "measure-hint" > SKU 与该箱申报一致 , 数量为箱内件数 </ text >
< 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 >
< button v-if = "!cartonsCounted" class="refresh-button" :disabled="countingCartons" @tap="box.received = box.received === false" >{{ box.received === false ? ' 标记为已收到 ' : ' 标记为未收到 ' }}</ button >
< text v-if = "box.received === false" class="measure-hint" > 本箱未收到 , 不计入实收 </ text >
< picker v-if = "!box.cartonMark && !cartonsCounted" :range="selected.items.map(item => item.skuCode)" @change="selectExtraCartonSku(box, Number($event.detail.value))" >< text > 选择 SKU : {{ box . items [ 0 ]. skuCode }} ▾ </ text ></ picker >
< CartonCountFields :box = "box" : sku -codes =" box.items.map ( line = > line . skuCode ) " :allow-sku-change=" false " :disabled=" cartonsCounted || countingCartons || box . received === false " />
</view>
<button v-if=" cartonsCounted " class=" add - pallet - button " @tap=" printCartonLabels ">打开箱标签打印</button>
<button v-if=" ! cartonsCounted " class=" add - pallet - button " :disabled=" countingCartons " @tap=" addExtraCarton ">新增实收箱</button>
<label v-if=" receivedCartonBoxes . length !== Number ( selected . packageCount ) "><text class=" measure - hint ">差异原因(必填)</text><textarea v-model=" countDiscrepancyReason " :disabled=" cartonsCounted || countingCartons " maxlength=" 1000 " placeholder=" 填写短收或超收原因 " /></label>
<button class=" transfer - primary " :disabled=" ! cartonCountReady || countingCartons || cartonsCounted " @tap=" confirmCartons ">{{ cartonsCounted ? '全部箱子已清点' : countingCartons ? '保存中…' : '确认逐箱测量清点' }}</button>
</view>
2026-09-17 13:36:08 +08:00
<view class=" photo - card ">
2026-10-15 15:45:32 +08:00
<view class=" workflow - step "><text class=" step - number ">3</text><view><text class=" step - title ">拍照并确认入库</text><text class=" step - caption ">核对全部托盘与签收箱子</text></view></view>
2026-09-17 13:36:08 +08:00
<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=" # 135 ee8 " 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>
<view v-if=" error " class=" transfer - error " role=" alert "><text class=" feedback - title ">操作失败</text><text>{{ error.message || error }}</text><text v-if=" error . requestId ">requestId: {{ error.requestId }}</text></view>
<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>
<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-15 15:45:32 +08:00
import { getWarehouseTransferCartonLabel , countWarehouseTransferCartons , 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 },
data () { return { receivingPalletId : '' , cartonPalletFilter : '' , batchPalletId : '' , batchCountOpen : false , batchSku : '' , batchBox : newBatchBox (), batchSelectedIndexes : [], batchCountMessage : '' , cartonScrollTarget : '' , cartonBoxes : [], countDiscrepancyReason : '' , cartonScan : '' , selectedCartonMark : '' , 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-15 15:45:32 +08:00
generatedPallets () { return this . pallets . filter ( pallet => pallet . generated && pallet . draftId ) },
batchSkuCodes () { return [... new Set ( this . receivedCartonBoxes . map ( box => box . items [ 0 ] ? . skuCode ). filter ( Boolean ))] },
batchTargetBoxes () { return this . cartonBoxes . filter (( box , index ) => this . batchSelectedIndexes . includes ( index ) && box . received !== false && box . items [ 0 ] ? . skuCode === this . batchSku ) },
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 )) },
actualReceivedCartons () { return this . cartonsCounted ? this . receivedCartonBoxes . length : 0 },
cartonsCounted () { return Boolean ( this . selected ? . cartons ? . length ) && this . selected . cartons . every ( box => box . countedAt ) },
cartonVolume () { return this . receivedCartonBoxes . reduce (( sum , box ) => sum + ( Number ( box . length ) || 0 ) * ( Number ( box . width ) || 0 ) * ( Number ( box . height ) || 0 ) / 1000000 , 0 ). toFixed ( 6 ) },
cartonWeight () { return this . receivedCartonBoxes . reduce (( sum , box ) => sum + ( Number ( box . weight ) || 0 ), 0 ). toFixed ( 2 ) },
cartonQuantity () { return this . receivedCartonBoxes . reduce (( sum , box ) => sum + ( Number ( box . items [ 0 ] ? . quantity ) || 0 ), 0 ) },
receivedCartonBoxes () { return this . cartonBoxes . filter ( box => box . received !== false ) },
cartonCountReady () { return this . receivedCartonBoxes . length > 0 && this . pallets . every ( pallet => pallet . generated && this . palletSummary ( pallet ). cartonCount > 0 ) && ( this . receivedCartonBoxes . length === Number ( this . selected ? . packageCount ) || Boolean ( this . countDiscrepancyReason . trim ())) && 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 ) && box . items [ 0 ] ? . skuCode && Number . isSafeInteger ( Number ( box . items [ 0 ]. quantity )) && Number ( box . items [ 0 ]. quantity ) > 0 && Number ( box . items [ 0 ]. quantity ) <= 2147483647 ) },
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-15 15:45:32 +08:00
totalVolume () { return this . pallets . reduce (( sum , pallet ) => sum + this . rawPalletVolume ( pallet ), 0 ). toFixed ( 6 ) },
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 () },
cartonBoxes : { deep : true , handler () { if ( this . selected && ! this . cartonsCounted ) uni . setStorageSync ( this . cartonDraftKey (), { boxes : this . cartonBoxes , discrepancyReason : this . countDiscrepancyReason , key : this . cartonRequestKey , signature : this . cartonSignature }) } },
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-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 );
},
validCartonValues ( box ) { return Boolean ( box && [ box . length , box . width , box . height , box . weight ]. every ( value => Number ( value ) >= 0.01 && Number . isFinite ( Number ( value ))) && Number . isSafeInteger ( Number ( box . items [ 0 ] ? . quantity )) && Number ( box . items [ 0 ]. quantity ) > 0 && Number ( box . items [ 0 ]. quantity ) <= 2147483647 ) },
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 ;
this . batchSelectedIndexes = selected ? this . cartonBoxes . flatMap (( box , index ) => box . received !== false && box . items [ 0 ] ? . skuCode === this . batchSku ? [ index ] : []) : [];
this . batchCountMessage = '' ;
},
toggleBatchCarton ( index ) {
if ( this . countingCartons || this . cartonsCounted ) return ;
const box = this . cartonBoxes [ index ];
if ( ! box || box . received === false || box . items [ 0 ] ? . skuCode !== this . batchSku ) return ;
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 ];
target . items [ 0 ]. quantity = source . items [ 0 ]. quantity ;
},
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 ) {
const sku = this . cartonBoxes [ index ] ? . items [ 0 ] ? . skuCode ;
return this . cartonBoxes . slice ( 0 , index ). reverse (). find ( box => box . received !== false && box . items [ 0 ] ? . skuCode === sku );
},
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 ();
},
saveCartonDraft () { if ( this . selected && ! this . cartonsCounted ) uni . setStorageSync ( this . cartonDraftKey (), { boxes : this . cartonBoxes , discrepancyReason : this . countDiscrepancyReason , key : this . cartonRequestKey , signature : this . cartonSignature }) },
cartonDraftKey () { return `transfer-carton-count: ${ session . apiBase } : ${ this . user ? . id } : ${ this . user ? . warehouseId } : ${ this . selected ? . transferNo } ` },
restoreCartons () {
const rows = ( this . selected ? . cartons || []). map ( box => ({ ... box , transferPalletId : box . transferPalletId || ( this . generatedPallets . length === 1 ? this . generatedPallets [ 0 ]. draftId : '' ), received : box . status !== 'NOT_RECEIVED' , items : [{ skuCode : this . selected . items . find ( item => String ( item . id ) === String ( box . transferItemId )) ? . skuCode || '' , quantity : box . quantity ?? '' }] }));
const draft = uni . getStorageSync ( this . cartonDraftKey ());
this . countDiscrepancyReason = this . selected ? . cartons ? . find ( box => box . discrepancyReason ) ? . discrepancyReason || '' ; this . cartonRequestKey = '' ; this . cartonSignature = '' ; this . selectedCartonMark = '' ; this . cartonScan = '' ;
if ( ! this . cartonsCounted && draft ? . boxes ? . length >= rows . length && draft . boxes . every ( box => box . cartonMark || this . selected . items . some ( item => String ( item . id ) === String ( box . transferItemId ))) && rows . every ( box => draft . boxes . some ( row => row . cartonMark === box . cartonMark && row . items ? .[ 0 ] ? . skuCode === box . items [ 0 ]. skuCode ))) {
this . cartonBoxes = draft . boxes ; this . countDiscrepancyReason = draft . discrepancyReason || '' ; this . cartonRequestKey = draft . key || '' ; this . cartonSignature = draft . signature || '' ;
} else this . cartonBoxes = rows ;
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
},
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 : '' }] });
},
selectExtraCartonSku ( box , index ) { const item = this . selected . items [ index ]; if ( item ) { box . transferItemId = item . id ; box . items [ 0 ]. skuCode = item . skuCode } },
locateCarton ( value ) {
const code = String ( typeof value === 'string' ? value : this . cartonScan ). trim ();
const matches = this . cartonBoxes . filter ( box => box . cartonMark === code || box . customerCartonMark === code );
if ( matches . length !== 1 ) { this . error = new Error ( matches . length ? '客户箱唛对应多箱,请扫描系统箱号' : '该箱不属于当前转运单' ); return }
this . error = null ; this . cartonPalletFilter = '' ; this . selectedCartonMark = matches [ 0 ]. cartonMark ; this . cartonScan = '' ;
this . cartonScrollTarget = `transfer-box- ${ this . cartonBoxes . indexOf ( matches [ 0 ]) } ` ;
},
async confirmCartons () {
if ( ! this . cartonCountReady || this . countingCartons || this . cartonsCounted ) return ;
this . countingCartons = true ; this . error = null ;
try {
const boxes = this . receivedCartonBoxes . map ( box => ({ cartonMark : box . cartonMark || null , transferItemId : box . transferItemId , transferPalletId : box . transferPalletId , length : Number ( box . length ), width : Number ( box . width ), height : Number ( box . height ), weight : Number ( box . weight ), skuCode : box . items [ 0 ]. skuCode , quantity : Number ( box . items [ 0 ]. quantity ) }));
const signature = JSON . stringify ({ boxes , discrepancyReason : this . countDiscrepancyReason });
if ( ! this . cartonRequestKey || signature !== this . cartonSignature ) { this . cartonRequestKey = `transfer-count- ${ Date . now () } - ${ Math . random (). toString ( 36 ). slice ( 2 ) } ` ; this . cartonSignature = signature }
uni . setStorageSync ( this . cartonDraftKey (), { boxes : this . cartonBoxes , discrepancyReason : this . countDiscrepancyReason , key : this . cartonRequestKey , signature });
const detail = await countWarehouseTransferCartons ( this . selected . transferNo , { idempotencyKey : this . cartonRequestKey , boxes , discrepancyReason : this . countDiscrepancyReason });
if ( ! detail . cartons ? . length || ! detail . cartons . every ( box => box . countedAt )) throw new Error ( '清点结果未确认,请刷新核实' );
uni . removeStorageSync ( this . cartonDraftKey ()); this . selected = detail ; this . pallets = detail . pallets . map ( restoredPallet ); this . restoreCartons ();
} 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-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 15:45:32 +08:00
handlePalletScan ( value ) { if ( ! this . activePalletKey && this . selected && ! this . cartonsCounted ) { this . locateCarton ( value ); return }; 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 ) };
},
rawPalletVolume ( pallet ) { return this . palletSummary ( pallet ). volume },
palletVolume ( pallet ) { return this . rawPalletVolume ( pallet ). toFixed ( 6 ) },
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-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-15 15:45:32 +08:00
try { this . selected = await getWarehouseTransferDetail ( item . transferNo ); 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-09-17 13:36:08 +08:00
async removePallet ( index ) { const pallet = this . pallets [ index ]; if ( ! pallet || pallet . generated || pallet . busy ) return ; pallet . busy = true ; try { if ( pallet . draftId ) await deleteWarehouseTransferPallet ( this . selected . transferNo , pallet . draftId ); this . pallets . splice ( index , 1 ); this . pendingKey = '' } catch ( error ) { pallet . busy = false ; this . error = error } },
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 {
if ( ! this . pendingKey ) this . pendingKey = `pda-transfer- ${ this . selected . transferNo } - ${ Date . now () } - ${ Math . random (). toString ( 36 ). slice ( 2 , 10 ) } `
const payload = { idempotencyKey : this . pendingKey , photoFileIds : [... this . photoFileIds ] }
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-15 15:45:32 +08:00
. workflow - step { display : flex ; align - items : center ; gap : 10 px ; margin : 18 px 0 12 px }. step - number { display : flex ; align - items : center ; justify - content : center ; flex : none ; width : 28 px ; height : 28 px ; border - radius : 9 px ; background : # e5efff ; color : # 135 ee8 ; font - weight : 800 ; font - size : 14 px }. step - title ,. step - caption { display : block }. step - title { font - size : 16 px ; font - weight : 800 ; color : # 17314 f }. step - caption { margin - top : 3 px ; font - size : 11 px ; color : # 7 a899c ; line - height : 17 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 }. photo - card > . workflow - step { margin - top : 0 }
. 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 }
. carton - count - card { padding : 14 px 10 px ; margin : 10 px 0 ; border : 1 px solid # d8e3f1 ; border - radius : 12 px }. carton - selected { border - color : # 135 ee8 ; background : # f0f6ff }
2026-09-17 13:36:08 +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 - heading { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 12 px ; margin : 4 px 2 px 17 px }. transfer - title ,. transfer - subtitle ,. 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 }. transfer - title { color : # 14263 d ; font - size : 22 px ; font - weight : 850 ; letter - spacing : .2 px }. transfer - subtitle { margin - top : 4 px ; color : # 718095 ; font - size : 11 px ; line - height : 17 px }. 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 }. refresh - button [ disabled ]{ opacity : .55 }. section - heading { display : flex ; align - items : center ; justify - content : space - between ; margin : 0 2 px 8 px ; color : # 243951 ; font - size : 12 px ; font - weight : 800 }. section - heading text : last - child { color : # 8290 a2 ; font - size : 10 px ; font - weight : 600 }. candidate - list { display : flex ; flex - direction : column ; gap : 9 px ; margin - bottom : 16 px }. transfer - card { box - sizing : border - box ; width : 100 % ; margin : 0 ; padding : 13 px ; border : 1 px solid # dce5f0 ; border - radius : 13 px ; text - align : left ; background : # fff ; box - shadow : 0 4 px 15 px rgba ( 32 , 67 , 111 , .055 )}. transfer - card . selected { border - color : # 6 da3f1 ; background : # f7faff ; box - shadow : 0 5 px 18 px rgba ( 19 , 94 , 232 , .11 )}. transfer - card - head { display : flex ; align - items : center ; justify - content : space - between ; gap : 10 px }. transfer - no { overflow : hidden ; color : # 18324 f ; font - size : 14 px ; font - weight : 850 ; text - overflow : ellipsis ; white - space : nowrap }. status - pill { flex - shrink : 0 ; padding : 4 px 7 px ; border - radius : 6 px ; color : # 135 ee8 ; font - size : 9 px ; font - weight : 750 ; background : # eaf2ff }. task - metrics { display : grid ; grid - template - columns : .75 fr 1.25 fr .75 fr ; gap : 6 px ; margin - top : 11 px }. task - metrics view { min - width : 0 ; padding : 7 px 8 px ; border - radius : 8 px ; background : # f4f7fa }. task - metrics text { display : block ; color : # 8492 a4 ; font - size : 8 px }. task - metrics strong { display : block ; overflow : hidden ; margin - top : 2 px ; color : # 2 c4058 ; font - size : 11 px ; text - overflow : ellipsis ; white - space : nowrap }. transfer - meta { margin - top : 8 px ; color : # 7 b899b ; font - size : 9 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 - 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 r
. 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-09-24 08:55:38 +08:00
. operation - detail - heading { display : flex ; align - items : flex - start ; gap : 10 px ; margin : 0 2 px 12 px ; padding : 2 px 0 }. 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 }. operation - detail - heading > view { min - width : 0 }. operation - detail - title { display : block ; color : # 243951 ; font - size : 15 px ; font - weight : 850 }. operation - detail - subtitle { display : block ; margin - top : 3 px ; color : # 8794 a4 ; font - size : 9 px ; line - height : 14 px }
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-15 15:45:32 +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 { font - size : 10 px ; line - height : 16 px }. transfer - content . summary - no { font - size : 13 px }. transfer - content . batch - summary { font - size : 12 px }
2026-09-17 13:36:08 +08:00
</ style >