669 lines
82 KiB
Vue
669 lines
82 KiB
Vue
<template>
|
||
<view v-if="user" class="page-shell portal-page transfer-page" :class="{ 'transfer-list-page': entryMode === 'list' }">
|
||
<PortalHeader :title="entryMode === 'list' ? '仓储转运测量' : '转运测量作业'" :user="user" />
|
||
<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>
|
||
<button class="refresh-button" :disabled="loading" aria-label="刷新当前转运单" @tap="refreshCurrentTransfer"><uni-icons type="refresh" color="#135ee8" size="18" /><text>{{ loading ? '加载中' : '刷新' }}</text></button>
|
||
</view>
|
||
<scroll-view scroll-y :scroll-into-view="cartonScrollTarget" scroll-with-animation class="transfer-scroll"><view class="portal-content transfer-content">
|
||
<view v-if="entryMode === 'list'" class="transfer-heading">
|
||
<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>
|
||
</view>
|
||
|
||
<template v-if="!selected && entryMode === 'list'">
|
||
<view v-if="loading && !candidates.length" class="transfer-empty"><uni-load-more status="loading" /><text>正在加载待测转运单…</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" :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>
|
||
</button>
|
||
</view>
|
||
</template>
|
||
<view v-if="!selected && entryMode === 'work' && !detailLoading" class="transfer-empty"><text class="empty-title">暂无进行中的转运测量作业</text><text>请先从“仓储转运测量”选择待测转运单。</text></view>
|
||
|
||
<view v-if="detailLoading" class="transfer-empty"><text class="empty-title">正在读取转运明细</text><text>请稍候,不要重复选择</text></view>
|
||
<template v-if="selected && !detailLoading">
|
||
<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">
|
||
<view class="order-summary" @tap="summaryCollapsed = !summaryCollapsed" role="button" :aria-expanded="!summaryCollapsed" aria-label="展开或收起任务汇总">
|
||
<view><text class="summary-eyebrow">当前测量任务</text><text class="summary-no">{{ selected.transferNo }}</text></view>
|
||
<view class="summary-count"><strong>{{ cartonsCounted ? actualReceivedCartons : receivedCartonBoxes.length }}</strong><text>{{ cartonsCounted ? '实收箱' : '待确认箱' }}</text></view>
|
||
<text class="summary-toggle">{{ summaryCollapsed ? '展开 ⌄' : '收起 ⌃' }}</text>
|
||
</view>
|
||
<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>
|
||
|
||
<view v-for="(pallet, index) in pallets" :key="pallet.key" class="pallet-card" :class="{ 'active-receiving-pallet': String(receivingPalletId) === String(pallet.draftId) }">
|
||
<view class="pallet-head">
|
||
<view class="pallet-index"><text>{{ String(index + 1).padStart(2, '0') }}</text></view>
|
||
<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>
|
||
<button v-if="pallet.generated" class="collapse-button" :aria-label="pallet.collapsed ? '展开托盘明细' : '折叠托盘明细'" @tap.stop="pallet.collapsed = !pallet.collapsed">{{ pallet.collapsed ? '⌄' : '⌃' }}</button>
|
||
</view>
|
||
<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>
|
||
<view v-show="!pallet.collapsed">
|
||
<view class="measure-section-head"><view><text class="measure-title">本托箱子汇总</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>
|
||
<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>
|
||
<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>
|
||
|
||
<button class="add-pallet-button" :disabled="cartonsCounted || countingCartons" @tap="addPallet"><uni-icons type="plus" color="#135ee8" size="18" /><text>新增一个托盘</text></button>
|
||
<text v-if="!generatedPallets.length" class="carton-prerequisite">请先生成托盘号,再填写签收箱子。</text>
|
||
<view v-if="generatedPallets.length" class="photo-card">
|
||
<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>
|
||
<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>
|
||
<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>
|
||
<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" volume-unit="m3" :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-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>
|
||
<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>
|
||
<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>
|
||
<button class="confirm-carton-button" :disabled="countingCartons || submitting || !validCartonValues(box) || !generatedPallets.some(pallet => String(pallet.draftId) === String(box.transferPalletId))" @tap="confirmSingleCarton(box)">确认本箱</button>
|
||
</template>
|
||
</view>
|
||
<button v-if="cartonsCounted" class="add-pallet-button" @tap="printCartonLabels">打印唛头</button>
|
||
<button v-if="restorableCartons.length" class="add-pallet-button" :disabled="countingCartons || submitting || deletingCarton" @tap="restoreDeletedCarton">恢复已删除箱子({{ restorableCartons.length }})</button>
|
||
<button class="transfer-primary" :disabled="!allCartonsConfirmed || !cartonCountReady || countingCartons || cartonsCounted" @tap="confirmCartons">{{ cartonsCounted ? '全部箱子已清点' : countingCartons ? '保存中…' : '保存全部清点结果' }}</button>
|
||
</view>
|
||
<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>
|
||
<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>
|
||
</template>
|
||
</template>
|
||
|
||
<view v-if="error" class="transfer-error" role="alert"><text class="feedback-title">操作失败</text><text>{{ simpleErrorMessage(error) }}</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">
|
||
<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>
|
||
</view>
|
||
</view></scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import CartonCountFields from '@/components/CartonCountFields.vue'
|
||
import PortalHeader from '@/components/PortalHeader.vue'
|
||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||
import { session, requireSession } from '@/stores/session.js'
|
||
import { restoreWarehouseWork, resetWarehouseDomain, updateWarehouseWork, warehouseWork } from '@/stores/warehouse-work.js'
|
||
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
|
||
import { getWarehouseTransferCartonLabel, countWarehouseTransferCartons, reopenWarehouseTransferMeasurement, bindWarehouseTransferPallet, deleteWarehouseTransferPallet, downloadWarehouseTransferMeasurementPhoto, downloadWarehouseTransferPalletLabel, generateWarehouseTransferPallet, getWarehouseTransferDetail, listWarehouseTransferCandidates, listWarehouseTransferMeasurementPhotos, palletizeWarehouseTransfer, saveWarehouseTransferPalletDraft, uploadWarehouseTransferMeasurementPhoto } from '@/api/warehouse-transfers.js'
|
||
|
||
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 })
|
||
const MAX_MEASUREMENT_PHOTOS = 20
|
||
const newBatchBox = () => ({ length: '', width: '', height: '', weight: '', items: [{ skuCode: '', quantity: '' }] })
|
||
|
||
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,
|
||
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,
|
||
draftId: value.draftId || value.id, version: value.version
|
||
})
|
||
|
||
export default {
|
||
components: { PortalHeader, ScanTargetInput, CartonCountFields },
|
||
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: '' } },
|
||
computed: {
|
||
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()];
|
||
},
|
||
generatedPallets() { return this.pallets.filter(pallet => pallet.generated && pallet.draftId) },
|
||
declaredSkuCodes() { return [...new Set((this.selected?.items || []).map(item => item.skuCode))] },
|
||
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.length === 1 && 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)) },
|
||
// 确认快照与当前内容一致,才允许保存整单清点结果。
|
||
// 按 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)) },
|
||
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(3) },
|
||
cartonWeight() { return this.receivedCartonBoxes.reduce((sum, box) => sum + (Number(box.weight) || 0), 0).toFixed(2) },
|
||
cartonQuantity() { return this.receivedCartonBoxes.reduce((sum, box) => sum + box.items.reduce((total, line) => total + (Number(line.quantity) || 0), 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.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)) },
|
||
completedDestinationLabel() { return this.destinationLabel({ fbaWarehouseCode: this.completedDestination, destinationType: this.completedDestinationType }) },
|
||
totalCartons() { return this.pallets.reduce((sum, pallet) => sum + this.palletSummary(pallet).cartonCount, 0) },
|
||
cartonDifference() { return this.actualReceivedCartons - Number(this.selected?.packageCount || 0) },
|
||
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))) },
|
||
totalVolume() { return this.pallets.reduce((sum, pallet) => sum + this.rawPalletVolume(pallet), 0).toFixed(3) },
|
||
totalWeight() { return this.pallets.reduce((sum, pallet) => sum + this.palletSummary(pallet).weight, 0).toFixed(2) },
|
||
validationMessage() {
|
||
if (this.isInventoryTransfer) return ''
|
||
if (!this.cartonsCounted) return '请先完成全部箱子的尺寸测量和箱内清点'
|
||
if (this.totalCartons !== this.receivedCartonBoxes.length) return '打托箱数必须与已清点实收箱数一致'
|
||
if (!this.selected || !this.pallets.length) return '转运单缺少可测量的托盘'
|
||
if (this.pallets.some(pallet => this.palletSummary(pallet).cartonCount === 0)) return '每个托盘必须至少分配一个实收箱'
|
||
if (this.pallets.some((pallet) => !pallet.bound)) return '请先生成并扫描确认绑定全部托盘'
|
||
if (this.photoFileIds.length > MAX_MEASUREMENT_PHOTOS) return `测量现场照片最多 ${MAX_MEASUREMENT_PHOTOS} 张,请移除多余照片`
|
||
if (!this.photoFileIds.length) return '请至少拍摄 1 张测量现场照片'
|
||
return ''
|
||
},
|
||
canSubmit() { return !this.validationMessage }
|
||
},
|
||
watch: {
|
||
countDiscrepancyReason() { this.saveCartonDraft() },
|
||
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 }) } },
|
||
pallets: { deep: true, handler() { if (!this.submitting) this.pendingKey = '' } }
|
||
},
|
||
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) } } }) } },
|
||
onLoad(options) { this.entryMode = options?.mode === 'list' ? 'list' : 'work'; this.pendingTaskNo = String(options?.task || '') },
|
||
onHide() { this.stopScanner() },
|
||
beforeUnmount() { this.stopScanner() },
|
||
methods: {
|
||
simpleErrorMessage(error) { return String(error?.message || error || '操作失败,请重试').replace(/[((]\s*(?:错误码|code|requestId|traceId)\s*[::=][\s\S]*$/i, '').replace(/(?:requestId|traceId)\s*[::=][\s\S]*$/i, '').trim() || '操作失败,请重试' },
|
||
// 快照只包含业务数据;复制、批量修改或改托后会自动失效并重新展开。
|
||
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)
|
||
uni.setStorageSync(this.cartonDraftKey(), { deletedBoxes: this.deletedCartonBoxes }); this.selected = detail; this.restoreCartons()
|
||
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 }
|
||
},
|
||
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))) && this.validCartonSkuLines(box)) },
|
||
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.length === 1 && 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.length !== 1 || 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 = source.items.map(line => ({ ...line }));
|
||
},
|
||
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) {
|
||
if (this.cartonBoxes[index]?.items.length !== 1) return null;
|
||
const sku = this.cartonBoxes[index]?.items[0]?.skuCode;
|
||
return this.cartonBoxes.slice(0, index).reverse().find(box => box.received !== false && box.items.length === 1 && 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, deletedBoxes: this.deletedCartonBoxes, forecastMarks: (this.selected.cartons || []).map(box => box.cartonMark), discrepancyReason: this.countDiscrepancyReason }) },
|
||
cartonDraftKey() { return `transfer-carton-count:${session.apiBase}:${this.user?.id}:${this.user?.warehouseId}:${this.selected?.transferNo}` },
|
||
restoreCartons() {
|
||
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 ?? '' }] }));
|
||
const draft = uni.getStorageSync(this.cartonDraftKey());
|
||
this.deletedCartonBoxes = Array.isArray(draft?.deletedBoxes) ? draft.deletedBoxes.filter(box => !box.cartonMark || (this.selected?.cartons || []).some(row => row.cartonMark === box.cartonMark)) : [];
|
||
this.countDiscrepancyReason = this.selected?.cartons?.find(box => box.discrepancyReason)?.discrepancyReason || ''; this.cartonRequestKey = ''; this.cartonSignature = '';
|
||
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))) {
|
||
const knownMarks = new Set(draft.forecastMarks || draft.boxes.map(box => box.cartonMark));
|
||
this.cartonBoxes = [...draft.boxes, ...rows.filter(box => !knownMarks.has(box.cartonMark))]; this.countDiscrepancyReason = draft.discrepancyReason || ''; this.cartonRequestKey = ''; this.cartonSignature = '';
|
||
} else this.cartonBoxes = rows;
|
||
this.cartonBoxes.forEach(box => { if (this.cartonsCounted || (box.status === 'COUNTED' && this.validCartonValues(box))) box.confirmedSignature = this.cartonConfirmationSignature(box) });
|
||
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
|
||
},
|
||
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 }
|
||
},
|
||
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: '' }] });
|
||
},
|
||
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) {
|
||
return box.items.length > 0 && box.items.length <= 200 && new Set(box.items.map(line => line.skuCode)).size === box.items.length
|
||
&& box.items.every(line => this.declaredSkuCodes.includes(line.skuCode) && Number.isSafeInteger(Number(line.quantity)) && Number(line.quantity) > 0 && Number(line.quantity) <= 2147483647);
|
||
},
|
||
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.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) })) }));
|
||
const signature = JSON.stringify({ boxes, discrepancyReason: this.countDiscrepancyReason, forecastPackageCount: Number(this.selected.packageCount) });
|
||
if (!this.cartonRequestKey || this.cartonSignature !== signature) {
|
||
this.cartonRequestKey = `transfer-count-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||
this.cartonSignature = signature
|
||
}
|
||
uni.setStorageSync(this.cartonDraftKey(), { boxes: this.cartonBoxes, deletedBoxes: this.deletedCartonBoxes, forecastMarks: (this.selected.cartons || []).map(box => box.cartonMark), discrepancyReason: this.countDiscrepancyReason });
|
||
const detail = await countWarehouseTransferCartons(this.selected.transferNo, { idempotencyKey: this.cartonRequestKey, boxes, discrepancyReason: this.countDiscrepancyReason, forecastPackageCount: Number(this.selected.packageCount) });
|
||
if (!detail.cartons?.length || !detail.cartons.every(box => box.countedAt)) throw new Error('清点结果未确认,请刷新核实');
|
||
uni.setStorageSync(this.cartonDraftKey(), { deletedBoxes: this.deletedCartonBoxes }); this.selected = detail; this.pallets = detail.pallets.map(restoredPallet); this.restoreCartons();
|
||
} catch (error) { this.error = error } finally { this.countingCartons = false }
|
||
},
|
||
openPalletOperations() { uni.navigateTo({ url: '/pages/pallet/pallet' }) },
|
||
destinationLabel(value, fallback = {}) { const code = String(value?.fbaWarehouseCode || fallback?.fbaWarehouseCode || '').trim(); if (code) return code; const type = value?.destinationType || fallback?.destinationType; return type === 'PRIVATE' ? '私人仓' : '目的仓待定' },
|
||
listInboundCode(item) { const code = String(item?.inboundCode || '').trim(); return code !== String(item?.transferNo || '').trim() ? code : '' },
|
||
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 },
|
||
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) },
|
||
statusLabel(status, transferNo = '') { if (String(status) === 'WAIT_PDA_RECEIVING' && String(warehouseWork.warehouseTransfer.currentNo) === String(transferNo)) return '测量中'; return ({ WAIT_PDA_RECEIVING: '待测量', APPROVED: '待测量', PARTIALLY_RECEIVED: '测量中' })[status] || '待测量' },
|
||
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 Number(this.palletSummary(pallet).volume.toFixed(3)) },
|
||
palletVolume(pallet) { return this.rawPalletVolume(pallet).toFixed(3) },
|
||
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 || '' },
|
||
applyPallet(target, value) { Object.assign(target, restoredPallet(value, this.pallets.indexOf(target)), { key: target.key, busy: false, collapsed: false }) },
|
||
async generatePallet(pallet) {
|
||
if (!this.selected || pallet.busy || pallet.generated || this.cartonsCounted || this.countingCartons) return
|
||
pallet.busy = true; this.error = null
|
||
try {
|
||
const measured = { palletId: pallet.draftId || null, idempotencyKey: `pda-transfer-draft-${this.selected.transferNo}-${pallet.key}` }
|
||
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('服务端未返回新托盘号')
|
||
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()
|
||
} 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)) },
|
||
// 刷新本单时合并预报新增箱号,保留本机已填写的测量草稿。
|
||
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 }
|
||
},
|
||
async loadCandidates() {
|
||
this.loading = true; this.error = null
|
||
try {
|
||
const activeNo = String(warehouseWork.warehouseTransfer.currentNo || '')
|
||
const measuredStatuses = new Set(['WAIT_PDA_RECEIVING', 'PARTIALLY_RECEIVED'])
|
||
this.candidates = (await listWarehouseTransferCandidates()).map((item) => String(item.transferNo) === activeNo && item.status === 'WAIT_PDA_RECEIVING' ? { ...item, status: 'PARTIALLY_RECEIVED' } : item)
|
||
this.candidates = this.candidates.filter((item) => measuredStatuses.has(String(item.status)))
|
||
// 服务端已完成当前单但设备在提交响应前断网时,本地 currentNo 会遗留。
|
||
// 候选已成功读取且当前单不再可测,清掉本地锁,允许继续下一票。
|
||
if (activeNo && !this.candidates.some((item) => String(item.transferNo) === activeNo && measuredStatuses.has(String(item.status)))) {
|
||
resetWarehouseDomain('warehouseTransfer')
|
||
}
|
||
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 = []
|
||
}
|
||
} catch (e) { this.error = e } finally { this.loading = false }
|
||
},
|
||
async select(item, fromExplicitTask = false) {
|
||
if (this.detailLoading || this.submitting || this.countingCartons || this.pallets.some(pallet => pallet.busy)) return
|
||
const requestedNo = String(item?.transferNo || '')
|
||
const activeNo = String(warehouseWork.warehouseTransfer.currentNo || '')
|
||
if (!activeNo && !fromExplicitTask) {
|
||
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 }
|
||
this.detailLoading = true; this.error = null; this.success = false; this.completedPallets = []; this.completedTransferNo = ''; this.completedDestination = ''; this.completedDestinationType = ''
|
||
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) }
|
||
catch (e) { this.selected = null; this.error = e }
|
||
finally { this.detailLoading = false }
|
||
},
|
||
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()) },
|
||
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);
|
||
uni.setStorageSync(this.cartonDraftKey(), { deletedBoxes: this.deletedCartonBoxes }); this.selected = detail; this.restoreCartons(); this.pendingKey = '';
|
||
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 }
|
||
},
|
||
async restorePhotos(transferNo, knownPhotos) {
|
||
let photos
|
||
try {
|
||
// 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
|
||
},
|
||
takePhoto() {
|
||
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
|
||
}
|
||
const transferNo = this.selected.transferNo
|
||
uni.chooseImage({ count: Math.min(9, MAX_MEASUREMENT_PHOTOS - this.photoFileIds.length), sourceType: ['camera', 'album'], success: async ({ tempFilePaths, tempFiles }) => {
|
||
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 }
|
||
const remaining = MAX_MEASUREMENT_PHOTOS - this.photoFileIds.length
|
||
const uploadPaths = paths.slice(0, remaining)
|
||
const skipped = paths.length - uploadPaths.length
|
||
this.uploadingPhoto = true; this.error = null
|
||
try {
|
||
for (const [index, path] of uploadPaths.entries()) {
|
||
if (this.photoFileIds.length >= MAX_MEASUREMENT_PHOTOS) throw new Error(`测量现场照片最多 ${MAX_MEASUREMENT_PHOTOS} 张`)
|
||
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 || '测量现场照片')
|
||
}
|
||
if (skipped > 0) this.error = new Error(`最多上传 ${MAX_MEASUREMENT_PHOTOS} 张测量现场照片,已忽略超出的 ${skipped} 张`)
|
||
this.pendingKey = ''
|
||
} 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 }
|
||
}, 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 {
|
||
// 清点、绑托和拍照完成后,仅 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()
|
||
}
|
||
this.pendingKey = `pda-transfer-${this.selected.transferNo}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
||
const payload = { idempotencyKey: this.pendingKey, photoFileIds: [...this.photoFileIds], discrepancyReason: this.hasSkuReceiptDifference ? this.countDiscrepancyReason : null }
|
||
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()
|
||
} 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>
|
||
.collapsed-carton-row{position:relative;min-height:40px;padding:0 36px 0 0;color:#415a78;font-size:12px;line-height:40px}.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:32px;height:32px;margin:0;padding:0;border:0;border-radius:6px;background:transparent;line-height:32px}.collapsed-carton-expand::after{border:0}.transfer-content .carton-count-card.carton-collapsed{padding:6px 12px}
|
||
.confirm-carton-button{margin:12px 0 0;border:1px solid #cbdcff;border-radius:8px;color:#135ee8;background:#eff5ff;font-size:13px;line-height:38px}
|
||
|
||
.pallet-title-line{display:flex;align-items:center;flex-wrap:wrap;gap:5px 10px}.pallet-inline-metrics{display:flex;flex-wrap:wrap;gap:4px 8px}.transfer-content .pallet-inline-metrics text{margin:0;color:#3f5671;font-size:10px;font-weight:500;white-space:nowrap}.pallet-index{flex-shrink:0}.remeasure-button{width:auto;margin:0;padding:0 12px;border:1px solid #b9d3f4;border-radius:8px;color:#135ee8;font-size:12px;line-height:36px;background:#f0f6ff}
|
||
.carton-title-row{display:flex;align-items:flex-start;gap:8px}.carton-title-row{flex-wrap:wrap}.carton-title-row>.measure-title{flex:1;min-width:150px}.delete-carton-button{flex:none;width:auto;margin:0;padding:0 8px;border:1px solid #f0caca;border-radius:7px;color:#b74545;background:#fff7f7;font-size:12px;line-height:30px}.add-sku-button{display:flex;align-items:center;justify-content:center;flex:none;box-sizing:border-box;width:24px;height:24px;margin:0;padding:0;border:1px solid #cbdcff;border-radius:7px;color:#135ee8;background:#f0f6ff;font-size:12px;line-height:30px}.add-sku-button[disabled]{color:#a0adbe;background:#f5f7fa}
|
||
|
||
.pallet-index{display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:10px;color:#fff;font-size:13px;font-weight:850;background:#135ee8;box-shadow:0 4px 10px 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:12px;padding:8px 14px;border-bottom:1px solid #e1e7ef;background:#fff}
|
||
.transfer-work-toolbar .back-to-list,.transfer-work-toolbar .refresh-button{display:flex;align-items:center;justify-content:center;gap:4px;width:auto;min-height:40px;margin:0;padding:0 12px;border:1px solid #d6e2f1;border-radius:8px;color:#135ee8;font-size:13px;line-height:40px;background:#f8fbff}
|
||
.carton-prerequisite{display:block;margin:0 0 12px;color:#718095;font-size:12px;line-height:18px}
|
||
.active-receiving-pallet{border-color:#79a7ec!important}.pallet-brief{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin:-3px 0 12px;color:#3f5671;font-size:12px}.pallet-brief button{margin:0 0 0 auto;padding:0 10px;height:36px;line-height:36px;border-radius:8px;background:#eaf2ff;color:#135ee8;font-size:11px}.carton-filter,.batch-box-options{display:flex;flex-wrap:wrap;gap:7px;margin:12px 0}.carton-filter button,.batch-box-options button{width:auto;margin:0;padding:0 11px;height:36px;line-height:36px;border:1px solid #d4dfed;border-radius:9px;background:#fff;color:#5b6e84;font-size:12px}.carton-filter .active,.batch-box-options .chosen{border-color:#135ee8;background:#eaf2ff;color:#135ee8}.batch-count-panel .add-pallet-button{margin:0}.carton-count-card>.measure-title{overflow-wrap:anywhere;font-size:12px;line-height:19px}.batch-sku{padding:10px;border:1px solid #cbdcf2;border-radius:8px;background:#fff}.summary-no{overflow-wrap:anywhere}
|
||
|
||
.batch-count-panel{margin:12px 0;padding:10px;border:1px solid #b9d3f4;border-radius:10px;background:#f4f8ff}.batch-sku{margin:10px 0;color:#17314f;font-size:12px;word-break:break-all}.batch-actions{display:flex;flex-wrap:wrap;gap:8px;margin:8px 0}.batch-actions button{width:auto;margin:0;padding:0 10px;min-height:34px;line-height:34px;font-size:11px;color:#135ee8;border:1px solid #b9d3f4;border-radius:7px;background:#fff}.batch-actions button[disabled]{opacity:.45}.batch-summary{display:block;margin:10px 0;color:#245981;font-size:11px;line-height:18px}
|
||
|
||
.carton-count-card{padding:14px 10px;margin:10px 0;border:1px solid #d8e3f1;border-radius:12px}
|
||
|
||
.transfer-page{height:100vh;overflow:hidden;background:#f2f5f9}.transfer-scroll{height:calc(100vh - 48px - var(--status-bar-height,0px))}.transfer-content{padding-bottom:52px}.transfer-list-page{background:#f1f5fd}
|
||
.transfer-list-page .transfer-content{padding-top:14px;padding-bottom:calc(24px + env(safe-area-inset-bottom,0px))}
|
||
.transfer-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 11px}
|
||
.transfer-list-count{min-width:0;color:#526174;font-size:14px;font-weight:700;line-height:20px}
|
||
.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:4px;width:auto;height:34px;margin:0;padding:0 11px;border:1px solid #b8d1f5;border-radius:17px;color:#135ee8;font-size:10px;line-height:34px;background:#f8fbff}
|
||
.transfer-heading .refresh-button{box-sizing:border-box;flex-shrink:0;gap:5px;min-width:72px;height:36px;padding:0 14px;border-radius:20px;font-size:14px;font-weight:700;line-height:normal;background:#fff}
|
||
.refresh-button[disabled]{opacity:.55}
|
||
.candidate-list{display:flex;flex-direction:column;gap:11px;margin-bottom:16px}
|
||
.transfer-card{display:block;box-sizing:border-box;width:100%;margin:0;padding:14px;border:1px solid #dce5f0;border-radius:13px;color:#1f2937;line-height:normal;text-align:left;white-space:normal;background:#fff;box-shadow:none;cursor:pointer;transition:background-color .12s ease,border-color .12s ease}
|
||
.transfer-card-pressed{border-color:#93baf0;background:#f4f8ff}
|
||
.transfer-card-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
||
.transfer-no{flex:1;min-width:0;color:#1f2937;font-size:18px;font-weight:750;line-height:26px;overflow-wrap:anywhere;word-break:break-all}
|
||
.status-pill{flex-shrink:0;padding:6px 10px;border-radius:9px;color:#135ee8;font-size:13px;font-weight:700;line-height:16px;white-space:nowrap;background:#e5efff}
|
||
.status-measuring{color:#08705f;background:#e2f6f0}
|
||
.task-metrics{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.3fr) minmax(0,1fr);margin-top:12px;padding:7px 0;border-top:1px solid #e1e7f0;border-bottom:1px solid #e1e7f0}
|
||
.task-metrics view{min-width:0;padding:0 6px;border-right:1px solid #e1e7f0;text-align:center}
|
||
.task-metrics view:last-child{border-right:0}
|
||
.task-metrics strong{display:block;color:#1f2937;font-size:22px;font-weight:750;line-height:26px;overflow-wrap:anywhere}
|
||
.task-metrics text{display:block;margin-top:2px;color:#6f8194;font-size:12px;line-height:18px}
|
||
.transfer-card-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;min-height:28px;margin-top:10px}
|
||
.transfer-meta{flex:1;min-width:0;color:#718095;font-size:12px;line-height:20px;overflow-wrap:anywhere}
|
||
.transfer-card-action{display:flex;flex-shrink:0;align-items:center;gap:2px;color:#135ee8;font-size:13px;font-weight:700;line-height:22px;white-space:nowrap}
|
||
.transfer-list-page .transfer-empty{font-size:12px;line-height:20px}
|
||
.transfer-list-page .empty-title{font-size:14px}
|
||
.transfer-empty{margin-bottom:12px;padding:26px 14px;border:1px dashed #ccd7e4;border-radius:13px;color:#8996a6;font-size:10px;line-height:18px;text-align:center;background:rgba(255,255,255,.65)}.empty-title{color:#56677b;font-size:12px;font-weight:800}.order-summary{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 15px;border-radius:14px 14px 0 0;color:#fff;background:linear-gradient(135deg,#123d73,#135ee8)}.summary-eyebrow{font-size:9px;opacity:.72}.summary-no{margin-top:3px;font-size:15px;font-weight:850}.summary-count{display:flex;align-items:baseline;gap:3px;flex-shrink:0}.summary-count strong{font-size:24px}.summary-count text{font-size:10px;opacity:.8}.summary-metrics{display:grid;grid-template-columns:repeat(3,1fr);margin-bottom:12px;padding:11px 8px;border-radius:0 0 14px 14px;background:#fff;box-shadow:0 5px 17px rgba(32,67,111,.07)}.summary-metrics view{position:relative;text-align:center}.summary-metrics view+view:before{position:absolute;top:3px;bottom:3px;left:0;width:1px;background:#e7edf4;content:''}.summary-metrics strong{display:block;color:#17314f;font-size:14px}.summary-metrics text{display:block;margin-top:2px;color:#8390a1;font-size:8px}.pallet-card{box-sizing:border-box;margin-bottom:12px;padding:14px;border:1px solid #d9e3ee;border-radius:15px;background:#fff;box-shadow:0 5px 18px rgba(32,67,111,.065)}.pallet-head{display:flex;align-items:center;gap:9px;margin:-2px -2px 14px;padding-bottom:11px;border-bottom:1px solid #edf1f5}.pallet-heading-copy{min-width:0;flex:1}.pallet-heading-copy text:first-child{color:#17314f;font-size:13px;font-weight:850}.pallet-heading-copy>text:last-child{margin-top:2px;color:#8995a5;font-size:8px}.remove-button{width:auto;height:28px;margin:0;padding:0 9px;border-radius:7px;color:#b33030;font-size:9px;line-height:28px;background:#fff0f0}.field-label{display:block;margin-bottom:5px;color:#65768b;font-size:9px;font-weight:700}.field-label small{margin-left:3px;color:#9aa5b3;font-size:8px;font-weight:500}.measure-input,.input-with-unit{box-sizing:border-box;width:100%;height:42px;border:1px solid #cad7e5;border-radius:9px;color:#203750;background:#fafcfe}.measure-input{padding:0 11px;font-size:12px}.pallet-code-field{display:block;margin-bottom:14px}.measure-section-head,.sku-section-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:9px}.measure-title{color:#243a53;font-size:11px;font-weight:850}.measure-hint{margin-top:2px;color:#8a96a5;font-size:8px}.volume-chip{flex-shrink:0;padding:5px 8px;border-radius:8px;text-align:right;background:#edf5ff}.volume-chip text{display:block;color:#7189a7;font-size:7px}.volume-chip strong{display:block;margin-top:1px;color:#135ee8;font-size:10px}.dimension-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:7px}.measure-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:9px;margin-top:10px}.input-with-unit{display:flex;align-items:center;overflow:hidden}.input-with-unit input{box-sizing:border-box;min-width:0;height:40px;padding:0 4px 0 10px;flex:1;color:#203750;font-size:16px;font-weight:800}.input-with-unit>text{padding-right:8px;color:#8491a1;font-size:8px}.input-with-unit.important{border-color:#9ebff0;background:#f8fbff}.sku-section{margin:15px -2px -2px;padding:12px;border-radius:11px;background:#f5f8fb}.sku-total{flex-shrink:0;padding:5px 7px;border-radius:7px;color:#9a5c13;font-size:9px;font-weight:800;background:#fff1dd}.sku-total.matched{color:#08705f;background:#dff5ed}.sku-row{display:flex;align-items:center;gap:9px;min-height:48px;border-top:1px solid #e3eaf2}.sku-copy{min-width:0;flex:1}.sku-code{overflow:hidden;color:#233b56;font-size:11px;font-weight:800;text-overflow:ellipsis;white-space:nowrap}.sku-copy text:last-child{margin-top:2px;color:#8693a3;font-size:8px}.sku-input{display:flex;align-items:center;gap:4px;color:#718095;font-size:9px}.sku-input input{box-sizing:border-box;width:64px;height:34px;padding:0 7px;border:1px solid #abc4e8;border-radius:8px;color:#17314f;font-size:14px;font-weight:800;text-align:center;background:#fff}.add-pallet-button{display:flex;align-items:center;justify-content:center;gap:5px;box-sizing:border-box;width:100%;height:44px;margin:2px 0 11px;border:1px dashed #78a8ed;border-radius:11px;color:#135ee8;font-size:11px;font-weight:800;background:#f7faff}.transfer-validation,.transfer-error,.transfer-success{box-sizing:border-box;margin-bottom:11px;padding:11px 12px;border-radius:10px;font-size:9px;line-height:16px}.transfer-validation,.transfer-error{color:#9d3030;background:#fff0f0}.validation-title,.feedback-title{font-size:11px;font-weight:850}.transfer-success{color:#176b42;background:#eaf8f1}.submit-panel{box-sizing:border-box;padding:12px;border:1px solid #dce5f0;border-radius:13px;background:#fff;box-shadow:0 5px 18px rgba(32,67,111,.07)}.submit-panel>view{display:flex;align-items:center;justify-content:space-between;margin-bottom:9px;color:#778598;font-size:9px}.submit-panel strong{color:#243951;font-size:11px}.transfer-primary{box-sizing:border-box;width:100%;height:44px;margin:0;border-radius:9px;color:#fff;font-size:12px;font-weight:850;line-height:44px;background:#135ee8}.transfer-primary[disabled]{color:#9aa7b5;background:#e7ecf2;opacity:1}
|
||
.photo-card{box-sizing:border-box;margin-bottom:11px;padding:14px;border:1px solid #d9e3ee;border-radius:13px;background:#fff;box-shadow:0 5px 18px rgba(32,67,111,.06)}
|
||
.photo-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:9px}
|
||
.photo-count{flex-shrink:0;padding:5px 8px;border-radius:7px;color:#135ee8;font-size:9px;font-weight:800;background:#eaf2ff}
|
||
.photo-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px;margin:11px 0}
|
||
.photo-tile{position:relative;overflow:hidden;border-radius:10px;background:#eef2f6}
|
||
.photo-image{display:block;width:100%;height:112px}
|
||
.photo-remove{position:absolute;right:5px;bottom:5px;width:auto;height:26px;margin:0;padding:0 8px;border-radius:6px;color:#fff;font-size:9px;line-height:26px;background:rgba(152,35,35,.88)}
|
||
.take-photo-button{display:flex;align-items:center;justify-content:center;gap:6px;box-sizing:border-box;width:100%;height:42px;margin:9px 0 0;border:1px solid #9fc0ef;border-radius:9px;color:#135ee8;font-size:10px;font-weight:800;background:#f8fbff}
|
||
.take-photo-button[disabled]{color:#9aa7b5;background:#e7ecf2;opacity:1}
|
||
.print-card{box-sizing:border-box;margin-bottom:11px;padding:14px;border:1px solid #b9d3f4;border-radius:13px;background:#f8fbff}.print-row button{width:auto;margin:0;padding:0 10px;color:#135ee8;border:1px solid #9fc0ef;border-radius:8px;background:#fff}.print-row{display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-top:1px solid #dce8f5;color:#728196;font-size:9px}.print-code{display:block;color:#17314f;font-size:13px;font-weight:850}
|
||
.generate-button{width:100%;margin-top:12px;color:#fff;background:#135ee8}.generated-card{display:flex;align-items:center;justify-content:space-between;margin-top:12px;padding:10px;border-radius:9px;background:#edf5ff}.generated-card button,.bind-panel>button{width:auto;margin:0;color:#135ee8;background:#fff}.bind-panel{margin-top:10px}.bound-state{margin-top:10px;padding:9px;border-radius:8px;color:#08705f;font-weight:800;background:#dff5ed}
|
||
.generated-card,.print-row{gap:10px}.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 10px}
|
||
.back-to-list{width:auto;height:30px;margin:0;padding:0 9px;border:1px solid #c8d8ec;border-radius:8px;color:#135ee8;font-size:10px;line-height:30px;background:#f8fbff}
|
||
.collapse-button{width:30px;height:28px;margin:0;padding:0;color:#135ee8;font-size:16px;line-height:28px;background:#edf5ff}
|
||
.receipt-comparison{display:grid;grid-template-columns:repeat(3,1fr);gap:7px;margin-bottom:9px}.receipt-comparison view{padding:9px;border-radius:9px;text-align:center;background:#fff}.receipt-comparison strong,.receipt-comparison text{display:block}.receipt-comparison strong{color:#17314f;font-size:15px}.receipt-comparison text{margin-top:2px;color:#8390a1;font-size:8px}.receipt-comparison .difference{border:1px solid #e3a044;background:#fff4df}.receipt-comparison .difference strong,.receipt-comparison .difference text{color:#a35b0c;font-weight:800}
|
||
.transfer-content .measure-hint{font-size:11px;line-height:17px}.transfer-content .measure-title{font-size:13px}.transfer-content .summary-metrics text{font-size:10px}.transfer-content .transfer-primary{height:46px;line-height:46px;font-size:14px}.transfer-content .photo-card{padding:14px}.transfer-content .pallet-heading-copy>text:last-child{margin-top:2px;color:#8995a5;font-size:8px}.transfer-content .summary-no{font-size:13px}.transfer-content .batch-summary{font-size:12px}
|
||
/* 测量任务默认展开,收起后仍保留单号和实收箱数,方便现场核对。 */
|
||
.order-summary{position:relative;margin-bottom:10px;border-radius:12px;cursor:pointer;padding:12px;gap:8px}
|
||
.order-summary>view:first-child{flex:1;min-width:0}.summary-toggle{flex:none;font-size:11px;white-space:nowrap;opacity:.9}.transfer-content .summary-no{font-size:12px;line-height:18px;word-break:break-all}.summary-count strong{font-size:20px}
|
||
.receipt-comparison{margin-top:0}.summary-metrics{box-shadow:none}
|
||
/* 托盘卡片按身份、实测指标、业务码分层,所有信息沿左侧基线排列。 */
|
||
.transfer-content .pallet-card{padding:12px;border-radius:12px;border-color:#dce5f0;box-shadow:0 2px 8px rgba(32,67,111,.04);text-align:left}
|
||
.transfer-content .pallet-card.active-receiving-pallet{border-color:#91b7ef!important}
|
||
.pallet-card .pallet-head{display:flex;align-items:center;gap:9px;margin:0;padding:0 0 10px;border-bottom:0;text-align:left}
|
||
.pallet-card .pallet-index{width:30px;height:30px;min-width:30px;border-radius:8px;font-size:12px;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:8px;flex-wrap:wrap}
|
||
.pallet-card .pallet-title-line .pallet-name{margin:0;font-size:14px;font-weight:750;color:#18324f}
|
||
.pallet-card .pallet-title-line .pallet-status{margin:0;padding:3px 6px;border-radius:5px;color:#a47320;background:#fff6e5;font-size:10px;font-weight:500;line-height:15px}
|
||
.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:28px;height:28px;margin:0;padding:0;line-height:26px;border-radius:7px;font-size:18px}
|
||
.pallet-card .remove-button{color:#c46868;background:#fff5f5}.pallet-card .collapse-button{color:#6180a6;background:#f2f6fc}
|
||
.pallet-stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;padding:10px;border-radius:8px;background:#f5f8fc}
|
||
.pallet-stats>view{min-width:0;text-align:left}.pallet-stats strong{display:block;color:#213b58;font-size:15px;font-weight:750;line-height:21px}.pallet-stats strong text{font-size:10px;font-weight:400;color:#73859b}.pallet-stats>view>text{display:block;margin-top:3px;color:#8090a3;font-size:10px}
|
||
.pallet-card .pallet-brief{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:10px 0 0;padding:0;flex-wrap:nowrap}
|
||
.pallet-business-code{flex:1;min-width:0;color:#7c8ca0;font-size:9px;line-height:15px;overflow-wrap:anywhere;word-break:break-all;text-align:left}
|
||
.receiving-badge{flex:none;display:flex;align-items:center;gap:5px;color:#3273c7;font-size:10px;white-space:nowrap}.receiving-badge:before{content:'';width:5px;height:5px;border-radius:50%;background:#4184e1}
|
||
.pallet-card .pallet-brief button{flex:none;width:auto;height:28px;line-height:28px;margin:0;padding:0 8px;font-size:10px;border-radius:6px;background:#edf4ff;color:#246ac6}
|
||
.pallet-card .measure-section-head{margin-top:12px}.pallet-card .generated-card{margin-top:10px}
|
||
/* 箱号独占一行,操作按钮固定在标题右侧,长业务编号不会挤压按钮。 */
|
||
.transfer-content .carton-count-card{padding:12px;margin:12px 0;border-color:#dce5f0;border-radius:12px;background:#fff}
|
||
.carton-title-row{flex-wrap:nowrap;align-items:center;justify-content:space-between;gap:8px}.carton-title-row>.measure-title{min-width:0;font-size:14px!important}.carton-icon-actions{display:flex;flex:none;gap:6px}
|
||
.carton-icon-actions button{box-sizing:border-box;width:30px;height:30px;padding:0;margin:0;line-height:28px;font-size:20px;border-radius:8px}
|
||
.carton-business-code{display:block;margin:5px 0 10px;color:#7c8da2;font-size:10px;line-height:15px;overflow-wrap:anywhere;word-break:break-all}
|
||
.carton-count-card .batch-sku{padding:9px 10px;margin:8px 0;font-size:11px;background:#f7faff;border-color:#dce5f0}.carton-count-card .batch-actions:empty{display:none}
|
||
</style>
|