diff --git a/AGENTS.md b/AGENTS.md
index 4e0f08d..a4dcfa6 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -50,6 +50,7 @@
## 海外业务接口边界
- `src/api/inbound.js` 已接入待卸柜候选、柜详情/prepare、current 恢复、三类照片签收、统一箱码定位入仓码、实物托盘解析、FBA bulk/OVERSEAS carton 收货及整柜完成;FBA bulk 不提交 `skuCode`,库存分配由服务端按计划明细完成;响应与 404/409 判定集中在 `src/api/receiving-adapter.js`,双 Token 刷新与海外 401 单次重放由 `src/api/overseas.js` 统一处理。
+- 待卸柜候选保留服务端返回的未齐全柜并严格消费 `allPlansReady`、`operable`、`blockReason`、`plannedArrivalAt` 与柜级计划统计;`operable=false` 只能查看只读摘要,不得调用 prepare。候选顺序沿用服务端计划到仓时间排序,前端不得用入仓码/计划数组长度自行推导就绪或把预计到港冒充计划到仓。
- `src/api/pallet.js` 只调用 Lastleg 托盘专用 PDA Controller;前端提交服务端扫码返回的 `palletVersion`、`positionVersion` 和随机幂等键,不提交客户、仓库或库位标识。409 后必须重新扫码获取最新版本,禁止用旧库存快照继续作业。
- 请求身份和资源范围只取 Token 与 current 会话;前端不得提交 `customerId`、`planNo`、`allocationId`、`warehouseId` 或 `locationId`。409 必须引导账号恢复当前柜,完成前不得切柜。
- 签收照片固定为三个角色且每个角色只有一个当前文件;上传成功后使用服务端 `downloadPath` 在当前会话内鉴权读取。更换失败时保留原 `fileId` 和可提交状态,无原图的失败槽不得计入 `photosReady`;本地临时路径仅用于本次设备预览,不作为跨会话文件事实。
diff --git a/README.md b/README.md
index 6683547..5fa07f4 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,7 @@ npm run build:app
- 已复用并内置:登录壳、PDA 样式、扫码广播、声音反馈、导航、网络检测、图标及 `uni_modules`。
- 已隔离:国内集装箱拣货、装柜证据、国内 Firstleg API、国内任务状态和国内本地草稿。
- 入仓页面已接待卸柜候选、柜详情与账号锁柜、三类角色照片签收、current 恢复、统一箱码与既有物理托盘解析、FBA 按箱数和 OVERSEAS 逐箱收货、逐入仓码核平及整柜完成的真实 API;收货页不预列柜内入仓码,箱码解析成功后只显示本次命中的入仓码;409 会引导返回当前柜,错误会保留业务码、requestId 与 traceId。
+- 候选列表同时显示已齐全与未齐全柜,展示计划到仓时间、总订单、入仓码、已生成计划、未操作和缺入仓码数量;未齐全柜置灰但可查看服务端阻断原因,只有服务端 `allPlansReady=true && operable=true` 才能 prepare。
- 托盘管理已接 `/api/pda/v1/last-leg/pallet-operations/**`:支持扫描查询当前仓实物托盘及库存内容,并按服务端版本执行组托、拆托、换托和解绑;不同目的仓不能合托,冻结或处理中的库存由服务端拒绝。
- 入仓请求不手填客户、计划、分配、仓库或库位标识;身份与当前柜范围由 Token、Gateway 和服务端会话确定。页面同时支持 PDA 广播扫码与 H5 人工输入。
- 登录、恢复和刷新会 fail-closed 校验国内/海外双 Token 的同一平台操作主体、`OVERSEAS_WAREHOUSE_PDA_OPERATOR`、`lastleg:pda:operate` 及同一冻结海外仓;并发海外 401 共享一次国内 refresh,校验完成后原子替换双 Token并将原请求或照片上传重放一次。
diff --git a/src/api/receiving-adapter.js b/src/api/receiving-adapter.js
index 0249dc8..7b1cf6d 100644
--- a/src/api/receiving-adapter.js
+++ b/src/api/receiving-adapter.js
@@ -15,14 +15,23 @@ export function normalizeReceivingInbound(value = {}) {
export function normalizeReceivingCandidate(value = {}) {
return {
...value,
+ sourceContainerBookingId: typeof value.sourceContainerBookingId === 'string' ? value.sourceContainerBookingId.trim() : '',
containerNo: String(value.containerNo || '').trim(),
+ plannedArrivalAt: typeof value.plannedArrivalAt === 'string' && value.plannedArrivalAt.trim() ? value.plannedArrivalAt.trim() : null,
+ totalOrderCount: number(value.totalOrderCount),
planCount: number(value.planCount),
inboundCodeCount: number(value.inboundCodeCount),
+ missingInboundCodeCount: number(value.missingInboundCodeCount),
+ plannedInboundCodeCount: number(value.plannedInboundCodeCount),
+ pendingPlanCount: number(value.pendingPlanCount),
destinationCodes: Array.isArray(value.destinationCodes) ? value.destinationCodes.filter(Boolean) : [],
plannedPalletCount: number(value.plannedPalletCount),
totalPackages: number(value.totalPackages),
receivedPackages: number(value.receivedPackages),
- currentAccount: value.currentAccount === true
+ currentAccount: value.currentAccount === true,
+ allPlansReady: value.allPlansReady === true,
+ operable: value.operable === true,
+ blockReason: typeof value.blockReason === 'string' ? value.blockReason.trim() : ''
}
}
diff --git a/src/pages/operation/operation.vue b/src/pages/operation/operation.vue
index 964555c..964f5ee 100644
--- a/src/pages/operation/operation.vue
+++ b/src/pages/operation/operation.vue
@@ -12,17 +12,30 @@
{{ candidateError.message || '待卸货集装箱加载失败' }}code={{ candidateError.code }}requestId={{ candidateError.requestId }}traceId={{ candidateError.traceId }}
正在刷新待卸货集装箱…
暂无待卸货集装箱
-
- {{ item.containerNo }}{{ statusLabel(item.status) }}{{ item.currentAccount ? '继续' : '详情' }} ›
- {{ item.inboundCodeCount }}入仓码{{ item.totalPackages }}箱{{ item.plannedPalletCount }}计划托盘{{ item.destinationCodes?.length || 0 }}目的仓
+
+ {{ item.containerNo }}计划到仓:{{ formatPlannedArrival(item.plannedArrivalAt) }}{{ item.operable ? statusLabel(item.status) : '待齐全' }}{{ item.operable ? (item.currentAccount ? '继续' : '详情') : '只读' }} ›
+ {{ item.totalOrderCount }}总订单{{ item.inboundCodeCount }}入仓码{{ item.planCount }}已生成计划{{ item.pendingPlanCount }}未操作
+ 已生成 {{ item.planCount }} 个入库计划,{{ item.pendingPlanCount }} 个未操作,其中 {{ item.missingInboundCodeCount }} 个缺入仓码
+ {{ item.blockReason || '当前柜入库计划尚未齐全' }}
仓码:{{ item.destinationCodes.join(' / ') }}
-
- 选择后账号将锁定当前柜,完成前不能切换其他柜号。
-
+
+
+ 选择后账号将锁定当前柜,完成前不能切换其他柜号。
+
+
+
+
+ {{ selected?.containerNo }}计划到仓:{{ formatPlannedArrival(selected?.plannedArrivalAt) }}暂不可操作
+ {{ selected?.totalOrderCount }}总订单{{ selected?.inboundCodeCount }}入仓码{{ selected?.planCount }}已生成计划{{ selected?.pendingPlanCount }}未操作
+ 已生成 {{ selected?.planCount }} 个入库计划,{{ selected?.pendingPlanCount }} 个未操作,其中 {{ selected.missingInboundCodeCount }} 个缺入仓码
+ {{ selected?.blockReason || '当前柜入库计划尚未齐全' }}
+
+
+
@@ -128,6 +141,7 @@
methods: {
statusLabel(status) { return ({ WAIT_SIGN: '待签收', SIGNED: '已签收', RECEIVING: '收货中', COMPLETED: '已完成' })[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 || '海外仓') },
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 },
@@ -137,11 +151,11 @@
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 recoverCurrentAfterConflict(error) { if (!isReceivingConflict(error)) return false; try { const current = await getCurrentReceiving(); await this.applyCurrentReceiving(current); this.detailOpen = false; this.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.error = null; this.showToast('该柜已被其他账号选择,列表已刷新', 'warning'); return true } this.error = error; return 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(raw) { const containerNo = String(raw?.detail?.value ?? raw ?? '').trim(); if (!containerNo || this.busy) return; try { receiving.selected = await this.run('查询柜号', () => getReceivingContainer(containerNo)); this.detailOpen = true; playSuccessSound() } catch (error) { await this.recoverCurrentAfterConflict(error) } },
+ 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) } },
cancelDetail() { this.detailOpen = false; receiving.selected = null },
- async prepareContainer() { if (!this.selected?.containerNo || 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) } },
+ 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) } },
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 }) },
@@ -187,4 +201,5 @@
.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,.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}
diff --git a/tests/multi-plan-receiving.test.mjs b/tests/multi-plan-receiving.test.mjs
index 52441d8..4662e75 100644
--- a/tests/multi-plan-receiving.test.mjs
+++ b/tests/multi-plan-receiving.test.mjs
@@ -20,16 +20,42 @@ const inbounds = Array.from({ length: 15 }, (_, index) => ({
test('one container candidate aggregates fifteen independent one-inbound plans', () => {
const candidate = normalizeReceivingCandidate({
- containerNo: 'C10000000', status: 'WAIT_SIGN', planCount: 15, inboundCodeCount: 15,
+ sourceContainerBookingId: '2093166483653849089', containerNo: 'C10000000', status: 'WAIT_SIGN',
+ plannedArrivalAt: '2026-08-29T09:30:00', totalOrderCount: 15, planCount: 15, inboundCodeCount: 15,
+ missingInboundCodeCount: 0, plannedInboundCodeCount: 15, pendingPlanCount: 0, allPlansReady: true, operable: true, blockReason: '',
destinationCodes: ['ABE3', 'US-LAX-01'], plannedPalletCount: 15,
totalPackages: 60, receivedPackages: 0, currentAccount: false
})
assert.deepEqual(candidate, {
- containerNo: 'C10000000', status: 'WAIT_SIGN', planCount: 15, inboundCodeCount: 15,
+ sourceContainerBookingId: '2093166483653849089', containerNo: 'C10000000', status: 'WAIT_SIGN',
+ plannedArrivalAt: '2026-08-29T09:30:00', totalOrderCount: 15, planCount: 15, inboundCodeCount: 15,
+ missingInboundCodeCount: 0, plannedInboundCodeCount: 15, pendingPlanCount: 0, allPlansReady: true, operable: true, blockReason: '',
destinationCodes: ['ABE3', 'US-LAX-01'], plannedPalletCount: 15,
totalPackages: 60, receivedPackages: 0, currentAccount: false
})
assert.equal(candidate.planCount, candidate.inboundCodeCount)
+ assert.equal(candidate.sourceContainerBookingId, '2093166483653849089')
+})
+
+test('incomplete containers remain visible but fail closed from server readiness facts', () => {
+ const candidate = normalizeReceivingCandidate({
+ sourceContainerBookingId: '2093166483653849099', containerNo: 'C10000001',
+ plannedArrivalAt: null, totalOrderCount: 15, inboundCodeCount: 12, planCount: 10,
+ missingInboundCodeCount: 3, plannedInboundCodeCount: 10, pendingPlanCount: 5, allPlansReady: false, operable: false,
+ blockReason: '存在3票缺少入仓码'
+ })
+ assert.equal(candidate.totalOrderCount, 15)
+ assert.equal(candidate.planCount, 10)
+ assert.equal(candidate.plannedInboundCodeCount, 10)
+ assert.equal(candidate.pendingPlanCount, 5)
+ assert.equal(candidate.missingInboundCodeCount, 3)
+ assert.equal(candidate.allPlansReady, false)
+ assert.equal(candidate.operable, false)
+ assert.equal(candidate.blockReason, '存在3票缺少入仓码')
+
+ const absentGate = normalizeReceivingCandidate({ containerNo: 'OLD-CONTRACT' })
+ assert.equal(absentGate.allPlansReady, false)
+ assert.equal(absentGate.operable, false)
})
test('current recovery retains all independent-plan inbound facts while progress remains container-level', () => {
@@ -53,7 +79,7 @@ test('container UI locks and signs once across independent plans, then renders o
read('../src/api/inbound.js')
])
- assert.match(operation, /item\.inboundCodeCount[\s\S]*item\.totalPackages[\s\S]*item\.plannedPalletCount[\s\S]*item\.destinationCodes/)
+ assert.match(operation, /item\.totalOrderCount[\s\S]*item\.inboundCodeCount[\s\S]*item\.planCount[\s\S]*item\.pendingPlanCount/)
assert.match(summary, /inboundCodeCount[\s\S]*plannedPalletCount[\s\S]*totalPackages/)
assert.doesNotMatch(operation, /v-for="[^"]*inbound[^"]*"/)
assert.match(operation, /v-if="activeInbound"[\s\S]*activeInbound\.inboundCode[\s\S]*activeInbound\.pendingPackages/)
@@ -70,10 +96,19 @@ test('container UI locks and signs once across independent plans, then renders o
})
test('PDA does not infer container eligibility from partial plan workflow states', async () => {
- const operation = await read('../src/pages/operation/operation.vue')
+ const [operation, adapter, inbound] = await Promise.all([
+ read('../src/pages/operation/operation.vue'), read('../src/api/receiving-adapter.js'), read('../src/api/inbound.js')
+ ])
for (const internalStatus of ['PENDING_ASSIGNMENT', 'INBOUND_PLANNED', 'PDA_PENDING', 'ASSIGNED', 'PICKING']) {
assert.doesNotMatch(operation, new RegExp(internalStatus))
}
assert.match(operation, /listReceivingCandidates\(\)/)
assert.match(operation, /statusLabel\(status\)[\s\S]*WAIT_SIGN[\s\S]*SIGNED[\s\S]*RECEIVING[\s\S]*COMPLETED/)
+ assert.match(adapter, /allPlansReady: value\.allPlansReady === true/)
+ assert.match(adapter, /operable: value\.operable === true/)
+ assert.match(operation, /if \(!candidate\.operable\) \{ receiving\.selected = candidate; this\.detailOpen = true; return \}/)
+ assert.match(operation, /if \(!this\.selected\?\.containerNo \|\| !this\.selected\?\.allPlansReady \|\| !this\.selected\?\.operable/)
+ assert.match(operation, /catch \(error\) \{ await this\.recoverCurrentAfterConflict\(error, true\) \}/)
+ assert.match(operation, /preserveEvidence \? error : null[\s\S]*this\.candidateError = preserveEvidence \? error : null/)
+ assert.doesNotMatch(inbound, /sort\(/)
})
diff --git a/tests/project-contract.test.mjs b/tests/project-contract.test.mjs
index 85ed239..962b33a 100644
--- a/tests/project-contract.test.mjs
+++ b/tests/project-contract.test.mjs
@@ -66,7 +66,7 @@ test('login has no committed demo credentials and inbound operation implements t
for (const role of ['LICENSE_PLATE', 'CONTAINER_SEAL', 'DOOR_OPEN']) assert.match(adapter, new RegExp(role))
for (const forbidden of ['customerId', 'planNo', 'allocationId', 'warehouseId', 'locationId']) assert.doesNotMatch(inbound, new RegExp(forbidden))
assert.match(operation, /isReceivingConflict|recoverCurrentAfterConflict/)
- assert.match(operation, /isNoCurrentReceiving\(recoveryError\)[\s\S]*该柜已被其他账号选择,列表已刷新/)
+ assert.match(operation, /isNoCurrentReceiving\(recoveryError\)[\s\S]*柜状态已变化,列表已刷新/)
assert.match(operation, /onShow\(\)[\s\S]*this\.phase === 'candidates'[\s\S]*this\.bootstrap\(\)/)
assert.match(operation, /candidateLoading[\s\S]*candidateError/)
assert.match(operation, /candidateError\.requestId[\s\S]*candidateError\.traceId/)
@@ -112,13 +112,16 @@ test('receiving phases use a compiled container summary with cabinet and aggrega
test('candidate containers use compact card navigation and a small refresh action', async () => {
const operation = await read('../src/pages/operation/operation.vue')
- assert.match(operation, /class="container-card container-card-compact"[\s\S]*@tap="openContainer\(item\.containerNo\)"/)
+ assert.match(operation, /class="container-card container-card-compact"[\s\S]*container-card-disabled[\s\S]*@tap="openContainer\(item\)"/)
assert.match(operation, /class="container-detail-link"/)
assert.doesNotMatch(operation, /