diff --git a/src/pages/operation/operation.vue b/src/pages/operation/operation.vue index 8d5aa7a..cb0973e 100644 --- a/src/pages/operation/operation.vue +++ b/src/pages/operation/operation.vue @@ -157,9 +157,9 @@ 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) { 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 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) { 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 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(); 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 { 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 } }, diff --git a/tests/project-contract.test.mjs b/tests/project-contract.test.mjs index 4d00b14..2f342d0 100644 --- a/tests/project-contract.test.mjs +++ b/tests/project-contract.test.mjs @@ -79,7 +79,7 @@ test('login has no committed demo credentials and inbound operation implements t assert.match(operation, /@tap="loadCandidates"[^>]*>重新加载/) assert.doesNotMatch(operation, /扫描柜号|查询柜号<\/button>|v-model="containerInput"/) assert.match(operation, /phase === 'candidates'[^\n]*请直接点击待卸货集装箱进入详情/) - assert.match(operation, /async loadCandidates\(feedback = true\)[\s\S]*if \(this\.candidateLoading\) return[\s\S]*this\.candidateError = error/) + assert.match(operation, /async loadCandidates\(feedback = true, refreshWarehouse = true\)[\s\S]*if \(this\.candidateLoading\) return[\s\S]*this\.candidateError = error/) assert.match(operation, /async prepareContainer\(\)[\s\S]*this\.busy\) return/) assert.match(operation, /prepareReceivingContainer[\s\S]*releaseIdempotencyKey\(key\.signature\)/) assert.match(overseas, /replaceTokens|refreshOverseasToken/) @@ -147,8 +147,10 @@ test('Chinese English and Spanish locale selection is persisted from profile wit }) test('home reuses the persisted warehouse profile instead of requesting it on every show', async () => { - const [home, inbound] = await Promise.all([read('../src/pages/home/home.vue'), read('../src/api/inbound.js')]) + const [home, inbound, operation] = await Promise.all([read('../src/pages/home/home.vue'), read('../src/api/inbound.js'), read('../src/pages/operation/operation.vue')]) assert.match(home, /if \(!this\.user\?\.warehouseCode \|\| !this\.user\?\.warehouseName\) this\.loadWarehouseProfile\(\)/) assert.match(inbound, /let warehouseProfileRequest = null/) assert.match(inbound, /if \(!warehouseProfileRequest\)[\s\S]*\.finally\(\(\) => \{ warehouseProfileRequest = null \}\)/) + assert.match(operation, /await this\.refreshBoundWarehouse\(\)[\s\S]*await this\.loadCandidates\(false, false\)/) + assert.match(operation, /if \(refreshWarehouse\) await this\.refreshBoundWarehouse\(\)/) })