diff --git a/AGENTS.md b/AGENTS.md
index fa96fcc..4a157ad 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -39,7 +39,7 @@
| Android APK/WGT 在线更新 | `src/App.vue`、`src/utils/app-update.js`、`.env.example`、`src/manifest.json` | `VITE_PDA_UPDATE_URL` 使用海外独立 HTTPS 通道;合同 `tests/app-update.test.mjs`,H5 不执行更新逻辑 |
| 登录、刷新与会话 | `src/pages/login/login.vue`、`src/api/auth.js`、`src/stores/session.js` | 国内 Auth 签发 Token;API 根见 `src/config/runtime.js` |
| 海外仓工作台与模块入口 | `src/pages/home/home.vue`、`src/pages/module/module.vue` | `src/data/overseas-workbench.js` 是模块与作业可用性入口;首页通过 `/api/pda/v1/last-leg/receiving-containers/warehouse-profile` 读取 Token 绑定仓的中文主档名称 |
-| 中英西多语言预留 | `src/i18n/index.js`、`src/i18n/locales/` | 当前只保留 `zh-CN`、`en`、`es` 初始化、空资源目录和本地语言状态能力;业务文案仍统一使用中文,需求稳定前不迁移页面、不开放语言切换入口 |
+| 中英西语言设置 | `src/i18n/index.js`、`src/i18n/locales/`、`src/pages/profile/profile.vue` | 个人中心可选择并持久化 `zh-CN`、`en`、`es`;当前业务翻译资源仍为空,页面业务文案继续使用中文,不得把语言选择入口误报为全量翻译完成 |
| 柜号签收、照片与入仓真实作业 | `src/pages/operation/operation.vue`、`src/api/inbound.js`、`src/api/receiving-adapter.js`、`src/api/overseas.js`、`src/stores/receiving.js` | 三个固定照片角色各一张,支持拍照/相册、鉴权预览及更换失败保留原图;Backend `lastleg-service/inboundplan/pda/controller/ReceivingContainerController.java`、`ReceivingContainerService.java`,公网前缀 `/api/pda/v1/last-leg/receiving-containers/**` |
| 库内、托盘、换标与现场查询待接前端 | `src/data/overseas-workbench.js` 中对应 `available: false` 入口 | Backend 已有 `pda/stocktake/`、`pda/move/`、`pda/warehouse/`、`palletops/`、`relabel/pda/` 与 `overseas-pda-operations-v1.yaml`;本应用新增专用 API/状态恢复和测试前不得启用 |
| 扫码设备与诊断 | `src/utils/scan-broadcast.js`、`src/pages/scanner-test/scanner-test.vue` | `src/components/ScanTargetInput.vue`、声音反馈工具;广播 Action/Extra 见本文件服务边界 |
diff --git a/src/api/pallet.js b/src/api/pallet.js
new file mode 100644
index 0000000..50a596d
--- /dev/null
+++ b/src/api/pallet.js
@@ -0,0 +1,23 @@
+import { overseasRequest } from './overseas.js'
+
+const BASE = '/api/pda/v1/last-leg/pallet-operations'
+
+export function resolvePallet(code) {
+ return overseasRequest('POST', `${BASE}/pallets/resolve`, { code: String(code || '').trim() })
+}
+
+export function assemblePallet(payload) {
+ return overseasRequest('POST', `${BASE}/assemble`, payload)
+}
+
+export function disassemblePallet(payload) {
+ return overseasRequest('POST', `${BASE}/disassemble`, payload)
+}
+
+export function swapPallet(payload) {
+ return overseasRequest('POST', `${BASE}/swap`, payload)
+}
+
+export function unbindPallet(payload) {
+ return overseasRequest('POST', `${BASE}/unbind`, payload)
+}
diff --git a/src/i18n/index.js b/src/i18n/index.js
index 8668cf3..816918d 100644
--- a/src/i18n/index.js
+++ b/src/i18n/index.js
@@ -6,9 +6,9 @@ import es from './locales/es.js'
const LOCALE_KEY = 'overseas-pda-locale'
export const SUPPORTED_LOCALES = ['zh-CN', 'en', 'es']
export const LOCALE_OPTIONS = [
- { value: 'zh-CN', labelKey: 'language.zh' },
- { value: 'en', labelKey: 'language.en' },
- { value: 'es', labelKey: 'language.es' }
+ { value: 'zh-CN', label: '简体中文', labelKey: 'language.zh' },
+ { value: 'en', label: 'English', labelKey: 'language.en' },
+ { value: 'es', label: 'Español', labelKey: 'language.es' }
]
function normalizeLocale(value) {
diff --git a/src/pages/operation/operation.vue b/src/pages/operation/operation.vue
index 8054548..f108bc9 100644
--- a/src/pages/operation/operation.vue
+++ b/src/pages/operation/operation.vue
@@ -3,25 +3,19 @@
- {{ pageTitle }}{{ pageSubtitle }}
+ {{ pageTitle }}{{ pageSubtitle }}
正在恢复当前柜作业…
-
- 扫描柜号
-
-
-
- {{ candidates.length }} 个待卸货集装箱
+ 共 {{ candidates.length }} 柜
{{ candidateError.message || '待卸货集装箱加载失败' }}code={{ candidateError.code }}requestId={{ candidateError.requestId }}traceId={{ candidateError.traceId }}
正在刷新待卸货集装箱…
暂无待卸货集装箱
-
- {{ item.containerNo }}{{ item.inboundCodeCount }} 个入仓码 · {{ item.planCount }} 个计划 · {{ item.plannedPalletCount }} 托目的仓:{{ item.destinationCodes.join(' / ') }}{{ statusLabel(item.status) }}
- 收货进度{{ item.receivedPackages }} / {{ item.totalPackages }} 箱
-
-
+
+ {{ item.containerNo }}{{ statusLabel(item.status) }}{{ item.currentAccount ? '继续' : '详情' }} ›
+ {{ item.inboundCodeCount }}入仓码{{ item.totalPackages }}箱{{ item.plannedPalletCount }}计划托盘{{ item.destinationCodes?.length || 0 }}目的仓
+ 仓码:{{ item.destinationCodes.join(' / ') }}
@@ -113,7 +107,7 @@
export default {
components: { PortalHeader, ReceivingSessionSummary, ScanTargetInput },
- data() { return { user: session.currentUser, moduleKey: 'query', operationName: '库存查询', loading: false, candidateLoading: false, busy: false, bootstrapped: false, detailOpen: false, containerInput: '', cartonInput: '', palletInput: '', packageCount: 1, candidateError: null, error: null, toast: { visible: false, message: '', type: '' } } },
+ data() { return { user: session.currentUser, moduleKey: 'query', operationName: '库存查询', loading: false, candidateLoading: false, busy: false, bootstrapped: false, detailOpen: false, cartonInput: '', palletInput: '', packageCount: 1, candidateError: null, error: null, 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] })) },
@@ -132,19 +126,19 @@
onHide() { this.stopScanner() }, onUnload() { this.stopScanner() }, beforeUnmount() { clearTimeout(this._toastTimer); this.stopScanner() },
methods: {
statusLabel(status) { return ({ WAIT_SIGN: '待签收', SIGNED: '已签收', RECEIVING: '收货中', COMPLETED: '已完成' })[status] || status },
- statusTone(status) { return status === 'RECEIVING' ? 'status-current' : status === 'COMPLETED' ? 'status-good' : 'status-wait' }, percent(received, total) { return total ? Math.min(100, Math.round(Number(received || 0) / Number(total) * 100)) : 0 },
+ statusTone(status) { return status === 'RECEIVING' ? 'status-current' : status === 'COMPLETED' ? 'status-good' : 'status-wait' },
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.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') { this.containerInput = value; return this.openContainer(value) } 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) },
+ 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 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) { this.error = recoveryError; return 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 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 ?? this.containerInput).trim(); if (!containerNo || this.busy) return; try { receiving.selected = await this.run('查询柜号', () => getReceivingContainer(containerNo)); this.containerInput = containerNo; this.detailOpen = true; playSuccessSound() } catch (error) { await this.recoverCurrentAfterConflict(error) } },
+ 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) } },
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) } },
choosePhoto(index) { if (this.busy) return; uni.chooseImage({ count: 1, sourceType: ['camera'], success: ({ tempFilePaths }) => { const path = tempFilePaths?.[0]; if (path) this.replacePhoto(index, path) } }) },
@@ -191,4 +185,5 @@
.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}
diff --git a/src/pages/profile/profile.vue b/src/pages/profile/profile.vue
index 9b5d0b6..e9bbcb5 100644
--- a/src/pages/profile/profile.vue
+++ b/src/pages/profile/profile.vue
@@ -7,9 +7,10 @@
作业仓库{{ warehouseDisplay }}
当前班次以海外仓排班为准
应用版本{{ appVersion }}
+
+
+
-
-
退出后会清空海外 PDA 的本地登录会话;未提交作业数据不得跨账号保留。
@@ -20,15 +21,17 @@
import PortalHeader from '@/components/PortalHeader.vue'
import PopupHeader from '@/components/PopupHeader.vue'
import { getCurrentWarehouseProfile } from '@/api/inbound.js'
+ import { currentLocale, LOCALE_OPTIONS, setLocale } from '@/i18n/index.js'
import { applyWarehouseProfile, session, requireSession, signOut } from '@/stores/session.js'
import { checkForAppUpdate } from '@/utils/app-update.js'
export default {
components: { PopupHeader, PortalHeader },
- data() { return { user: session.currentUser, appVersion: '0.0.1', logoutOpen: false, toast: false } },
+ data() { return { user: session.currentUser, appVersion: '0.0.1', locale: currentLocale(), logoutOpen: false, toast: false } },
computed: {
displayName() { const name = String(this.user?.name || ''); return name && !/^[a-z0-9._-]+$/i.test(name) ? name : '仓库作业员' },
avatar() { return this.displayName.slice(0, 1).toUpperCase() },
- warehouseDisplay() { return this.user?.warehouseName ? (this.user.warehouseCode ? `${this.user.warehouseName} · ${this.user.warehouseCode}` : this.user.warehouseName) : (this.user?.warehouse || '正在读取仓库主档') }
+ warehouseDisplay() { return this.user?.warehouseName ? (this.user.warehouseCode ? `${this.user.warehouseName} · ${this.user.warehouseCode}` : this.user.warehouseName) : (this.user?.warehouse || '正在读取仓库主档') },
+ languageLabel() { return LOCALE_OPTIONS.find((item) => item.value === this.locale)?.label || 'English' }
},
onShow() { if (!requireSession()) return; this.user = session.currentUser; this.loadAppVersion(); this.loadWarehouseProfile() },
beforeUnmount() { clearTimeout(this._toastTimer) },
@@ -48,6 +51,17 @@
uni.showToast({ title: '请在 PDA App 中检查更新', icon: 'none' })
// #endif
},
+ selectLanguage() {
+ uni.showActionSheet({
+ itemList: LOCALE_OPTIONS.map((item) => item.label),
+ success: ({ tapIndex }) => {
+ const selected = LOCALE_OPTIONS[tapIndex]
+ if (!selected) return
+ this.locale = setLocale(selected.value)
+ uni.showToast({ title: `语言设置已保存:${selected.label}`, icon: 'none' })
+ }
+ })
+ },
async loadWarehouseProfile() { try { this.user = applyWarehouseProfile(await getCurrentWarehouseProfile()) } catch (_) { this.user = session.currentUser } },
openLogout() { this.logoutOpen = true; this.$refs.logoutPopup.open() }, closeLogout() { this.logoutOpen = false; this.$refs.logoutPopup.close() }, handleLogoutChange(event) { this.logoutOpen = Boolean(event.show) },
showSettings() { clearTimeout(this._toastTimer); this.toast = true; this._toastTimer = setTimeout(() => { this.toast = false }, 2200) },
diff --git a/src/styles/pda.scss b/src/styles/pda.scss
index 26507cf..d1f37aa 100644
--- a/src/styles/pda.scss
+++ b/src/styles/pda.scss
@@ -24,8 +24,9 @@
letter-spacing: 0;
}
- button::after {
- border: 0;
+ button::after,
+ uni-button::after {
+ border: 0 !important;
}
button:focus-visible,
@@ -892,9 +893,12 @@
.profile-row { display: flex; align-items: center; justify-content: space-between; min-height: 52px; border-bottom: 1px solid $border; font-size: 13px; }
.profile-row:last-child { border-bottom: 0; }
.profile-value { max-width: 66%; color: $text-secondary; font-size: 12px; line-height: 18px; text-align: right; overflow-wrap: anywhere; }
- .profile-action, .logout-action { box-sizing: border-box; width: 100%; height: 50px; margin-top: 14px; padding: 0 15px; border-radius: 11px; font-size: 13px; line-height: 50px; text-align: left; background: #ffffff; }
- .profile-action { display: flex; justify-content: space-between; color: $text; }
- .logout-action { color: $danger; text-align: center; }
+ .profile-row-action { box-sizing: border-box; width: 100%; height: 52px; margin: 0; padding: 0; border-radius: 0; color: $text; line-height: normal; text-align: left; background: transparent; }
+ .profile-row-action::after { border: 0; }
+ .profile-row-action:active { background: #f7f9fc; }
+ .profile-row-end { display: flex; min-width: 0; align-items: center; justify-content: flex-end; gap: 8px; }
+ .profile-row-arrow { flex-shrink: 0; color: $text-secondary; font-size: 18px; }
+ .logout-action { box-sizing: border-box; width: 100%; height: 50px; margin-top: 14px; padding: 0 15px; border-radius: 11px; color: $danger; font-size: 13px; line-height: 50px; text-align: center; background: #ffffff; }
.logout-dialog-card { box-sizing: border-box; width: calc(100vw - 48px); max-width: 320px; overflow: hidden; border-radius: 12px; background: #ffffff; box-shadow: 0 18px 50px rgba(0, 0, 0, 0.24); }
.logout-dialog-title { display: block; padding: 24px 22px 0; color: $danger; font-size: 17px; font-weight: 700; text-align: center; }
.logout-dialog-content { display: block; padding: 18px 22px 22px; color: $text-secondary; font-size: 14px; line-height: 22px; }
diff --git a/tests/app-update.test.mjs b/tests/app-update.test.mjs
index 7df2435..efb363d 100644
--- a/tests/app-update.test.mjs
+++ b/tests/app-update.test.mjs
@@ -35,7 +35,7 @@ test('overseas profile exposes manual App update checking and the runtime versio
const profile = await read('../src/pages/profile/profile.vue')
assert.match(profile, /应用版本[\s\S]*?\{\{ appVersion \}\}/)
- assert.match(profile, /@tap="checkUpdate">检查更新/)
+ assert.match(profile, /class="profile-row profile-row-action" @tap="checkUpdate"[\s\S]*检查更新/)
assert.match(profile, /plus\.runtime\.version/)
assert.match(profile, /checkForAppUpdate\(\{ manual: true \}\)/)
assert.match(profile, /#ifndef APP-PLUS[\s\S]*?请在 PDA App 中检查更新/)
diff --git a/tests/h5-button-reset.test.mjs b/tests/h5-button-reset.test.mjs
new file mode 100644
index 0000000..5166ad2
--- /dev/null
+++ b/tests/h5-button-reset.test.mjs
@@ -0,0 +1,16 @@
+import assert from 'node:assert/strict'
+import { readFile } from 'node:fs/promises'
+import test from 'node:test'
+
+const stylesheet = await readFile(new URL('../src/styles/pda.scss', import.meta.url), 'utf8')
+
+test('H5 removes the uni-app default button pseudo-element border', () => {
+ assert.match(
+ stylesheet,
+ /button::after,\s*uni-button::after\s*\{\s*border:\s*0\s*!important;/
+ )
+})
+
+test('keyboard focus remains visible after removing the default border', () => {
+ assert.match(stylesheet, /button:focus-visible[\s\S]*?outline:\s*3px\s+solid/)
+})
diff --git a/tests/multi-plan-receiving.test.mjs b/tests/multi-plan-receiving.test.mjs
index bc7eb96..52441d8 100644
--- a/tests/multi-plan-receiving.test.mjs
+++ b/tests/multi-plan-receiving.test.mjs
@@ -53,7 +53,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\.planCount[\s\S]*item\.totalPackages/)
+ assert.match(operation, /item\.inboundCodeCount[\s\S]*item\.totalPackages[\s\S]*item\.plannedPalletCount[\s\S]*item\.destinationCodes/)
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/)
diff --git a/tests/project-contract.test.mjs b/tests/project-contract.test.mjs
index 371e1b9..41577f6 100644
--- a/tests/project-contract.test.mjs
+++ b/tests/project-contract.test.mjs
@@ -65,10 +65,13 @@ 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, /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/)
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 prepareContainer\(\)[\s\S]*this\.busy\) return/)
assert.match(operation, /prepareReceivingContainer[\s\S]*releaseIdempotencyKey\(key\.signature\)/)
@@ -99,10 +102,22 @@ test('receiving phases use a compiled container summary with cabinet and aggrega
assert.match(summary, /padding:10px 12px/)
assert.match(summary, /destination-tags\{[^}]*flex-wrap:wrap/)
assert.doesNotMatch(summary, /destinationSummary|text-overflow:ellipsis/)
- assert.match(operation, /v-if="phase !== 'receive'" class="receiving-heading"/)
+ assert.match(operation, /v-if="phase !== 'candidates' && phase !== 'receive'" class="receiving-heading"/)
})
-test('Chinese English and Spanish locale scaffolding is installed without exposing unfinished translations', async () => {
+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-detail-link"/)
+ assert.doesNotMatch(operation, /