diff --git a/pages/picking/picking.vue b/pages/picking/picking.vue
index d788f2c..ff63bfb 100644
--- a/pages/picking/picking.vue
+++ b/pages/picking/picking.vue
@@ -33,10 +33,10 @@
-
+
-
+
@@ -123,7 +123,7 @@
-
+
装柜证据空仓、半仓、满仓、锁仓及带锁码照
{{ remoteSyncState.label }}
@@ -132,12 +132,14 @@
{{ slot.label }}
- {{ photo.fileName || '已记录' }}
+
+ {{ photo.fileName || '已记录' }}
{{ evidenceUploadState(slot.type) }}
+ 本地暂存,待自动上传
@@ -146,6 +148,7 @@
+ 当前可先拍照留存;拣货完成并同步装柜状态后,照片会自动上传绑定。
{{ remoteError }}
@@ -153,12 +156,12 @@
-
+
拣货已完成,装柜状态尚未同步,请刷新重试
-
+
{{ summary.pending }} 箱待拣
{{ submitStatusHint }}
@@ -382,7 +385,7 @@
selectedIncompleteReason: '',
completionSummary: { picked: 0, partial: 0, skipped: 0, autoSkipped: 0 },
evidenceSlots: EVIDENCE_SLOTS,
- evidenceFiles: {}, evidenceQueueItems: [], evidenceQueue: null, evidenceContextId: 0, evidenceIdentity: null, cameraPicking: false, sealNo: '', loadingVersion: null, pickingVersion: null, remotePick: null,
+ evidenceFiles: {}, evidenceDrafts: {}, evidencePreviewCache: {}, evidenceQueueItems: [], evidenceQueue: null, evidenceContextId: 0, evidenceIdentity: null, cameraPicking: false, sealNo: '', loadingVersion: null, pickingVersion: null, remotePick: null,
remoteTaskId: null, assignmentVersion: null, submitId: '',
taskRequestId: 0,
remoteError: '', remoteLoading: false, submitting: false,
@@ -524,10 +527,6 @@
},
methods: {
selectStage(stage) {
- if (stage === 'evidence' && !this.loadingStageReady) {
- this.showToast('请先完成拣货并刷新任务', 'warning')
- return
- }
this.activeStage = stage
},
debugEvidence(event, payload = {}) {
@@ -563,6 +562,37 @@
return contextId === this.evidenceContextId
&& this.isSameEvidenceIdentity(this.evidenceIdentity, identity)
},
+ evidenceDraftKey(identity) {
+ return ['taskId', 'inboundId', 'loadingPlanId', 'containerBookingId'].map((key) => String(identity?.[key] ?? '')).join('|')
+ },
+ rememberEvidenceDraft(identity = this.evidenceIdentity) {
+ const key = this.evidenceDraftKey(identity)
+ if (!key.replace(/\|/g, '')) return
+ const files = Object.fromEntries(Object.entries(this.evidenceFiles).map(([type, entries]) => [type, (Array.isArray(entries) ? entries : [entries]).filter((entry) => entry && !entry.id && entry.localPath).map((entry) => ({ ...entry }))]).filter(([, entries]) => entries.length))
+ if (Object.keys(files).length || String(this.sealNo || '').trim()) this.evidenceDrafts = { ...this.evidenceDrafts, [key]: { files, sealNo: this.sealNo || '' } }
+ },
+ restoreEvidenceDraft(identity = this.evidenceIdentity) {
+ const draft = this.evidenceDrafts[this.evidenceDraftKey(identity)]
+ if (!draft) return
+ this.evidenceFiles = Object.fromEntries(Object.entries(draft.files || {}).map(([type, entries]) => [type, entries.map((entry) => ({ ...entry }))]))
+ if (draft.sealNo) this.sealNo = draft.sealNo
+ },
+ removeDraftPhoto(type, localPath) {
+ const key = this.evidenceDraftKey(this.evidenceIdentity)
+ const draft = this.evidenceDrafts[key]
+ if (!draft) return
+ const files = { ...(draft.files || {}) }
+ files[type] = (files[type] || []).filter((entry) => entry.localPath !== localPath)
+ if (!files[type].length) delete files[type]
+ this.evidenceDrafts = Object.keys(files).length || draft.sealNo ? { ...this.evidenceDrafts, [key]: { ...draft, files } } : Object.fromEntries(Object.entries(this.evidenceDrafts).filter(([draftKey]) => draftKey !== key))
+ },
+ requireEvidenceCapture() {
+ if (!this.evidenceIdentity?.inboundId) {
+ this.showToast('任务状态尚未同步,请刷新重试', 'warning')
+ return false
+ }
+ return true
+ },
requireEvidenceStage() {
if (!this.pickingCompleted) {
this.showToast('请先提交拣货完成', 'warning')
@@ -765,12 +795,32 @@
this.pickScannedBox(result.item, value)
},
evidenceEntries(type) { return Array.isArray(this.evidenceFiles[type]) ? this.evidenceFiles[type].filter((item) => item.status !== 'VOID' && item.status !== 'VOIDED') : (this.evidenceFiles[type] ? [this.evidenceFiles[type]] : []) },
+ evidencePreviewSource(photo) { return photo?.localPath || photo?.previewUrl || photo?.fileUrl || photo?.downloadUrl || photo?.url || '' },
+ evidencePreviewCacheKey(fileId, identity = this.evidenceIdentity) { return `${this.evidenceDraftKey(identity)}|${String(fileId || '')}` },
+ cacheEvidencePreview(fileId, localPath, identity = this.evidenceIdentity) {
+ if (!fileId || !localPath) return
+ this.evidencePreviewCache = { ...this.evidencePreviewCache, [this.evidencePreviewCacheKey(fileId, identity)]: localPath }
+ },
+ withCachedEvidencePreview(photo, identity = this.evidenceIdentity) {
+ if (!photo || this.evidencePreviewSource(photo)) return photo
+ const localPath = this.evidencePreviewCache[this.evidencePreviewCacheKey(photo.fileId, identity)]
+ return localPath ? { ...photo, localPath } : photo
+ },
+ previewEvidencePhoto(photo, type) {
+ const current = this.evidencePreviewSource(photo)
+ const urls = this.evidenceEntries(type).map((item) => this.evidencePreviewSource(item)).filter(Boolean)
+ if (!current || !urls.length) { this.showToast('照片地址尚未同步,请刷新后重试', 'warning'); return }
+ if (typeof uni.previewImage !== 'function') { this.showToast('当前环境不支持预览照片', 'warning'); return }
+ this.debugEvidence('preview.open', { evidenceType: type, evidenceIdentity: this.evidenceIdentity, count: urls.length })
+ uni.previewImage({ current, urls })
+ },
evidenceItems(result) {
if (Array.isArray(result?.evidences)) return result.evidences
return Array.isArray(result?.evidences?.evidences) ? result.evidences.evidences : []
},
validEvidenceEntries(type) { return this.evidenceEntries(type).filter((item) => item.id && item.fileId && item.uploadStatus !== 'PENDING') },
evidenceEntry(type) { const list = this.evidenceEntries(type); return list.length ? list[list.length - 1] : null },
+ hasPendingLocalEvidence(type) { return this.evidenceEntries(type).some((item) => item.localPath && !item.fileId) },
evidenceQueueEntry(type) {
return [...this.evidenceQueueItems].reverse().find((item) => item.evidenceType === type) || null
},
@@ -780,12 +830,13 @@
if (item.status === 'PENDING') return '本地待上传'
if (item.status === 'UPLOADING') return `上传中 ${item.progress || 0}%`
if (item.status === 'FAILED') return item.error || '上传失败'
- if (item.status === 'SUCCEEDED') return item.attempts > 1 ? '重试成功' : '已上传'
+ if (item.status === 'SUCCEEDED') return this.validEvidenceEntries(type).length ? '已上传并保存' : (item.attempts > 1 ? '重试成功' : '已上传')
if (item.status === 'REPLACED') return '已替换'
return ''
},
async syncRemoteTask() {
if (!isRemoteReady(this.currentBox.inboundId || this.currentBox.boxNo)) return
+ this.rememberEvidenceDraft()
const requestId = ++this.taskRequestId
const selector = { taskId: this.remoteTaskId || this.currentBox.taskId, cabinetNo: this.currentBox.cabinetNo, loadingPlanId: this.currentBox.loadingPlanId }
this.currentBox.inboundId = null
@@ -858,6 +909,7 @@
this.pickingVersion = this.remotePick.pickingVersion ?? this.remotePick.version ?? null
this.loadingVersion = task.inboundVersion ?? task.loadingVersion ?? this.remotePick.inboundVersion ?? this.remotePick.loadingVersion ?? null
this.evidenceIdentity = this.captureEvidenceIdentity()
+ this.restoreEvidenceDraft()
this.debugSubmit('refresh.ready', { requestId, workflowStatus: this.remotePick.workflowStatus, taskStatus: this.remotePick.taskStatus, assignmentVersion: this.assignmentVersion, loadingVersion: this.loadingVersion, evidenceIdentity: this.evidenceIdentity })
this.assignmentVersion = this.remotePick.assignmentVersion ?? this.assignmentVersion
if (!this.pickingCompleted) this.restoreRemoteDraft()
@@ -875,9 +927,11 @@
try {
const evidence = await getEvidence(identity.inboundId, this.currentBox.tenantId)
if (!this.isEvidenceContextActive(contextId, identity)) return
- this.evidenceFiles = (evidence && evidence.evidences ? evidence.evidences : []).reduce((all, item) => { (all[item.evidenceType] ||= []).push(item); return all }, {})
+ const localFiles = Object.fromEntries(Object.entries(this.evidenceFiles).map(([type, entries]) => [type, (Array.isArray(entries) ? entries : [entries]).filter((entry) => entry && !entry.id && entry.localPath)]))
+ this.evidenceFiles = this.evidenceItems(evidence).map((item) => this.withCachedEvidencePreview(item, identity)).reduce((all, item) => { (all[item.evidenceType] ||= []).push(item); return all }, localFiles)
const seal = this.evidenceEntry('SEAL')
- this.sealNo = seal?.sealNo || ''
+ if (seal?.sealNo) this.sealNo = seal.sealNo
+ await this.flushPendingEvidence()
} catch (error) {
if (!this.isEvidenceContextActive(contextId, identity)) return
this.remoteError = error.network ? '网络失败,装柜证据未加载,请刷新重试' : (error.message || '装柜证据加载失败')
@@ -891,13 +945,14 @@
},
evidenceUploadLabel(type) {
const queued = this.evidenceQueueEntry(type)
- if (queued) return queued.status === 'UPLOADING' ? `上传中 ${queued.progress || 0}%` : '等待上传'
+ if (queued?.status === 'UPLOADING') return `上传中 ${queued.progress || 0}%`
+ if (queued?.status === 'PENDING') return '等待上传'
return this.evidenceEntry(type) ? '继续拍照' : '拍照'
},
chooseEvidence(slot) {
const evidenceType = slot?.type || null
this.debugEvidence('choose.start', { evidenceType, evidenceIdentity: this.evidenceIdentity, loadingStageReady: this.loadingStageReady, hasChooseImage: typeof uni.chooseImage === 'function' })
- if (!this.requireEvidenceStage()) { this.debugEvidence('choose.blocked', { evidenceType, reason: 'stage-not-ready', evidenceIdentity: this.evidenceIdentity, loadingVersion: this.loadingVersion }); return }
+ if (!this.requireEvidenceCapture()) { this.debugEvidence('choose.blocked', { evidenceType, reason: 'identity-not-ready', evidenceIdentity: this.evidenceIdentity }); return }
if (!isOnline()) { this.debugEvidence('choose.blocked', { evidenceType, reason: 'offline' }); this.showToast('当前网络不可用,请联网后再拍照上传', 'warning'); return }
if (typeof uni.chooseImage !== 'function') { this.debugEvidence('choose.blocked', { evidenceType, reason: 'chooseImage-unavailable' }); this.showToast('当前环境不支持选择照片', 'warning'); return }
const contextId = this.evidenceContextId
@@ -912,7 +967,9 @@
this.debugEvidence('camera.selected', { evidenceType: slot.type, evidenceIdentity: identity })
const next = { evidenceType: slot.type, fileName: path.split('/').pop() || path, localPath: path, uploadStatus: 'PENDING' }
this.evidenceFiles = { ...this.evidenceFiles, [slot.type]: [...(this.evidenceFiles[slot.type] || []), next] }
- this.queueEvidenceUpload(slot.type, path)
+ this.rememberEvidenceDraft(identity)
+ if (this.loadingStageReady) this.queueEvidenceUpload(slot.type, path)
+ else this.showToast('照片已暂存,装柜状态同步后自动上传', 'success')
}, fail: (error) => {
this.cameraPicking = false
if (!this.isEvidenceContextActive(contextId, identity)) return
@@ -921,7 +978,7 @@
} })
},
async queueEvidenceUpload(type, path) {
- if (!this.requireEvidenceStage()) return
+ if (!this.loadingStageReady) return
if (!this.evidenceIdentity?.inboundId || !isRemoteReady(this.evidenceIdentity.inboundId)) { this.showToast('当前会话未连接服务端,证据保留本地', 'warning'); return }
const contextId = this.evidenceContextId
const identity = { ...this.evidenceIdentity }
@@ -933,9 +990,15 @@
const latest = queue.snapshot().find((item) => item.id === entry.id)
this.debugSubmit('evidence.queue.result', { evidenceType: type, queueId: entry.id, status: latest?.status || 'missing', fileId: latest?.result?.fileId || null, error: latest?.error || '' })
if (latest?.status !== 'SUCCEEDED') { this.remoteError = latest?.error || '文件上传失败'; return }
- const current = this.evidenceEntry(type) || {}
- this.evidenceFiles = { ...this.evidenceFiles, [type]: [...(this.evidenceFiles[type] || []).slice(0, -1), { ...current, fileId: latest.result?.fileId, fileName: latest.result?.fileName || current.fileName, uploadStatus: 'SUCCEEDED' }] }
- await this.saveEvidence(type)
+ const entries = this.evidenceFiles[type] || []
+ const current = entries.find((item) => item.localPath === path) || {}
+ this.evidenceFiles = { ...this.evidenceFiles, [type]: entries.map((item) => item.localPath === path ? { ...item, fileId: latest.result?.fileId, fileName: latest.result?.fileName || item.fileName, uploadStatus: 'SUCCEEDED' } : item) }
+ await this.saveEvidence(type, null, path)
+ },
+ async flushPendingEvidence() {
+ if (!this.loadingStageReady) return
+ const pending = this.evidenceSlots.flatMap((slot) => this.evidenceEntries(slot.type).filter((entry) => entry.localPath && !entry.fileId).map((entry) => ({ type: slot.type, path: entry.localPath })))
+ for (const item of pending) await this.queueEvidenceUpload(item.type, item.path)
},
async retryEvidence(type) {
if (!this.requireEvidenceStage()) return
@@ -953,7 +1016,7 @@
this.evidenceFiles = { ...this.evidenceFiles, [type]: [...(this.evidenceFiles[type] || []).slice(0, -1), { ...current, fileId: latest.result?.fileId, fileName: latest.result?.fileName || current.fileName, uploadStatus: 'SUCCEEDED' }] }
await this.saveEvidence(type)
},
- async saveEvidence(type, operationId = null) {
+ async saveEvidence(type, operationId = null, localPath = null) {
if (!this.requireEvidenceStage()) return
if (!this.evidenceIdentity?.inboundId) { this.showToast('状态尚未同步,请刷新重试', 'warning'); return }
if (!isRemoteReady(this.evidenceIdentity.inboundId)) { this.showToast('当前会话未连接服务端,证据保留本地', 'warning'); return }
@@ -961,7 +1024,7 @@
const identity = { ...this.evidenceIdentity }
const inboundId = identity.inboundId
const tenantId = this.currentBox.tenantId
- const current = this.evidenceEntry(type) || {}
+ const current = (localPath ? this.evidenceEntries(type).find((item) => item.localPath === localPath) : null) || this.evidenceEntry(type) || {}
if (!current.fileId) { this.showToast('照片尚未上传完成', 'warning'); return }
const payload = { expectedVersion: current.version || 0, evidenceType: type, fileId: current.fileId || null, fileName: current.fileName || null, sealNo: type === 'SEAL' ? this.sealNo : null, remark: null, operationId: operationId || `evidence-${identity.taskId || identity.inboundId}-${type}-${current.fileId || 'seal'}` }
this.debugSubmit('evidence.bind.start', { evidenceType: type, fileId: current.fileId, expectedVersion: payload.expectedVersion, evidenceIdentity: identity })
@@ -969,7 +1032,10 @@
const result = await upsertEvidence(inboundId, payload, tenantId)
if (!this.isEvidenceContextActive(contextId, identity)) return
this.debugSubmit('evidence.bind.success', { evidenceType: type, fileId: current.fileId, evidenceCount: result?.evidences?.length || 0, evidenceIdentity: identity })
- this.evidenceFiles = this.evidenceItems(result).reduce((all, item) => { const list = (all[item.evidenceType] || []).filter((entry) => entry.id || entry.fileId !== item.fileId); const index = list.findIndex((entry) => entry.id === item.id); all[item.evidenceType] = index >= 0 ? list.map((entry, i) => i === index ? item : entry) : [...list, item]; return all }, this.evidenceFiles)
+ this.cacheEvidencePreview(current.fileId, current.localPath, identity)
+ this.evidenceFiles = this.evidenceItems(result).map((item) => this.withCachedEvidencePreview(item, identity)).reduce((all, item) => { const list = (all[item.evidenceType] || []).filter((entry) => entry.id || entry.fileId !== item.fileId); const index = list.findIndex((entry) => entry.id === item.id); all[item.evidenceType] = index >= 0 ? list.map((entry, i) => i === index ? item : entry) : [...list, item]; return all }, this.evidenceFiles)
+ if (current.localPath) this.evidenceFiles = { ...this.evidenceFiles, [type]: (this.evidenceFiles[type] || []).filter((entry) => entry.id || entry.localPath !== current.localPath) }
+ this.removeDraftPhoto(type, current.localPath)
this.showToast(`${type === 'SEAL' ? '封签' : '照片'}已保存`, 'success')
} catch (error) {
if (!this.isEvidenceContextActive(contextId, identity)) return
@@ -978,8 +1044,9 @@
}
},
async saveSealNo() {
- if (!this.requireEvidenceStage()) return
if (!String(this.sealNo || '').trim()) { this.showToast('请输入封签号', 'warning'); return }
+ this.rememberEvidenceDraft()
+ if (!this.loadingStageReady) { this.showToast('封签号已暂存,装柜状态同步后自动保存', 'success'); return }
await this.saveEvidence('SEAL', `seal-no-${this.evidenceIdentity?.taskId || this.evidenceIdentity?.inboundId}-${Date.now()}`)
},
async voidEvidencePhoto(photo) {
@@ -1183,7 +1250,11 @@
this.debugSubmit('submit.batch', { localItemIds: this.currentBox.items.map((item) => item.id), actionItemIds: actions.map((action) => action.itemId), actionCount: actions.length })
const submittedTask = await submitContainerPickingBatch({ taskId: this.remoteTaskId, loadingPlanId: this.currentBox.loadingPlanId, containerBookingId: this.currentBox.containerBookingId, version: Number(this.pickingVersion) }, actions, { warehouseId, operationId: submitId, inventorySnapshotToken: inventorySnapshot?.snapshotToken })
this.debugSubmit('submit.success', { itemCount: actions.length, workflowStatus: submittedTask?.workflowStatus || null, taskStatus: submittedTask?.taskStatus || null })
- if (submittedTask) this.remotePick = normalizePickingTask({ ...this.remotePick, ...submittedTask })
+ if (submittedTask) {
+ this.remotePick = normalizePickingTask({ ...this.remotePick, ...submittedTask })
+ this.loadingVersion = submittedTask.inboundVersion ?? submittedTask.loadingVersion ?? this.remotePick.inboundVersion ?? this.remotePick.loadingVersion ?? this.loadingVersion
+ this.evidenceIdentity = this.captureEvidenceIdentity()
+ }
// Legacy inbound completePicking(expectedVersion: Number(this.pickingVersion)) remains adapter-only; cabinet tasks use item actions.
} catch (error) {
this.debugSubmit('submit.error', { message: error.message || '', code: error.code || '', statusCode: error.statusCode || '', requestIdFromServer: error.requestId || '', traceId: error.traceId || '', network: Boolean(error.network) })
diff --git a/styles/pda.scss b/styles/pda.scss
index c131b4a..be05608 100644
--- a/styles/pda.scss
+++ b/styles/pda.scss
@@ -330,6 +330,16 @@
line-height: 12px;
}
+ .evidence-stage-hint {
+ margin-top: 10px;
+ padding: 8px 10px;
+ border-left: 3px solid #d39a25;
+ color: #795b16;
+ font-size: 11px;
+ line-height: 17px;
+ background: #fff8e7;
+ }
+
.box-chevron.open {
transform: rotate(180deg);
}
@@ -1142,6 +1152,9 @@
.evidence-card { min-height: 86px; padding: 10px; border: 1px dashed #b8c9df; border-radius: 8px; background: #f8fafc; }
.evidence-card.filled { border-style: solid; border-color: #69b88a; background: #f0fbf4; }
.evidence-label { display: block; color: $text; font-size: 12px; font-weight: 700; }
+ .evidence-file-row { display: flex; align-items: center; gap: 7px; min-width: 0; margin-top: 7px; }
+ .evidence-thumbnail { flex: 0 0 48px; width: 48px; height: 48px; border-radius: 6px; background: #e7edf5; }
+ .evidence-file-detail { flex: 1; min-width: 0; }
.evidence-file { display: block; overflow: hidden; margin-top: 5px; color: $text-secondary; font-size: 10px; line-height: 15px; text-overflow: ellipsis; white-space: nowrap; }
.evidence-upload-state { display: block; margin-top: 5px; color: $text-secondary; font-size: 10px; line-height: 15px; }
.evidence-upload-state.uploading, .evidence-upload-state.pending { color: #174d91; }
diff --git a/tests/pda-loading-api.test.mjs b/tests/pda-loading-api.test.mjs
index c790765..d15b7bb 100644
--- a/tests/pda-loading-api.test.mjs
+++ b/tests/pda-loading-api.test.mjs
@@ -63,7 +63,8 @@ test('picking page exposes five evidence slots, retry and remote completion stat
test('PDA evidence binding unwraps the nested loading action response before rendering saved files', () => {
assert.match(page, /evidenceItems\(result\)[\s\S]*?Array\.isArray\(result\?\.evidences\?\.evidences\)[\s\S]*?result\.evidences\.evidences/)
- assert.match(page, /this\.evidenceFiles = this\.evidenceItems\(result\)\.reduce/)
+ assert.match(page, /this\.evidenceFiles = this\.evidenceItems\(result\)[\s\S]*?\.reduce/)
+ assert.match(page, /cacheEvidencePreview\(current\.fileId, current\.localPath/)
})
test('PDA cabinet picking adapter claims online and submits one atomic batch', () => {
diff --git a/tests/picking-evidence-stage.test.mjs b/tests/picking-evidence-stage.test.mjs
index 8b33b01..0ea7fd6 100644
--- a/tests/picking-evidence-stage.test.mjs
+++ b/tests/picking-evidence-stage.test.mjs
@@ -4,11 +4,11 @@ import { readFile } from 'node:fs/promises'
const page = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8')
-test('loading evidence is mounted only after the current picking task is complete', () => {
- assert.match(page, /v-if="loadingStageReady && activeStage === 'evidence'" class="loading-evidence-section"/)
+test('picking and photo workspaces are independently selectable', () => {
+ assert.match(page, /v-if="activeStage === 'evidence'" class="loading-evidence-section"/)
assert.match(page, /isPickingCompleteTask\(task\)[\s\S]*?AWAITING_REVIEW[\s\S]*?COMPLETED/)
assert.match(page, /pickingCompleted\(\)[\s\S]*?completedBoxes\[this\.currentBoxIndex\][\s\S]*?isPickingCompleteTask\(this\.remotePick\)/)
- assert.doesNotMatch(page, /class="loading-evidence-section" v-if="currentBox\.inboundId"/)
+ assert.match(page, /照片可先暂存/)
})
test('loading evidence is not requested before picking completion', () => {
@@ -26,7 +26,7 @@ test('operators receive actionable refresh guidance without internal version wor
assert.match(complete, /if \(!this\.requireEvidenceStage\(\)\) return/)
})
-test('AWAITING_REVIEW alone cannot mount or request evidence without a server loading version', () => {
+test('AWAITING_REVIEW alone cannot request or mutate server evidence without a server loading version', () => {
assert.match(page, /loadingStageReady\(\)[\s\S]*?this\.pickingCompleted[\s\S]*?this\.loadingVersion !== null[\s\S]*?Number\.isFinite/)
assert.match(page, /拣货已完成,装柜状态尚未同步,请刷新重试/)
assert.match(page, /if \(!this\.loadingStageReady \|\| !this\.evidenceIdentity/)
@@ -37,8 +37,13 @@ test('picking and evidence are separate operator-selected stages', () => {
assert.match(page, /class="stage-switch" role="tablist"/)
assert.match(page, /@tap="selectStage\('picking'\)"/)
assert.match(page, /@tap="selectStage\('evidence'\)"/)
- assert.match(page, /selectStage\(stage\)[\s\S]*?stage === 'evidence'[\s\S]*?!this\.loadingStageReady[\s\S]*?请先完成拣货并刷新任务/)
- assert.match(page, /activeStage === 'picking' \|\| !loadingStageReady/)
+ assert.match(page, /selectStage\(stage\) \{[\s\S]*?this\.activeStage = stage/)
+ assert.match(page, /activeStage === 'picking'/)
+ assert.doesNotMatch(page, /activeStage === 'picking' \|\| !loadingStageReady/)
+ assert.match(page, /requireEvidenceCapture\(\)[\s\S]*?evidenceIdentity\?\.inboundId/)
+ assert.match(page, /if \(this\.loadingStageReady\) this\.queueEvidenceUpload/)
+ assert.match(page, /照片已暂存,装柜状态同步后自动上传/)
+ assert.match(page, /本地暂存,待自动上传/)
})
test('evidence identity is bound to task, inbound, loading plan and container booking', () => {
@@ -49,14 +54,33 @@ test('evidence identity is bound to task, inbound, loading plan and container bo
assert.match(page, /this\.evidenceIdentity = this\.captureEvidenceIdentity\(\)/)
})
-test('evidence mutations defensively reject calls before picking completion', () => {
+test('server evidence mutations remain closed before the loading stage is ready', () => {
assert.match(page, /requireEvidenceStage\(\)[\s\S]*?if \(!this\.pickingCompleted\)[\s\S]*?请先提交拣货完成[\s\S]*?if \(!this\.loadingStageReady\)/)
- for (const method of ['chooseEvidence', 'queueEvidenceUpload', 'retryEvidence', 'saveEvidence', 'saveSealNo', 'voidEvidencePhoto', 'completeLoadingTask']) {
+ const queue = page.match(/async queueEvidenceUpload\([^)]*\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
+ assert.match(queue, /if \(!this\.loadingStageReady\) return/)
+ for (const method of ['retryEvidence', 'saveEvidence', 'voidEvidencePhoto', 'completeLoadingTask']) {
const body = page.match(new RegExp(`(?:async )?${method}\\([^)]*\\) \\{[\\s\\S]*?\\n\\t\\t\\t\\},`))?.[0] || ''
assert.match(body, /if \(!this\.requireEvidenceStage\(\)\)(?: return| \{)/, `${method} should guard the evidence stage`)
}
})
+test('refresh preserves local photos for the same evidence identity and flushes them after loading readiness', () => {
+ assert.match(page, /this\.rememberEvidenceDraft\(\)/)
+ assert.match(page, /this\.restoreEvidenceDraft\(\)/)
+ assert.match(page, /async flushPendingEvidence\(\)[\s\S]*?queueEvidenceUpload/)
+ assert.match(page, /const localFiles = Object\.fromEntries/)
+})
+
+test('evidence photos render thumbnails and support full-screen preview', () => {
+ assert.match(page, /class="evidence-thumbnail" :src="evidencePreviewSource\(photo\)"/)
+ assert.match(page, /@tap="previewEvidencePhoto\(photo, slot\.type\)"/)
+ assert.match(page, /evidencePreviewSource\(photo\)[\s\S]*?localPath[\s\S]*?previewUrl[\s\S]*?fileUrl/)
+ assert.match(page, /previewEvidencePhoto\(photo, type\)[\s\S]*?uni\.previewImage\([\s\S]*?current[\s\S]*?urls/)
+ assert.match(page, /cacheEvidencePreview\(fileId, localPath/)
+ assert.match(page, /withCachedEvidencePreview\(photo[\s\S]*?evidencePreviewCache/)
+ assert.match(page, /已上传并保存/)
+})
+
test('switching cabinets invalidates old sync and evidence upload state', () => {
const selector = page.match(/selectBox\(index\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
assert.match(selector, /this\.taskRequestId \+= 1/)