优化
This commit is contained in:
+2
-2
@@ -1,11 +1,11 @@
|
||||
import { overseasDownload, overseasRequest, overseasUpload } from '@/api/overseas.js'
|
||||
import { normalizeReceivingCandidate, normalizeReceivingSession } from '@/api/receiving-adapter.js'
|
||||
import { normalizeReceivingCandidate, normalizeReceivingSession, normalizeReceivingWarehouseProfile } from '@/api/receiving-adapter.js'
|
||||
|
||||
const RECEIVING_BASE = '/pda/v1/last-leg/receiving-containers'
|
||||
const current = (suffix = '') => `${RECEIVING_BASE}/current${suffix}`
|
||||
|
||||
export const listReceivingCandidates = async () => (await overseasRequest('GET', `${RECEIVING_BASE}/candidates`)).map(normalizeReceivingCandidate)
|
||||
export const getCurrentWarehouseProfile = () => overseasRequest('GET', `${RECEIVING_BASE}/warehouse-profile`)
|
||||
export const getCurrentWarehouseProfile = async () => normalizeReceivingWarehouseProfile(await overseasRequest('GET', `${RECEIVING_BASE}/warehouse-profile`))
|
||||
export const getReceivingContainer = async (containerNo) => normalizeReceivingSession(await overseasRequest('GET', `${RECEIVING_BASE}/${encodeURIComponent(containerNo)}`))
|
||||
export const prepareReceivingContainer = async (containerNo, request) => normalizeReceivingSession(await overseasRequest('POST', `${RECEIVING_BASE}/${encodeURIComponent(containerNo)}/prepare`, request))
|
||||
export const uploadCurrentUnloadPhoto = (filePath) => overseasUpload(current('/unload-photos/files'), filePath)
|
||||
|
||||
@@ -1,6 +1,40 @@
|
||||
export const RECEIVING_PHOTO_ROLES = ['LICENSE_PLATE', 'CONTAINER_SEAL', 'DOOR_OPEN']
|
||||
|
||||
const number = (value) => Math.max(0, Number(value || 0))
|
||||
const text = (value) => typeof value === 'string' ? value.trim() : ''
|
||||
|
||||
function warehouseContractError(message) {
|
||||
return Object.assign(new Error(message), { code: 'RECEIVING_WAREHOUSE_CONTRACT_INVALID' })
|
||||
}
|
||||
|
||||
export function normalizeReceivingWarehouseProfile(value = {}) {
|
||||
const profile = {
|
||||
warehouseId: value.warehouseId === null || value.warehouseId === undefined ? '' : String(value.warehouseId),
|
||||
warehouseCode: text(value.warehouseCode),
|
||||
warehouseName: text(value.warehouseName)
|
||||
}
|
||||
if (!profile.warehouseId || !profile.warehouseCode || !profile.warehouseName) {
|
||||
throw warehouseContractError('当前 PDA 绑定仓信息不完整,请联系管理员检查账号绑定')
|
||||
}
|
||||
return profile
|
||||
}
|
||||
|
||||
export function assertReceivingWarehouse(value, profile, label = '当前柜') {
|
||||
const expectedCode = text(profile?.warehouseCode)
|
||||
const expectedName = text(profile?.warehouseName)
|
||||
const actualCode = text(value?.overseasWarehouseCode)
|
||||
const actualName = text(value?.overseasWarehouseName)
|
||||
if (!expectedCode || !expectedName) {
|
||||
throw warehouseContractError('当前 PDA 绑定仓信息不完整,暂停接柜作业')
|
||||
}
|
||||
if (!actualCode || !actualName) {
|
||||
throw warehouseContractError(`${label}缺少已分配海外接柜仓信息,暂停操作并请联系管理员`)
|
||||
}
|
||||
if (actualCode !== expectedCode) {
|
||||
throw Object.assign(new Error(`${label}已分配至其他海外接柜仓,当前仓不可操作`), { code: 'RECEIVING_WAREHOUSE_MISMATCH' })
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function normalizeReceivingInbound(value = {}) {
|
||||
return {
|
||||
@@ -17,6 +51,8 @@ export function normalizeReceivingCandidate(value = {}) {
|
||||
...value,
|
||||
sourceContainerBookingId: typeof value.sourceContainerBookingId === 'string' ? value.sourceContainerBookingId.trim() : '',
|
||||
containerNo: String(value.containerNo || '').trim(),
|
||||
overseasWarehouseCode: text(value.overseasWarehouseCode),
|
||||
overseasWarehouseName: text(value.overseasWarehouseName),
|
||||
plannedArrivalAt: typeof value.plannedArrivalAt === 'string' && value.plannedArrivalAt.trim() ? value.plannedArrivalAt.trim() : null,
|
||||
totalOrderCount: number(value.totalOrderCount),
|
||||
planCount: number(value.planCount),
|
||||
@@ -42,6 +78,8 @@ export function normalizeReceivingSession(value) {
|
||||
: []
|
||||
return {
|
||||
...value,
|
||||
overseasWarehouseCode: text(value.overseasWarehouseCode),
|
||||
overseasWarehouseName: text(value.overseasWarehouseName),
|
||||
photos: RECEIVING_PHOTO_ROLES.map((role) => photos.find((photo) => photo?.role === role && photo?.fileId)).filter(Boolean),
|
||||
totalPackages: number(value.totalPackages),
|
||||
receivedPackages: number(value.receivedPackages),
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<text class="receiving-summary-status">{{ statusLabel }}</text>
|
||||
</view>
|
||||
<view class="receiving-summary-context">
|
||||
<text class="receiving-summary-warehouse">收货仓:{{ session?.overseasWarehouseName || '—' }}</text>
|
||||
<text class="receiving-summary-warehouse">海外接柜仓:{{ receivingWarehouseLabel }}</text>
|
||||
<view class="receiving-summary-destinations"><text class="destination-label">目的仓:</text><view class="destination-tags"><text v-for="value in destinationValues" :key="value">{{ value }}</text></view></view>
|
||||
</view>
|
||||
<view class="receiving-summary-metrics">
|
||||
@@ -22,6 +22,7 @@
|
||||
export default {
|
||||
props: { session: { type: Object, default: null } },
|
||||
computed: {
|
||||
receivingWarehouseLabel() { const code = String(this.session?.overseasWarehouseCode || '').trim(); const name = String(this.session?.overseasWarehouseName || '').trim(); return code && name ? `${code} · ${name}` : '—' },
|
||||
inboundCodeCount() { return Array.isArray(this.session?.inbounds) ? this.session.inbounds.length : 0 },
|
||||
plannedPalletCount() { return (this.session?.inbounds || []).reduce((sum, item) => sum + Math.max(0, Number(item?.plannedPalletCount || 0)), 0) },
|
||||
destinationValues() { const values = (this.session?.inbounds || []).map((item) => item?.fbaWarehouseCode || item?.destinationWarehouseName || item?.destinationWarehouseCode).filter(Boolean); const unique = [...new Set(values)]; return unique.length ? unique : ['—'] },
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<view v-if="loading" class="state-card"><uni-load-more status="loading" /><text>正在恢复当前柜作业…</text></view>
|
||||
|
||||
<template v-else-if="phase === 'candidates'">
|
||||
<view class="warehouse-scope-card"><text>当前 PDA 绑定仓</text><strong>{{ boundWarehouseLabel }}</strong><text>仅显示已分配到当前仓的集装箱</text></view>
|
||||
<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>
|
||||
@@ -17,15 +18,16 @@
|
||||
<view 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 class="candidate-progress-copy">已生成 {{ item.planCount }} 个入库计划,{{ item.pendingPlanCount }} 个未操作<template v-if="item.missingInboundCodeCount">,其中 {{ item.missingInboundCodeCount }} 个缺入仓码</template></text>
|
||||
<text v-if="!item.operable" class="candidate-block-reason">{{ item.blockReason || '当前柜入库计划尚未齐全' }}</text>
|
||||
<text v-if="item.destinationCodes?.length" class="candidate-destinations">仓码:{{ item.destinationCodes.join(' / ') }}</text>
|
||||
<text class="candidate-receiving-warehouse">海外接柜仓:{{ receivingWarehouseLabel(item) }}</text>
|
||||
<text v-if="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="lock-notice">选择后账号将锁定当前柜,完成前不能切换其他柜号。</view>
|
||||
<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>
|
||||
<view class="lock-notice">接柜仓由平台预先分配;确认后只建立当前账号的接柜会话,完成前不能切柜。</view>
|
||||
<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">
|
||||
@@ -33,6 +35,7 @@
|
||||
<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="ghost-button" :disabled="busy" @tap="cancelDetail">返回列表</button></view>
|
||||
</template>
|
||||
@@ -111,12 +114,12 @@
|
||||
import PortalHeader from '@/components/PortalHeader.vue'
|
||||
import ReceivingSessionSummary from '@/components/ReceivingSessionSummary.vue'
|
||||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||||
import { isNoCurrentReceiving, isReceivingConflict, RECEIVING_PHOTO_ROLES } from '@/api/receiving-adapter.js'
|
||||
import { session, requireSession } from '@/stores/session.js'
|
||||
import { assertReceivingWarehouse, isNoCurrentReceiving, isReceivingConflict, RECEIVING_PHOTO_ROLES } from '@/api/receiving-adapter.js'
|
||||
import { applyWarehouseProfile, session, requireSession } from '@/stores/session.js'
|
||||
import { idempotencyKey, receiving, receivingPhase, receivingProgress, releaseIdempotencyKey, resetReceivingDraft, setCurrentReceiving } from '@/stores/receiving.js'
|
||||
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
|
||||
import { playErrorSound, playSuccessSound } from '@/utils/audio-feedback.js'
|
||||
import { completeCurrentReceiving, downloadCurrentUnloadPhoto, getCurrentReceiving, getReceivingContainer, listReceivingCandidates, prepareReceivingContainer, receiveCurrentBulk, receiveCurrentCarton, resolveCurrentCarton, resolveCurrentPallet, signCurrentReceiving, uploadCurrentUnloadPhoto } from '@/api/inbound.js'
|
||||
import { completeCurrentReceiving, downloadCurrentUnloadPhoto, getCurrentReceiving, getCurrentWarehouseProfile, getReceivingContainer, listReceivingCandidates, prepareReceivingContainer, receiveCurrentBulk, receiveCurrentCarton, resolveCurrentCarton, resolveCurrentPallet, signCurrentReceiving, uploadCurrentUnloadPhoto } from '@/api/inbound.js'
|
||||
const MAX_UNLOAD_PHOTO_BYTES = 10 * 1024 * 1024
|
||||
|
||||
export default {
|
||||
@@ -132,6 +135,7 @@
|
||||
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 && count <= Number(this.activeInbound.pendingPackages) } return Boolean(this.activeInbound.cartonMark) },
|
||||
allInboundsReconciled() { const values = this.current?.inbounds || []; return values.length > 0 && values.every((item) => Number(item.pendingPackages) === 0) },
|
||||
boundWarehouseLabel() { const code = String(this.user?.warehouseCode || '').trim(); const name = String(this.user?.warehouseName || '').trim(); return code && name ? `${code} · ${name}` : '海外仓未关联' },
|
||||
pageTitle() { return { candidates: '待卸货集装箱', detail: '柜号详情', sign: '拆柜拍照签收', receive: '当前柜收货', completed: '入库完成' }[this.phase] },
|
||||
pageSubtitle() { return { candidates: '选择集装箱后拍摄 3 张照片完成签收', detail: '核对柜号与整柜计划箱数', sign: '完成现场取证后进入收货', receive: '扫描箱码定位入仓码后开始收货', completed: '库存与柜级进度已由服务端核平' }[this.phase] }
|
||||
},
|
||||
@@ -143,28 +147,31 @@
|
||||
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 photo?.fileId ? '点击左侧照片可查看大图' : '请拍摄现场照片' },
|
||||
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) { console.info('[Overseas PDA scan] dispatch', { value, phase: this.phase, target: this.activeTarget, matchedInbound: this.activeInbound?.inboundCode }); if (!this.inboundMode) return this.showToast('该功能缺少海外 PDA 专用接口,扫码未提交', 'warning'); if (this.phase === 'candidates') return this.showToast('请直接点击待卸货集装箱进入详情', 'warning'); if (this.phase !== 'receive') return; if (this.activeTarget === 'pallet' && this.activeInbound) { this.palletInput = value; return this.resolvePallet(value) } this.cartonInput = value; return this.resolveCarton(value) },
|
||||
async run(action, work) { this.busy = true; this.error = null; try { const result = await work(); this.showToast(`${action}成功`, 'success'); return result } catch (error) { this.error = error; playErrorSound(); this.showToast(error.message || `${action}失败`, 'error'); throw error } finally { this.busy = false } },
|
||||
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) => { photo.status = 'restoring'; try { photo.localPath = await downloadCurrentUnloadPhoto(photo.downloadPath); photo.status = 'success' } catch (error) { photo.status = 'error'; photo.errorMessage = error.message || '照片加载失败'; this.error = error } })) },
|
||||
async applyCurrentReceiving(value) { setCurrentReceiving(value); await this.restorePhotoPreviews(); this.setScanTargetForCurrent(value) },
|
||||
async bootstrap() { if (this.loading) return; this.loading = true; this.error = null; this.detailOpen = false; try { const current = await getCurrentReceiving(); await this.applyCurrentReceiving(current); this.candidateError = null; this.bootstrapped = true } catch (error) { if (isNoCurrentReceiving(error)) { resetReceivingDraft(); await this.loadCandidates(false); this.bootstrapped = true } else this.candidateError = error } finally { this.loading = false } },
|
||||
async applyCurrentReceiving(value) { this.validateReceivingWarehouse(value, '当前接柜会话'); setCurrentReceiving(value); await this.restorePhotoPreviews(); 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) } 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) { if (this.candidateLoading) return; this.candidateLoading = true; this.candidateError = null; this.error = null; try { receiving.candidates = await listReceivingCandidates(); if (feedback) this.showToast('刷新待卸柜成功', 'success') } catch (error) { this.candidateError = error; if (feedback) { playErrorSound(); this.showToast(error.message || '刷新待卸柜失败', 'error') } } finally { this.candidateLoading = false } },
|
||||
async openContainer(candidate) { const containerNo = String(candidate?.containerNo || '').trim(); if (!containerNo || this.busy) return; this.error = null; if (!candidate.operable) { receiving.selected = candidate; this.detailOpen = true; return } try { const detail = await this.run('查询柜号', () => getReceivingContainer(containerNo)); receiving.selected = { ...candidate, ...detail, allPlansReady: candidate.allPlansReady, operable: candidate.operable, blockReason: candidate.blockReason }; this.detailOpen = true; playSuccessSound() } catch (error) { await this.recoverCurrentAfterConflict(error) } },
|
||||
async loadCandidates(feedback = true) { if (this.candidateLoading) return; this.candidateLoading = true; this.candidateError = null; this.error = null; try { 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(); if (!containerNo || 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), `柜号 ${containerNo}`)); receiving.selected = { ...candidate, ...detail, allPlansReady: candidate.allPlansReady, operable: candidate.operable, blockReason: candidate.blockReason }; this.detailOpen = true; playSuccessSound() } catch (error) { if (!await this.recoverCurrentAfterConflict(error)) { this.error = error; this.showToast(error.message || '查询柜号失败', 'error') } } },
|
||||
cancelDetail() { this.detailOpen = false; receiving.selected = null },
|
||||
async prepareContainer() { if (!this.selected?.containerNo || !this.selected?.allPlansReady || !this.selected?.operable || this.busy) return; const key = idempotencyKey('prepare', { containerNo: this.selected.containerNo }); try { const value = await this.run('选择当前柜', () => prepareReceivingContainer(this.selected.containerNo, { idempotencyKey: key.value })); releaseIdempotencyKey(key.signature); setCurrentReceiving(value); this.setScanTargetForCurrent(value); this.detailOpen = false } catch (error) { await this.recoverCurrentAfterConflict(error, true) } },
|
||||
async prepareContainer() { if (!this.selected?.containerNo || !this.selected?.allPlansReady || !this.selected?.operable || this.busy) return; const key = idempotencyKey('prepare', { 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, { 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 } },
|
||||
previewPhoto(index) { const path = this.photoSlots[index]?.localPath; if (!path) return; uni.previewImage({ urls: [path], current: path }) },
|
||||
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('签收当前柜', () => signCurrentReceiving({ photos, idempotencyKey: key.value })); releaseIdempotencyKey(key.signature); setCurrentReceiving(value); this.clearReceiptContext() } catch (_) {} },
|
||||
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 = 1; receiving.activeScanTarget = 'carton' },
|
||||
async resolveCarton(raw) { const cartonMark = String(raw?.detail?.value ?? raw ?? this.cartonInput).trim(); if (!cartonMark) return; if (this.busy) { console.warn('[Overseas PDA carton] ignored while busy', { cartonMark }); return this.showToast('正在处理上一笔扫描,请稍后重试', 'warning') } console.info('[Overseas PDA carton] resolving', { cartonMark, containerNo: this.current?.containerNo }); try { const value = await this.run('解析箱码', () => resolveCurrentCarton(cartonMark)); console.info('[Overseas PDA carton] resolved', { cartonMark: value.cartonMark, inboundCode: value.inboundCode, destinationType: value.destinationType, pendingPackages: value.pendingPackages }); receiving.resolvedCarton = value; receiving.resolvedPallet = null; this.cartonInput = value.cartonMark; this.palletInput = ''; this.packageCount = 1; playSuccessSound() } catch (error) { console.error('[Overseas PDA carton] 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; if (!value.compatible) throw Object.assign(new Error('托盘不匹配'), { code: 'PALLET_INCOMPATIBLE' }); playSuccessSound() } catch (error) { if (!this.error) this.error = error } },
|
||||
async submitReceipt() { if (!this.canSubmitReceipt) return; const isFba = this.activeInbound.destinationType === 'FBA'; const payload = isFba ? { inboundCode: this.activeInbound.inboundCode, palletCode: this.resolvedPallet.palletCode, packageCount: Number(this.packageCount) } : { cartonMark: this.activeInbound.cartonMark, palletCode: this.resolvedPallet.palletCode }; const key = idempotencyKey(isFba ? 'bulk-receipt' : 'carton-receipt', payload); try { const result = await this.run('提交实收', () => isFba ? receiveCurrentBulk({ ...payload, idempotencyKey: key.value }) : receiveCurrentCarton({ ...payload, idempotencyKey: key.value })); releaseIdempotencyKey(key.signature); receiving.lastReceipt = result; setCurrentReceiving(await getCurrentReceiving()); this.clearReceiptContext(); playSuccessSound() } catch (_) {} },
|
||||
async completeContainer() { if (!this.allInboundsReconciled) return; const key = idempotencyKey('complete', { containerNo: this.current.containerNo }); try { const value = await this.run('完成入库', () => completeCurrentReceiving({ idempotencyKey: key.value })); releaseIdempotencyKey(key.signature); setCurrentReceiving(value) } catch (_) {} },
|
||||
async submitReceipt() { if (!this.canSubmitReceipt) return; const isFba = this.activeInbound.destinationType === 'FBA'; const payload = isFba ? { inboundCode: this.activeInbound.inboundCode, palletCode: this.resolvedPallet.palletCode, packageCount: Number(this.packageCount) } : { cartonMark: this.activeInbound.cartonMark, palletCode: this.resolvedPallet.palletCode }; const key = idempotencyKey(isFba ? 'bulk-receipt' : 'carton-receipt', payload); try { const result = await this.run('提交实收', () => isFba ? receiveCurrentBulk({ ...payload, idempotencyKey: key.value }) : receiveCurrentCarton({ ...payload, idempotencyKey: key.value })); const current = this.validateReceivingWarehouse(await getCurrentReceiving(), '当前接柜会话'); releaseIdempotencyKey(key.signature); receiving.lastReceipt = result; await this.applyCurrentReceiving(current); this.clearReceiptContext(); playSuccessSound() } catch (_) {} },
|
||||
async completeContainer() { if (!this.allInboundsReconciled) return; const key = idempotencyKey('complete', { containerNo: this.current.containerNo }); try { const value = await this.run('完成入库', async () => this.validateReceivingWarehouse(await completeCurrentReceiving({ 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) }
|
||||
}
|
||||
@@ -202,4 +209,5 @@
|
||||
.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,.container-card-disabled .candidate-facts strong{color:#778391}.container-card-disabled .container-detail-link{color:#788797}.container-card-disabled .candidate-progress-copy,.container-card-disabled .candidate-arrival,.container-card-disabled .candidate-destinations{color:#87919d}.status-disabled{color:#788491;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}
|
||||
.warehouse-scope-card{display:flex;flex-direction:column;gap:3px;margin-bottom:10px;padding:10px 12px;border:1px solid #b8d2f3;border-radius:10px;color:#617389;font-size:10px;line-height:15px;background:#f2f7ff}.warehouse-scope-card strong{color:#17324d;font-size:13px}.candidate-receiving-warehouse{display:block;margin-top:7px;color:#315777;font-size:11px;font-weight:700;line-height:17px}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user