251 lines
48 KiB
Vue
251 lines
48 KiB
Vue
<template>
|
||
<view v-if="user" class="page-shell portal-page receiving-page">
|
||
<PortalHeader :title="inboundMode ? '拆柜收货' : operationName" :user="user" />
|
||
<view class="portal-content receiving-content" :class="{ 'receiving-content-with-action': phase === 'detail' || phase === 'sign' || (phase === 'receive' && canFinishOnsite) }">
|
||
<template v-if="inboundMode">
|
||
<view v-if="phase !== 'candidates' && phase !== 'receive'" class="receiving-heading"><view><text class="receiving-title">{{ pageTitle }}</text></view></view>
|
||
|
||
<view v-if="loading" class="state-card"><uni-load-more status="loading" /><text>正在恢复当前柜作业…</text></view>
|
||
|
||
<template v-else-if="phase === 'candidates'">
|
||
<view class="list-toolbar"><text>共 {{ candidates.length }} 柜</text><button :disabled="busy || candidateLoading" @tap="loadCandidates">{{ candidateLoading ? '刷新中…' : '刷新' }}</button></view>
|
||
<view v-if="candidateError" class="error-card candidate-error-card"><text class="error-message">{{ candidateError.message || '待卸货集装箱加载失败' }}</text><text v-if="candidateError.code">code={{ candidateError.code }}</text><text v-if="candidateError.requestId">requestId={{ candidateError.requestId }}</text><text v-if="candidateError.traceId">traceId={{ candidateError.traceId }}</text><button class="secondary-button" :disabled="candidateLoading" @tap="loadCandidates">重新加载</button></view>
|
||
<view v-if="candidateLoading && !candidates.length" class="state-card"><uni-load-more status="loading" /><text>正在刷新待卸货集装箱…</text></view>
|
||
<view v-else-if="!candidateError && !candidates.length" class="empty-card"><uni-icons type="info" color="#8090a3" size="34" /><text>暂无待卸货集装箱</text><button class="secondary-button" :disabled="candidateLoading" @tap="loadCandidates">重新加载</button></view>
|
||
<view v-for="item in candidates" :key="candidateKey(item)" class="container-card container-card-compact" :class="{ 'container-card-disabled': !item.operable }" role="button" :aria-label="`${item.containerNo},${item.operable ? '查看可操作柜详情' : '查看未齐全柜摘要'}`" hover-class="container-card-pressed" @tap="openContainer(item)">
|
||
<view class="card-head"><view><text class="container-no">{{ item.containerNo }}</text><text v-if="item.operable || item.plannedArrivalAt" class="candidate-arrival">计划到仓:{{ formatPlannedArrival(item.plannedArrivalAt) }}</text></view><view class="container-card-actions"><text class="status-tag" :class="item.operable ? statusTone(item.status) : 'status-disabled'">{{ item.operable ? statusLabel(item.status) : '待齐全' }}</text><text class="container-detail-link">{{ item.operable ? (item.currentAccount ? '继续' : '详情') : '只读' }} ›</text></view></view>
|
||
<view v-if="item.operable" class="candidate-facts"><view><strong>{{ item.totalOrderCount }}</strong><text>总订单</text></view><view><strong>{{ item.inboundCodeCount }}</strong><text>入仓码</text></view><view><strong>{{ item.planCount }}</strong><text>已生成计划</text></view><view><strong>{{ item.pendingPlanCount }}</strong><text>未操作</text></view></view>
|
||
<text v-if="item.operable" class="candidate-progress-copy">已生成 {{ item.planCount }} 个入库计划,{{ item.pendingPlanCount }} 个未操作<template v-if="item.missingInboundCodeCount">,其中 {{ item.missingInboundCodeCount }} 个缺入仓码</template></text>
|
||
<text v-else class="candidate-pending-summary">{{ item.pendingPlanCount }} 单待处理</text>
|
||
<text v-if="item.operable" class="candidate-receiving-warehouse">海外接柜仓:{{ receivingWarehouseLabel(item) }}</text>
|
||
<text v-if="item.operable && item.destinationCodes?.length" class="candidate-destinations">业务目的仓:{{ item.destinationCodes.join(' / ') }}</text>
|
||
</view>
|
||
</template>
|
||
|
||
<template v-else-if="phase === 'detail'">
|
||
<template v-if="selected?.operable">
|
||
<ReceivingSessionSummary :session="selected" />
|
||
<view class="sticky-action"><button class="ghost-button" :disabled="busy" @tap="cancelDetail">返回列表</button><button class="primary-button" :disabled="busy || !selected.allPlansReady || !selected.operable" @tap="prepareContainer">确认接柜仓并开始拍照</button></view>
|
||
</template>
|
||
<template v-else>
|
||
<view class="readonly-candidate-card">
|
||
<view class="card-head"><view><text class="container-no">{{ selected?.containerNo }}</text><text class="candidate-arrival">计划到仓:{{ formatPlannedArrival(selected?.plannedArrivalAt) }}</text></view><text class="status-tag status-disabled">暂不可操作</text></view>
|
||
<view class="candidate-facts"><view><strong>{{ selected?.totalOrderCount }}</strong><text>总订单</text></view><view><strong>{{ selected?.inboundCodeCount }}</strong><text>入仓码</text></view><view><strong>{{ selected?.planCount }}</strong><text>已生成计划</text></view><view><strong>{{ selected?.pendingPlanCount }}</strong><text>未操作</text></view></view>
|
||
<text class="candidate-progress-copy">已生成 {{ selected?.planCount }} 个入库计划,{{ selected?.pendingPlanCount }} 个未操作<template v-if="selected?.missingInboundCodeCount">,其中 {{ selected.missingInboundCodeCount }} 个缺入仓码</template></text>
|
||
<text class="candidate-block-reason">{{ selected?.blockReason || '当前柜入库计划尚未齐全' }}</text>
|
||
<text class="candidate-receiving-warehouse">海外接柜仓:{{ receivingWarehouseLabel(selected) }}</text>
|
||
</view>
|
||
<view class="sticky-action one"><button class="primary-button" :disabled="busy" @tap="cancelDetail">返回列表</button></view>
|
||
</template>
|
||
</template>
|
||
|
||
<template v-else-if="phase === 'sign'">
|
||
<ReceivingSessionSummary :session="current" />
|
||
<view class="photo-card domestic-photo-card">
|
||
<view class="card-head"><view><text class="card-title">国内装柜照片(只读)</text></view><text class="status-tag status-good">{{ domesticPhotoCount }} / 5</text></view>
|
||
<view class="domestic-photo-grid">
|
||
<view v-for="photo in domesticLoadingPhotos" :key="photo.evidenceType" class="domestic-photo-item" @tap="previewDomesticPhoto(photo)">
|
||
<image v-if="photo.localPath" class="domestic-photo-thumb" :src="photo.localPath" mode="aspectFill" @error="invalidatePhotoPreview(photo)" />
|
||
<view v-else class="domestic-photo-placeholder"><uni-load-more v-if="photo.status === 'loading'" status="loading" :show-text="false" /><uni-icons v-else type="image" color="#8090a3" size="22" /><text>{{ photo.status === 'error' ? '加载失败,点按重试' : '正在准备照片' }}</text></view>
|
||
<text>{{ photo.label }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="photo-card">
|
||
<view class="card-head"><view><text class="card-title">卸货签收照片</text></view><text class="status-tag" :class="photosReady ? 'status-good' : 'status-wait'">{{ uploadedPhotoCount }} / 3</text></view>
|
||
<view class="photo-grid">
|
||
<view v-for="(step, index) in photoSteps" :key="step.role" class="photo-role" :class="{ complete: photoSlots[index]?.fileId }">
|
||
<view class="photo-preview" :class="{ failed: photoSlots[index]?.status === 'error' }" @tap="previewPhoto(index)">
|
||
<image v-if="photoSlots[index]?.localPath" class="photo-thumb" :src="photoSlots[index].localPath" mode="aspectFill" @error="invalidatePhotoPreview(photoSlots[index])" />
|
||
<view v-else class="photo-placeholder"><uni-icons type="camera" color="#8090a3" size="23" /><text>未添加</text></view>
|
||
</view>
|
||
<view class="photo-role-info">
|
||
<view class="photo-role-head"><text>{{ step.label }}</text><text :class="photoSlots[index]?.fileId ? 'photo-valid' : 'photo-required'">{{ photoSlots[index]?.fileId ? '已上传' : '必传' }}</text></view>
|
||
<text v-if="photoSlots[index]?.status === 'uploading' || photoSlots[index]?.status === 'error'" class="photo-state">{{ photoStateLabel(photoSlots[index]) }}</text>
|
||
<text v-if="photoSlots[index]?.status === 'error'" class="photo-error">{{ photoSlots[index].errorMessage || '请重试' }}</text>
|
||
</view>
|
||
<view class="photo-actions"><button :disabled="busy" @tap="choosePhoto(index)">{{ photoSlots[index]?.fileId ? '重拍' : '拍照' }}</button></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="sticky-action one"><button class="primary-button" :disabled="busy || !photosReady" @tap="signContainer">确认签收并开始收货</button></view>
|
||
</template>
|
||
|
||
<template v-else-if="phase === 'receive'">
|
||
<ReceivingSessionSummary :session="current" />
|
||
<view class="photo-card domestic-photo-card domestic-photo-collapsible">
|
||
<view class="card-head" @tap="toggleDomesticPhotos"><text class="card-title">国内装柜照片({{ domesticPhotoCount }} / 5)</text><text class="container-detail-link">{{ domesticPhotosExpanded ? '收起' : '查看' }} ›</text></view>
|
||
<view v-if="domesticPhotosExpanded" class="domestic-photo-grid">
|
||
<view v-for="photo in domesticLoadingPhotos" :key="photo.evidenceType" class="domestic-photo-item" @tap.stop="previewDomesticPhoto(photo)">
|
||
<image v-if="photo.localPath" class="domestic-photo-thumb" :src="photo.localPath" mode="aspectFill" @error="invalidatePhotoPreview(photo)" />
|
||
<view v-else class="domestic-photo-placeholder"><uni-load-more v-if="photo.status === 'loading'" status="loading" :show-text="false" /><uni-icons v-else type="image" color="#8090a3" size="22" /><text>{{ photo.status === 'error' ? '加载失败,点按重试' : '正在准备照片' }}</text></view>
|
||
<text>{{ photo.label }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="scan-card">
|
||
<text class="section-label">1. 货物码</text>
|
||
<ScanTargetInput name="carton" v-model="cartonInput" label="货物码" placeholder="扫描 DP 或箱码" :active="activeTarget === 'carton'" @activate="activeTarget = 'carton'" @submit="resolveCarton" />
|
||
<button class="secondary-button" :disabled="busy || !cartonInput.trim()" @tap="resolveCarton(cartonInput)">解析货物码</button>
|
||
|
||
<template v-if="activeInbound">
|
||
<view class="matched-inbound-card">
|
||
<view class="mode-banner" :class="activeInbound.destinationType === 'FBA' ? 'fba' : 'overseas'"><text>{{ activeInbound.destinationType === 'FBA' ? 'FBA 按箱数收货' : '海外仓逐箱收货' }}</text><text>{{ destinationLabel(activeInbound) }}</text></view>
|
||
<text class="matched-inbound-code">{{ (activeInbound.inboundCodes || [activeInbound.inboundCode]).join(' / ') }}</text>
|
||
<view class="matched-plan-flags"><text>{{ activeInbound.assigned ? '已分配' : '未分配' }}</text><text :class="{ optional: activeInbound.mergeAllowed }">{{ activeInbound.mergeAllowed ? '可合托(现场自选)' : '不合托' }}</text><text :class="{ urgent: activeInbound.urgent }">{{ activeInbound.urgent ? '加急' : '普通' }}</text></view>
|
||
<text v-if="activeInbound.mergeAllowed && activeInbound.mergeHint" class="merge-option-hint">{{ activeInbound.mergeHint }}</text>
|
||
<view class="matched-inbound-progress"><text>总计 {{ activeInbound.totalPackages }} 箱</text><text>已收 {{ activeInbound.receivedPackages }} 箱</text><text>待收 {{ activeInbound.pendingPackages }} 箱</text></view>
|
||
</view>
|
||
<text class="section-label">2. 托盘码</text>
|
||
|
||
<ScanTargetInput name="pallet" v-model="palletInput" label="托盘码" placeholder="扫描或人工输入托盘码" :active="activeTarget === 'pallet'" @activate="activeTarget = 'pallet'" @submit="resolvePallet" />
|
||
<button class="secondary-button" :disabled="busy || !palletInput.trim()" @tap="resolvePallet(palletInput)">校验托盘</button>
|
||
<view v-if="resolvedPallet" class="resolved-card"><text>{{ resolvedPallet.palletCode }}</text><text>{{ resolvedPallet.compatible ? '预留托盘校验通过,可收货' : '托盘不匹配' }}</text><text v-if="resolvedPallet.locationCode">库位:{{ resolvedPallet.locationCode }}{{ resolvedPallet.locationType === 'SHARED_OVERFLOW' ? '(A0共享溢出位)' : '' }}</text><text v-else>库位:待现场确认</text></view>
|
||
<template v-if="activeInbound.destinationType === 'FBA'">
|
||
<text class="section-label">3. 该托累计箱数</text>
|
||
<input v-model.number="packageCount" class="quantity-input" type="number" inputmode="numeric" :placeholder="`该托当前累计箱数`" />
|
||
</template>
|
||
<button class="primary-button receipt-button" :disabled="!canSubmitReceipt || busy" @tap="submitReceipt">{{ activeInbound.destinationType === 'FBA' ? '保存该托累计箱数' : '确认本箱收货' }}</button>
|
||
</template>
|
||
</view>
|
||
<view v-if="lastReceipt" class="success-card">本次已收 {{ lastReceipt.acceptedPackages }} 箱,整柜剩余 {{ current.pendingPackages }} 箱</view>
|
||
<view v-if="canFinishOnsite" class="sticky-action one"><button class="primary-button complete-button" :disabled="busy || activeTarget === 'quantity' || scanQueue.length" @tap="completeContainer">{{ allInboundsReconciled ? '完成整柜入库' : '现场收货结束(标记差异)' }}</button></view>
|
||
</template>
|
||
|
||
<template v-else-if="phase === 'completed'">
|
||
<view class="completed-card"><view class="completed-icon">✓</view><text class="completed-title">{{ current?.status === 'COMPLETED_WITH_DIFFERENCE' ? '现场收货已结束,差异待核平' : '当前柜已完成入库' }}</text><text>{{ current?.containerNo }}</text><text>实收 {{ current?.receivedPackages }} / {{ current?.totalPackages }} 箱</text><button class="primary-button" @tap="finishAndReturn">返回待卸货列表</button></view>
|
||
</template>
|
||
|
||
<view v-if="error" class="error-card">
|
||
<text class="error-message">{{ error.message }}</text>
|
||
<text v-if="error.code">code={{ error.code }}</text><text v-if="error.requestId">requestId={{ error.requestId }}</text><text v-if="error.traceId">traceId={{ error.traceId }}</text>
|
||
</view>
|
||
</template>
|
||
|
||
<template v-else>
|
||
<view class="receiving-heading"><view><text class="receiving-title">{{ operationName }}</text><text class="receiving-subtitle">当前功能暂未开放</text></view></view>
|
||
<view class="blocked-card"><uni-icons type="locked" color="#8090a3" size="36" /><text class="blocked-title">功能暂未开放</text><text>当前作业暂未开放,扫码不会提交,请返回工作台选择可用功能。</text></view>
|
||
</template>
|
||
</view>
|
||
<view v-if="toast.visible" class="app-toast" :class="toast.type" role="status">{{ toast.message }}</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import PortalHeader from '@/components/PortalHeader.vue'
|
||
import ReceivingSessionSummary from '@/components/ReceivingSessionSummary.vue'
|
||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||
import { assertReceivingWarehouse, isNoCurrentReceiving, isReceivingConflict, RECEIVING_PHOTO_ROLES, toReceivingError } from '@/api/receiving-adapter.js'
|
||
import { applyWarehouseProfile, session, requireSession } from '@/stores/session.js'
|
||
import { idempotencyKey, receiving, receivingPhase, receivingProgress, releaseIdempotencyKey, resetReceivingDraft, restoreReceivingPhoto, setCurrentReceiving } from '@/stores/receiving.js'
|
||
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
|
||
import { playErrorSound, playSuccessSound } from '@/utils/audio-feedback.js'
|
||
import { completeCurrentReceiving, downloadCurrentDomesticLoadingPhoto, downloadCurrentUnloadPhoto, getCurrentReceiving, getCurrentWarehouseProfile, getReceivingContainer, listReceivingCandidates, prepareReceivingContainer, receiveCurrentBulk, receiveCurrentCarton, receiveCurrentWholePallet, resolveCurrentCarton, resolveCurrentPallet, signCurrentReceiving, uploadCurrentUnloadPhoto } from '@/api/inbound.js'
|
||
const MAX_UNLOAD_PHOTO_BYTES = 10 * 1024 * 1024
|
||
const MAX_PACKAGES_PER_PALLET = 66
|
||
const PALLET_CAPACITY_MESSAGE = '一个托盘最多只能收66箱,请拆分到其他托盘'
|
||
|
||
export default {
|
||
components: { PortalHeader, ReceivingSessionSummary, ScanTargetInput },
|
||
data() { return { user: session.currentUser, moduleKey: 'query', operationName: '库存查询', loading: false, candidateLoading: false, busy: false, bootstrapped: false, detailOpen: false, domesticPhotosExpanded: false, cartonInput: '', palletInput: '', packageCount: '', candidateError: null, error: null, scanQueue: [], toast: { visible: false, message: '', type: '' } } },
|
||
computed: {
|
||
inboundMode() { return this.moduleKey === 'inbound' },
|
||
photoSteps() { return RECEIVING_PHOTO_ROLES.map((role) => ({ role, label: ({ LICENSE_PLATE: '车牌照片', CONTAINER_SEAL: '集装箱封签', DOOR_OPEN: '开柜现场' })[role] })) },
|
||
candidates() { return receiving.candidates }, selected() { return receiving.selected }, current() { return receiving.current }, photoSlots() { return receiving.photoSlots },
|
||
domesticLoadingPhotos() { return this.current?.domesticLoadingPhotos || [] }, domesticPhotoCount() { return this.domesticLoadingPhotos.length },
|
||
resolvedCarton() { return receiving.resolvedCarton }, resolvedPallet() { return receiving.resolvedPallet }, lastReceipt() { return receiving.lastReceipt },
|
||
activeTarget: { get() { return receiving.activeScanTarget }, set(value) { receiving.activeScanTarget = value } },
|
||
phase() { return this.detailOpen && !this.current ? 'detail' : receivingPhase(this.current) }, progress() { return receivingProgress(this.current) }, activeInbound() { return this.resolvedCarton },
|
||
uploadedPhotoCount() { return this.photoSlots.filter((item) => item?.fileId).length }, photosReady() { return this.uploadedPhotoCount === 3 && !this.photoSlots.some((item) => item?.status === 'uploading') },
|
||
canSubmitReceipt() { if (!this.activeInbound || !this.resolvedPallet?.compatible) return false; if (this.activeInbound.destinationType === 'FBA') { const count = Number(this.packageCount); return Number.isInteger(count) && count > 0 } return Boolean(this.activeInbound.cartonMark) },
|
||
allInboundsReconciled() { const values = this.current?.inbounds || []; return values.length > 0 && !this.current?.hasDifference && values.every((item) => Number(item.pendingPackages) === 0) },
|
||
canFinishOnsite() { return Boolean(this.current && ['SIGNED', 'RECEIVING'].includes(this.current.status)) },
|
||
pageTitle() { return { candidates: '待卸货集装箱', detail: '柜号详情', sign: '拆柜拍照签收', receive: '当前柜收货', completed: '入库完成' }[this.phase] }
|
||
},
|
||
onLoad(options) { this.moduleKey = options.module || 'query'; const key = options.op || (this.moduleKey === 'inbound' ? 'receiving' : 'inventoryQuery'); this.operationName = ({ receiving: '拆柜收货', stocktake: '库存盘点', move: '商品移库', pallet: '托盘管理', relabelReceive: '换标列表', relabelExecute: '换标执行', relabelReview: '换标复核', inventoryQuery: '库存查询', locationQuery: '库位查询', palletQuery: '托盘查询' })[key] || '库存查询' },
|
||
onShow() { if (!requireSession()) return; this.user = session.currentUser; this._pageActive = true; this.startScanner(); if (this.inboundMode && (!this.bootstrapped || this.phase === 'candidates' || this._resumeReceiving)) { if (!this.busy) { this._resumeReceiving = false; this.bootstrap() } } },
|
||
onHide() { this.suspendReceiving() }, onUnload() { this.suspendReceiving() }, beforeUnmount() { this.suspendReceiving() },
|
||
methods: {
|
||
candidateKey(item) { return `${item?.sourceContainerBookingId || ''}:${item?.containerNo || ''}` },
|
||
statusLabel(status) { return ({ WAIT_SIGN: '待签收', SIGNED: '已签收', RECEIVING: '收货中', COMPLETED: '已完成', COMPLETED_WITH_DIFFERENCE: '差异待核平' })[status] || status },
|
||
statusTone(status) { return status === 'RECEIVING' ? 'status-current' : status === 'COMPLETED' ? 'status-good' : 'status-wait' },
|
||
formatPlannedArrival(value) { const text = String(value || '').trim(); return text ? text.replace('T', ' ').slice(0, 16) : '待确认' },
|
||
destinationLabel(item) { return item?.destinationType === 'FBA' ? (item.fbaWarehouseCode || 'FBA') : (item?.destinationWarehouseName || item?.destinationWarehouseCode || '海外仓') },
|
||
receivingWarehouseLabel(item) { const code = String(item?.overseasWarehouseCode || '').trim(); const name = String(item?.overseasWarehouseName || '').trim(); return code && name ? `${code} · ${name}` : '—' },
|
||
validateReceivingWarehouse(value, label) { return assertReceivingWarehouse(value, this.user, label) },
|
||
async refreshBoundWarehouse() { this.user = applyWarehouseProfile(await getCurrentWarehouseProfile()); return this.user },
|
||
photoStateLabel(photo) { if (photo?.status === 'uploading') return '正在上传…'; if (photo?.status === 'error') return photo.errorMessage || (photo.fileId ? '重拍失败,已保留原图' : '上传失败,请重试'); return '' },
|
||
startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => this.handleHardwareScan(value), onError: (error) => { this.error = error; this.showToast('硬件扫码接收器异常,请检查扫描服务', 'warning') } }) }, stopScanner() { if (typeof this._stopScanner === 'function') this._stopScanner(); this._stopScanner = null },
|
||
handleHardwareScan(value) { if (!this._pageActive || this.loading) return; const scan = { value: String(value || '').trim(), target: this.activeTarget }; if (!scan.value) return; if (!this.inboundMode) return this.showToast('该功能缺少海外 PDA 专用接口,扫码未提交', 'warning'); if (this.phase === 'candidates') return this.showToast('请直接点击待卸货集装箱进入详情', 'warning'); if (this.phase !== 'receive') return; if (this.busy || this.activeTarget === 'quantity') { if (this.scanQueue.length >= 100) return this.showToast('扫码队列已满,请先处理当前托盘', 'warning'); this.scanQueue.push(scan); return this.showToast(`已排队 ${this.scanQueue.length} 个扫码`, 'warning') } return this.dispatchHardwareScan(scan) },
|
||
dispatchHardwareScan(scan) { if (!this._pageActive || this.loading) return; const palletWorkflowActive = Boolean(this.activeInbound || this.resolvedPallet); const target = palletWorkflowActive ? 'pallet' : scan.target; console.info('[Overseas PDA scan] dispatch', { value: scan.value, target, queuedTarget: scan.target }); if (target === 'pallet') { this.palletInput = scan.value; return this.resolvePallet(scan.value) } this.cartonInput = scan.value; return this.resolveCarton(scan.value) },
|
||
drainScanQueue() { if (!this._pageActive || this.loading || this.busy || this.activeTarget === 'quantity' || !this.scanQueue.length || this.phase !== 'receive') return; const next = this.scanQueue.shift(); this.dispatchHardwareScan(next) },
|
||
suspendReceiving() { this._pageActive = false; this._scanEpoch = (this._scanEpoch || 0) + 1; this._resumeReceiving = true; clearTimeout(this._drainTimer); clearTimeout(this._toastTimer); this.scanQueue = []; this.stopScanner() },
|
||
scheduleScanDrain() { clearTimeout(this._drainTimer); if (!this._pageActive) return; const epoch = this._scanEpoch || 0; this._drainTimer = setTimeout(() => { if (this._pageActive && epoch === (this._scanEpoch || 0)) this.drainScanQueue() }, 0) },
|
||
async run(action, work) { if (this.busy || !this._pageActive) throw new Error('当前页面未就绪,请返回作业页重试'); this.busy = true; this.error = null; try { const result = await work(); this.showToast(`${action}成功`, 'success'); return result } catch (error) { const mappedError = toReceivingError(error, `${action}失败`); this.error = mappedError; playErrorSound(); this.showToast(mappedError.message, 'error'); throw mappedError } finally { this.busy = false; if (this._pageActive && this._resumeReceiving) { this._resumeReceiving = false; this.bootstrap() } else this.scheduleScanDrain() } },
|
||
setScanTargetForCurrent(value) { receiving.activeScanTarget = value?.status === 'WAIT_SIGN' ? 'container' : 'carton' },
|
||
async restorePhotoPreviews() { await Promise.all(receiving.photoSlots.filter((photo) => photo?.downloadPath && !photo.localPath).map(async (photo) => { try { await restoreReceivingPhoto(photo, downloadCurrentUnloadPhoto) } catch (error) { this.error = error } })) },
|
||
async restoreDomesticPhotoPreviews() { if (this.phase !== 'sign' && !this.domesticPhotosExpanded) return; await Promise.all(this.domesticLoadingPhotos.filter((photo) => photo.downloadPath && !photo.localPath).map((photo) => this.loadDomesticPhoto(photo))) },
|
||
async toggleDomesticPhotos() { this.domesticPhotosExpanded = !this.domesticPhotosExpanded; if (this.domesticPhotosExpanded) await this.restoreDomesticPhotoPreviews() },
|
||
async loadDomesticPhoto(photo) { if (!photo?.downloadPath) return; try { await restoreReceivingPhoto(photo, downloadCurrentDomesticLoadingPhoto) } catch (_) { photo.status = 'error' } },
|
||
async previewDomesticPhoto(photo) { if (!photo?.localPath) await this.loadDomesticPhoto(photo); if (photo?.localPath) { const urls = this.domesticLoadingPhotos.map((item) => item.localPath).filter(Boolean); uni.previewImage({ urls, current: photo.localPath }) } },
|
||
async applyCurrentReceiving(value) { this.validateReceivingWarehouse(value, '当前接柜会话'); setCurrentReceiving(value, JSON.stringify([session.tenantId, this.user?.id, this.user?.warehouseId])); if (!this._pageActive) return; await Promise.all([this.restorePhotoPreviews(), this.restoreDomesticPhotoPreviews()]); this.setScanTargetForCurrent(value) },
|
||
async bootstrap() { if (this.loading) return; this.loading = true; this.error = null; this.detailOpen = false; try { await this.refreshBoundWarehouse(); try { const current = await getCurrentReceiving(); await this.applyCurrentReceiving(current); this.candidateError = null } catch (error) { if (!isNoCurrentReceiving(error)) throw error; resetReceivingDraft(); await this.loadCandidates(false, false) } this.bootstrapped = true } catch (error) { this.candidateError = error } finally { this.loading = false } },
|
||
async recoverCurrentAfterConflict(error, preserveEvidence = false) { if (!isReceivingConflict(error)) return false; try { const current = await getCurrentReceiving(); await this.applyCurrentReceiving(current); this.detailOpen = false; this.error = preserveEvidence ? error : null; this.showToast(`账号已有未完成柜 ${current.containerNo},已返回当前柜`, 'warning'); return true } catch (recoveryError) { if (isNoCurrentReceiving(recoveryError)) { this.detailOpen = false; receiving.selected = null; await this.loadCandidates(false); this.candidateError = preserveEvidence ? error : null; this.error = preserveEvidence ? error : null; this.showToast('柜状态已变化,列表已刷新', 'warning'); return true } this.error = error; return false } },
|
||
async loadCandidates(feedback = true, refreshWarehouse = true) { if (this.candidateLoading) return; this.candidateLoading = true; this.candidateError = null; this.error = null; try { if (refreshWarehouse) await this.refreshBoundWarehouse(); const values = await listReceivingCandidates(); receiving.candidates = values.map((item) => this.validateReceivingWarehouse(item, `柜号 ${item.containerNo || '—'}`)); if (feedback) this.showToast('刷新待卸柜成功', 'success') } catch (error) { receiving.candidates = []; this.candidateError = error; if (feedback) { playErrorSound(); this.showToast(error.message || '刷新待卸柜失败', 'error') } } finally { this.candidateLoading = false } },
|
||
async openContainer(candidate) { const containerNo = String(candidate?.containerNo || '').trim(); const sourceContainerBookingId = String(candidate?.sourceContainerBookingId || '').trim(); if (!containerNo || !sourceContainerBookingId || this.busy) return; this.error = null; try { this.validateReceivingWarehouse(candidate, `柜号 ${containerNo}`); if (!candidate.operable) { receiving.selected = candidate; this.detailOpen = true; return } const detail = await this.run('查询柜号', async () => this.validateReceivingWarehouse(await getReceivingContainer(containerNo, sourceContainerBookingId), `柜号 ${containerNo}`)); receiving.selected = { ...candidate, ...detail, sourceContainerBookingId, allPlansReady: candidate.allPlansReady, operable: candidate.operable, blockReason: candidate.blockReason }; this.detailOpen = true; playSuccessSound() } catch (error) { if (!await this.recoverCurrentAfterConflict(error, true)) { this.error = error; this.showToast(error.message || '查询柜号失败', 'error') } } },
|
||
cancelDetail() { this.detailOpen = false; receiving.selected = null },
|
||
async prepareContainer() { if (!this.selected?.containerNo || !this.selected?.sourceContainerBookingId || !this.selected?.allPlansReady || !this.selected?.operable || this.busy) return; const key = idempotencyKey('prepare', { sourceContainerBookingId: this.selected.sourceContainerBookingId, containerNo: this.selected.containerNo }); try { this.validateReceivingWarehouse(this.selected, `柜号 ${this.selected.containerNo}`); const value = await this.run('选择当前柜', async () => this.validateReceivingWarehouse(await prepareReceivingContainer(this.selected.containerNo, this.selected.sourceContainerBookingId, { idempotencyKey: key.value }), '新建接柜会话')); releaseIdempotencyKey(key.signature); await this.applyCurrentReceiving(value); this.detailOpen = false } catch (error) { if (!await this.recoverCurrentAfterConflict(error, true)) this.error = error } },
|
||
choosePhoto(index) { if (this.busy) return; uni.chooseImage({ count: 1, sourceType: ['camera'], success: ({ tempFilePaths, tempFiles }) => { const path = tempFilePaths?.[0]; const size = Number(tempFiles?.[0]?.size || 0); if (size > MAX_UNLOAD_PHOTO_BYTES) { const message = '照片不能超过10MB'; this.error = Object.assign(new Error(message), { code: 'A0400' }); this.showToast(message, 'error'); playErrorSound(); return } if (path) this.replacePhoto(index, path) } }) },
|
||
async replacePhoto(index, path) { const previous = receiving.photoSlots[index]; receiving.photoSlots.splice(index, 1, { ...(previous || {}), role: this.photoSteps[index].role, status: 'uploading', errorMessage: '' }); this.busy = true; this.error = null; try { const file = await uploadCurrentUnloadPhoto(path); receiving.photoSlots.splice(index, 1, { ...file, fileId: String(file.fileId), role: this.photoSteps[index].role, localPath: path, uploaded: true, status: 'success' }); this.showToast(`${this.photoSteps[index].label}上传成功`, 'success') } catch (error) { receiving.photoSlots.splice(index, 1, previous?.fileId ? { ...previous, status: 'error', errorMessage: error.message || '重拍失败,已保留原图' } : { role: this.photoSteps[index].role, localPath: path, uploaded: false, status: 'error', errorMessage: error.message || '上传失败,请重试' }); this.error = error; this.showToast(error.message || '照片上传失败', 'error'); playErrorSound() } finally { this.busy = false } },
|
||
invalidatePhotoPreview(photo) { if (!photo) return; photo.localPath = ''; photo.status = 'error'; photo.errorMessage = '照片加载失败,点按重试' },
|
||
async previewPhoto(index) { const photo = this.photoSlots[index]; if (!photo) return; try { const path = await restoreReceivingPhoto(photo, downloadCurrentUnloadPhoto); if (path) uni.previewImage({ urls: [path], current: path }) } catch (error) { this.error = error } },
|
||
async signContainer() { if (!this.photosReady) return; const photos = this.photoSlots.map((item, index) => ({ role: this.photoSteps[index].role, fileId: String(item.fileId) })); const key = idempotencyKey('sign', { containerNo: this.current.containerNo, photos }); try { const value = await this.run('签收当前柜', async () => this.validateReceivingWarehouse(await signCurrentReceiving({ photos, idempotencyKey: key.value }), '当前接柜会话')); releaseIdempotencyKey(key.signature); await this.applyCurrentReceiving(value); this.clearReceiptContext() } catch (_) {} },
|
||
clearReceiptContext() { receiving.resolvedCarton = null; receiving.resolvedPallet = null; this.cartonInput = ''; this.palletInput = ''; this.packageCount = ''; receiving.activeScanTarget = 'carton'; this.scheduleScanDrain() },
|
||
async resolveCarton(raw) { const cartonMark = String(raw?.detail?.value ?? raw ?? this.cartonInput).trim(); if (!cartonMark || !this._pageActive) return; const scanEpoch = this._scanEpoch || 0; if (this.busy) return this.handleHardwareScan(cartonMark); try { const outcome = await this.run('处理货物码', async () => { const value = await resolveCurrentCarton(cartonMark); if (value.operationMode !== 'DROPSHIP_PALLET') return { value }; if (!this._pageActive || scanEpoch !== (this._scanEpoch || 0)) throw new Error('页面已离开,请返回后重新扫码确认收货'); const payload = { sourcePalletNo: value.sourcePalletNo || cartonMark }; const key = idempotencyKey('whole-pallet-receipt', { containerNo: this.current?.containerNo, ...payload }); const result = await receiveCurrentWholePallet({ ...payload, idempotencyKey: key.value }); const current = this.validateReceivingWarehouse(await getCurrentReceiving(), '当前接柜会话'); releaseIdempotencyKey(key.signature); return { value, result, current } }); if (outcome.result) { receiving.lastReceipt = outcome.result; await this.applyCurrentReceiving(outcome.current); this.clearReceiptContext(); playSuccessSound(); return } receiving.resolvedCarton = outcome.value; receiving.resolvedPallet = null; this.cartonInput = outcome.value.cartonMark; this.palletInput = ''; this.packageCount = ''; receiving.activeScanTarget = 'pallet'; playSuccessSound() } catch (error) { console.error('[Overseas PDA cargo] failed', { cartonMark, message: error.message, code: error.code, requestId: error.requestId, traceId: error.traceId }) } },
|
||
async resolvePallet(raw) { const palletCode = String(raw?.detail?.value ?? raw ?? this.palletInput).trim(); if (!palletCode || !this.activeInbound || this.busy) return; try { const value = await this.run('校验托盘', () => resolveCurrentPallet(palletCode, this.activeInbound.inboundCode)); receiving.resolvedPallet = value; this.palletInput = value.palletCode; this.packageCount = Number(value.currentReceivedPackages || 0) || ''; if (!value.compatible) throw Object.assign(new Error('托盘不匹配'), { code: 'PALLET_INCOMPATIBLE' }); receiving.activeScanTarget = this.activeInbound.destinationType === 'FBA' ? 'quantity' : 'carton'; playSuccessSound() } catch (error) { if (!this.error) this.error = error } },
|
||
async submitReceipt() { const isFba = this.activeInbound?.destinationType === 'FBA'; if (isFba && Number(this.packageCount) > MAX_PACKAGES_PER_PALLET) { this.error = Object.assign(new Error(PALLET_CAPACITY_MESSAGE), { code: 'A0409' }); playErrorSound(); this.showToast(PALLET_CAPACITY_MESSAGE, 'error'); return } if (!this.canSubmitReceipt) return; const payload = isFba ? { inboundCode: this.activeInbound.inboundCode, palletCode: this.resolvedPallet.palletCode, packageCount: Number(this.packageCount), expectedPalletVersion: this.resolvedPallet.version } : { cartonMark: this.activeInbound.cartonMark, palletCode: this.resolvedPallet.palletCode }; const key = idempotencyKey(isFba ? 'bulk-receipt' : 'carton-receipt', { containerNo: this.current?.containerNo, groupKey: this.activeInbound.groupKey, ...payload }); try { const result = await this.run('提交实收', async () => { const receipt = isFba ? await receiveCurrentBulk({ ...payload, idempotencyKey: key.value }) : await receiveCurrentCarton({ ...payload, idempotencyKey: key.value }); const current = this.validateReceivingWarehouse(await getCurrentReceiving(), '当前接柜会话'); return { receipt, current } }); releaseIdempotencyKey(key.signature); receiving.lastReceipt = result.receipt; await this.applyCurrentReceiving(result.current); this.clearReceiptContext(); playSuccessSound() } catch (_) {} },
|
||
async completeContainer() { if (!this.canFinishOnsite || this.activeTarget === 'quantity' || this.scanQueue.length) return; const payload = { allowDifference: !this.allInboundsReconciled }; const key = idempotencyKey('complete', { containerNo: this.current.containerNo, ...payload }); try { const value = await this.run(this.allInboundsReconciled ? '完成入库' : '结束现场收货', async () => this.validateReceivingWarehouse(await completeCurrentReceiving({ ...payload, idempotencyKey: key.value }), '当前接柜会话')); releaseIdempotencyKey(key.signature); await this.applyCurrentReceiving(value) } catch (_) {} },
|
||
async finishAndReturn() { resetReceivingDraft(); await this.loadCandidates(false) },
|
||
showToast(message, type) { clearTimeout(this._toastTimer); this.toast = { visible: true, message, type }; this._toastTimer = setTimeout(() => { this.toast.visible = false }, 2600) }
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.reservation-hint{display:block;margin:6px 0;color:#64748b;font-size:12px;line-height:18px;overflow-wrap:anywhere}
|
||
.receiving-page{padding-bottom:calc(88px + env(safe-area-inset-bottom,0px))}.receiving-content{max-width:640px}.receiving-heading,.card-head,.progress-row,.list-toolbar{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.receiving-heading{margin-bottom:14px}.receiving-title,.receiving-subtitle,.container-no,.container-meta,.card-title,.section-label,.resolved-card text,.completed-card text,.error-card text,.prototype-tip{display:block}.receiving-title{font-size:21px;font-weight:800}.receiving-subtitle{margin-top:4px;color:#6f8194;font-size:12px;line-height:18px}.real-badge,.prototype-badge,.status-tag{flex-shrink:0;padding:5px 8px;border-radius:7px;font-size:11px;font-weight:700}.real-badge{color:#1668dc;background:#eaf2ff}.prototype-badge{color:#6f8194;background:#edf1f5}.scan-card,.container-card,.inbound-card,.inbound-list-card,.photo-card,.state-card,.empty-card,.completed-card{margin-bottom:12px;padding:14px;border:1px solid #dfe7ef;border-radius:14px;background:#fff;box-shadow:0 2px 10px rgba(16,24,40,.05)}.section-label,.card-title{margin-bottom:9px;color:#17324d;font-size:14px;font-weight:800}.primary-button,.secondary-button,.ghost-button{box-sizing:border-box;min-height:46px;border-radius:9px;font-size:13px;font-weight:700}.primary-button{width:100%;margin-top:10px;color:#fff;background:#1668dc}.secondary-button{width:100%;margin-top:8px;color:#1668dc;border:1px solid #9dbce9;background:#f4f8ff}.ghost-button{color:#496176;border:1px solid #d3dce6;background:#fff}.primary-button[disabled],.secondary-button[disabled]{color:#8b96a3;background:#e6eaf0;opacity:1}.list-toolbar{align-items:center;margin:8px 2px 12px;color:#6f8194;font-size:12px}.list-toolbar button{min-height:40px;padding:0 12px;color:#1668dc;background:transparent}.container-no{font-size:18px;font-weight:800}.container-meta{margin-top:4px;color:#6f8194;font-size:11px;line-height:17px}.status-wait{color:#a35b0c;background:#fff1dd}.status-current{color:#1668dc;background:#eaf2ff}.status-good{color:#10856f;background:#e2f6f0}.progress-row{margin-top:14px;color:#6f8194;font-size:12px}.progress-row strong{color:#10856f;font-size:16px}.progress-track{height:7px;margin-top:8px;overflow:hidden;border-radius:99px;background:#e7edf3}.progress-fill{height:100%;border-radius:inherit;background:#10856f}.empty-card,.state-card,.completed-card{display:flex;align-items:center;flex-direction:column;gap:10px;padding:34px 18px;color:#6f8194;text-align:center}.summary-card{border-color:#a9c8f3}.inbound-row{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 0;border-top:1px solid #edf1f5}.inbound-code{display:block;font-size:13px;font-weight:750}.inbound-count{flex-shrink:0;color:#6f8194;font-size:11px}.inbound-count strong{color:#10856f;font-size:17px}.success-card,.error-card{margin-bottom:12px;padding:11px 12px;border-radius:9px;font-size:12px;line-height:18px}.success-card{color:#0b6c5d;background:#e6f7f1}.error-card{color:#b42318;background:#fff1f0}.error-message{font-weight:700}.photo-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:14px}.photo-slot{display:flex;align-items:center;flex-direction:column;justify-content:center;min-height:112px;padding:8px;border:1px solid #d5dee8;border-radius:10px;color:#496176;font-size:11px;background:#f8fafc}.photo-slot.complete{border-color:#9cd0bf;color:#10856f;background:#effaf6}.photo-action{margin-top:6px;font-size:10px}.inbound-tabs{width:100%;white-space:nowrap}.inbound-tab{display:inline-flex;vertical-align:top;align-items:flex-start;flex-direction:column;min-width:190px;min-height:62px;margin-right:8px;padding:9px 11px;border:1px solid #d6e0eb;border-radius:9px;color:#496176;font-size:10px;text-align:left;background:#f8fafc}.inbound-tab text:first-child{margin-bottom:5px;color:#17324d;font-size:12px;font-weight:750}.inbound-tab.active{border:2px solid #1668dc;background:#f2f7ff}.inbound-tab.done{opacity:.65}.mode-banner{display:flex;justify-content:space-between;gap:8px;margin-bottom:13px;padding:10px;border-radius:9px;font-size:12px;font-weight:700}.mode-banner.fba{color:#8b5508;background:#fff1dd}.mode-banner.overseas{color:#08705f;background:#e2f6f0}.scan-card .section-label:not(:first-child){margin-top:14px}.resolved-card{margin-top:8px;padding:9px 11px;border-left:3px solid #10856f;color:#0b6c5d;font-size:11px;background:#eaf8f3}.resolved-card text:first-child{font-size:13px;font-weight:750}.quantity-input{box-sizing:border-box;width:100%;height:48px;padding:0 12px;border:2px solid #1668dc;border-radius:8px;font-size:18px;font-weight:750;background:#fff}.receipt-button{margin-top:14px}.sticky-action{position:fixed;z-index:8;right:10px;bottom:8px;left:10px;display:grid;grid-template-columns:1fr 2fr;gap:9px;max-width:620px;margin:auto;padding:10px;border:1px solid #dbe2ea;border-radius:14px;background:#fff;box-shadow:0 6px 20px rgba(31,41,55,.16)}.sticky-action.one{grid-template-columns:1fr}.sticky-action .primary-button{margin-top:0}.completed-icon{display:grid;place-items:center;width:56px;height:56px;border-radius:50%;color:#fff;font-size:30px;background:#10856f}.completed-title{color:#17324d;font-size:20px;font-weight:800}.prototype-tip{margin-top:10px;color:#8b610e;font-size:11px}
|
||
.blocked-card{display:flex;align-items:center;flex-direction:column;gap:10px;margin-bottom:12px;padding:34px 20px;border:1px solid #dfe7ef;border-radius:14px;color:#6f8194;text-align:center;line-height:20px;background:#fff;box-shadow:0 2px 10px rgba(16,24,40,.05)}.blocked-title{color:#17324d;font-size:17px;font-weight:800}.prototype-badge{color:#8b610e;background:#fff1dd}
|
||
.photo-grid{display:grid;grid-template-columns:1fr;gap:9px;margin-top:12px}
|
||
.photo-role{display:grid;min-width:0;min-height:94px;padding:10px;align-items:center;grid-template-columns:82px minmax(0,1fr) 78px;gap:10px;border:1px solid #d5dee8;border-radius:11px;background:#f8fafc}
|
||
.photo-role.complete{border-color:#9cd0bf;background:#f2fbf7}
|
||
.photo-preview{box-sizing:border-box;display:flex;width:82px;height:72px;overflow:hidden;align-items:center;justify-content:center;border:1px solid #dce4ec;border-radius:8px;background:#e9eef4}
|
||
.photo-preview.failed{border-color:#e8a09a;background:#fff7f6}
|
||
.photo-thumb{width:100%;height:100%}
|
||
.photo-placeholder{display:flex;width:100%;height:100%;align-items:center;flex-direction:column;justify-content:center;gap:3px;color:#8090a3;font-size:9px}
|
||
.photo-role-info{display:flex;min-width:0;flex-direction:column}
|
||
.photo-role-head{display:flex;align-items:center;justify-content:flex-start;gap:7px;color:#17324d;font-size:12px;font-weight:750}
|
||
.photo-required,.photo-valid{padding:2px 6px;border-radius:99px;font-size:9px;line-height:14px}
|
||
.photo-required{color:#a35b0c;background:#fff1dd}.photo-valid{color:#08705f;background:#dff5ed}
|
||
.photo-state{margin-top:6px;color:#6f8194;font-size:10px;line-height:15px;text-align:left}
|
||
.photo-error{margin-top:3px;overflow:hidden;color:#c9362b;font-size:9px;line-height:13px;text-overflow:ellipsis;white-space:nowrap}
|
||
.photo-actions{display:grid;grid-template-columns:1fr}
|
||
.photo-actions button{box-sizing:border-box;min-height:44px;padding:0 5px;border:1px solid #8fb8f5;border-radius:8px;color:#1668dc;font-size:11px;font-weight:700;line-height:44px;background:#fff}
|
||
.domestic-photo-card{border-color:#b9d3f4;background:#f9fbff}.domestic-photo-collapsible .card-head{cursor:pointer}.domestic-photo-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:7px;margin-top:11px}.domestic-photo-item{display:flex;min-width:0;align-items:center;flex-direction:column;gap:5px;color:#315777;font-size:10px;font-weight:700}.domestic-photo-thumb,.domestic-photo-placeholder{box-sizing:border-box;width:100%;height:66px;border:1px solid #cddbeb;border-radius:8px;background:#edf4fb}.domestic-photo-placeholder{display:flex;align-items:center;flex-direction:column;justify-content:center;gap:2px;padding:3px;color:#75869a;font-size:8px;line-height:11px;text-align:center}.domestic-photo-thumb{display:block}
|
||
.matched-inbound-card{margin:14px 0;padding:12px;border:1px solid #cfe0f2;border-radius:10px;background:#f8fbff}
|
||
.matched-inbound-card .mode-banner{margin-bottom:10px}
|
||
.matched-inbound-code{display:block;color:#17324d;font-size:15px;font-weight:800}
|
||
.matched-plan-flags{display:flex;flex-wrap:wrap;gap:5px;margin-top:7px}.matched-plan-flags text{padding:3px 7px;border-radius:99px;color:#617389;font-size:9px;background:#edf1f5}.matched-plan-flags .optional{color:#1668dc;background:#e7f0ff}.matched-plan-flags .urgent{color:#b54708;background:#fff0dc}.merge-option-hint{display:block;margin-top:7px;padding:7px 8px;border-radius:7px;color:#1668dc;font-size:10px;line-height:15px;background:#eef5ff}
|
||
.matched-inbound-progress{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px;margin-top:8px;color:#5f7185;font-size:10px}
|
||
.matched-inbound-progress text{padding:6px 4px;border-radius:6px;text-align:center;background:#edf3f9}
|
||
.error-card text{overflow-wrap:anywhere;word-break:break-word}
|
||
@media(max-width:360px){.photo-role{grid-template-columns:68px minmax(0,1fr) 64px;gap:7px;padding:8px}.photo-preview{width:68px;height:64px}.photo-actions button{min-height:34px;padding:0 2px;line-height:34px}.photo-state{font-size:9px;line-height:13px}.domestic-photo-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}
|
||
.receiving-page{padding-bottom:env(safe-area-inset-bottom,0px)}
|
||
.receiving-content{box-sizing:border-box;padding-bottom:calc(24px + env(safe-area-inset-bottom,0px))}
|
||
.receiving-content-with-action{padding-bottom:calc(152px + env(safe-area-inset-bottom,0px))}
|
||
.primary-button,.secondary-button,.ghost-button{display:flex;box-sizing:border-box;align-items:center;justify-content:center;padding-top:0;padding-bottom:0;line-height:normal;text-align:center}
|
||
.sticky-action button{display:flex;align-items:center;justify-content:center;line-height:normal;text-align:center}
|
||
.list-toolbar{height:28px;margin:0 2px 7px;align-items:center}.list-toolbar button{box-sizing:border-box;width:auto;min-width:48px;height:28px;min-height:28px;margin:0;padding:0 8px;border:1px solid #bdd1eb;border-radius:7px;color:#1668dc;font-size:11px;line-height:26px;background:#f7faff}.container-card-compact{padding:11px 13px;cursor:pointer;transition:background-color .12s ease,border-color .12s ease}.container-card-pressed{border-color:#93baf0;background:#f4f8ff}.container-card-actions{display:flex;flex-shrink:0;align-items:flex-end;flex-direction:column;gap:5px}.container-detail-link{box-sizing:border-box;min-height:24px;padding:0 4px;color:#1668dc;font-size:11px;font-weight:700;line-height:24px}.candidate-facts{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));margin-top:9px;padding:8px 0;border-top:1px solid #edf1f5;border-bottom:1px solid #edf1f5}.candidate-facts view{display:flex;align-items:center;flex-direction:column;border-right:1px solid #edf1f5}.candidate-facts view:last-child{border-right:0}.candidate-facts strong{color:#17324d;font-size:17px;line-height:21px}.candidate-facts text{margin-top:2px;color:#6f8194;font-size:10px}.candidate-destinations{display:block;margin-top:7px;overflow:hidden;color:#6f8194;font-size:11px;line-height:17px;text-overflow:ellipsis;white-space:nowrap}
|
||
.candidate-arrival{display:block;margin-top:3px;color:#718095;font-size:10px;line-height:15px}.candidate-progress-copy{display:block;margin-top:7px;color:#435a72;font-size:10px;line-height:16px}.candidate-block-reason{display:block;margin-top:6px;padding:6px 8px;border-radius:7px;color:#73551b;font-size:10px;line-height:16px;background:#f0e9dc}.container-card-disabled{border-color:#d9dee5;background:#f1f3f5;box-shadow:none}.container-card-disabled .container-no{color:#435a72}.container-card-disabled .container-detail-link{color:#526174;font-size:13px}.container-card-disabled .candidate-arrival{color:#607287;font-size:12px;line-height:18px}.candidate-pending-summary{display:block;margin-top:8px;color:#435a72;font-size:14px;font-weight:600;line-height:22px}.status-disabled{color:#526174;background:#e1e5e9}.readonly-candidate-card{padding:13px;border:1px solid #d9dee5;border-radius:13px;background:#f1f3f5}.readonly-candidate-card .candidate-block-reason{margin-top:9px}
|
||
.candidate-receiving-warehouse{display:block;margin-top:7px;color:#315777;font-size:11px;font-weight:700;line-height:17px}
|
||
</style>
|