优化
This commit is contained in:
+97
-26
@@ -33,10 +33,10 @@
|
||||
</view>
|
||||
<view class="stage-switch" role="tablist" aria-label="作业阶段">
|
||||
<button class="stage-tab" :class="{ active: activeStage === 'picking' }" role="tab" :aria-selected="activeStage === 'picking'" @tap="selectStage('picking')">拣货作业</button>
|
||||
<button class="stage-tab" :class="{ active: activeStage === 'evidence', locked: !loadingStageReady }" role="tab" :aria-selected="activeStage === 'evidence'" :aria-disabled="!loadingStageReady" @tap="selectStage('evidence')">装柜取证<text v-if="!loadingStageReady" class="stage-tab-hint">完成拣货后开放</text></button>
|
||||
<button class="stage-tab" :class="{ active: activeStage === 'evidence' }" role="tab" :aria-selected="activeStage === 'evidence'" @tap="selectStage('evidence')">装柜取证<text v-if="!loadingStageReady" class="stage-tab-hint">照片可先暂存</text></button>
|
||||
</view>
|
||||
|
||||
<view v-if="activeStage === 'picking' || !loadingStageReady" class="picking-stage">
|
||||
<view v-if="activeStage === 'picking'" class="picking-stage">
|
||||
<view class="scan-section">
|
||||
<view class="scan-row">
|
||||
<ScanTargetInput v-model="scanValue" name="boxCode" label="箱码扫码框" placeholder="扫描或输入箱唛标签码" :active="activeScanField === 'boxCode'" @activate="setScanTarget" @submit="handleManualSubmit" />
|
||||
@@ -123,7 +123,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loadingStageReady && activeStage === 'evidence'" class="loading-evidence-section">
|
||||
<view v-if="activeStage === 'evidence'" class="loading-evidence-section">
|
||||
<view class="section-heading-row">
|
||||
<view><text class="section-title">装柜证据</text><text class="section-subtitle">空仓、半仓、满仓、锁仓及带锁码照</text></view>
|
||||
<text class="sync-state" :class="remoteSyncState.tone">{{ remoteSyncState.label }}</text>
|
||||
@@ -132,12 +132,14 @@
|
||||
<view v-for="slot in evidenceSlots" :key="slot.type" class="evidence-card" :class="{ filled: evidenceEntries(slot.type).length }">
|
||||
<text class="evidence-label">{{ slot.label }}</text>
|
||||
<view v-for="photo in evidenceEntries(slot.type)" :key="photo.id || photo.fileId || photo.localPath" class="evidence-file-row">
|
||||
<text class="evidence-file">{{ photo.fileName || '已记录' }}</text>
|
||||
<image v-if="evidencePreviewSource(photo)" class="evidence-thumbnail" :src="evidencePreviewSource(photo)" mode="aspectFill" @tap="previewEvidencePhoto(photo, slot.type)" />
|
||||
<view class="evidence-file-detail" @tap="previewEvidencePhoto(photo, slot.type)"><text class="evidence-file">{{ photo.fileName || '已记录' }}</text></view>
|
||||
<button v-if="photo.id && photo.status !== 'VOID' && photo.status !== 'VOIDED'" class="evidence-retry-btn" @tap="voidEvidencePhoto(photo)">作废</button>
|
||||
</view>
|
||||
<text v-if="evidenceQueueEntry(slot.type)" class="evidence-upload-state" :class="evidenceQueueEntry(slot.type).status.toLowerCase()">
|
||||
{{ evidenceUploadState(slot.type) }}
|
||||
</text>
|
||||
<text v-else-if="hasPendingLocalEvidence(slot.type)" class="evidence-upload-state pending">本地暂存,待自动上传</text>
|
||||
<button class="evidence-btn" @tap="chooseEvidence(slot)">{{ evidenceUploadLabel(slot.type) }}</button>
|
||||
<button v-if="evidenceQueueEntry(slot.type)?.status === 'FAILED'" class="evidence-retry-btn" @tap="retryEvidence(slot.type)">重试上传</button>
|
||||
</view>
|
||||
@@ -146,6 +148,7 @@
|
||||
<input v-model="sealNo" class="seal-input" placeholder="输入封签号(必填)" maxlength="64" />
|
||||
<button class="evidence-btn seal-btn" @tap="saveSealNo">保存封签</button>
|
||||
</view>
|
||||
<view v-if="!loadingStageReady" class="evidence-stage-hint" role="status">当前可先拍照留存;拣货完成并同步装柜状态后,照片会自动上传绑定。</view>
|
||||
<view v-if="remoteError" class="remote-error-row"><text class="remote-error" role="alert">{{ remoteError }}</text><button class="evidence-retry-btn" :disabled="remoteLoading" @tap="refreshTask">刷新任务</button></view>
|
||||
<button class="loading-complete-btn" :disabled="submitting || loadingVersion === null || !evidenceComplete" @tap="completeLoadingTask">确认装柜完成</button>
|
||||
<view v-if="loadingVersion === null" class="remote-error-row">
|
||||
@@ -153,12 +156,12 @@
|
||||
<button class="evidence-retry-btn" :disabled="remoteLoading" @tap="refreshTask">刷新任务</button>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="pickingCompleted && !loadingStageReady" class="remote-error-row submit-error-row">
|
||||
<view v-if="pickingCompleted && !loadingStageReady && activeStage === 'picking'" class="remote-error-row submit-error-row">
|
||||
<text class="version-hint" role="status">拣货已完成,装柜状态尚未同步,请刷新重试</text>
|
||||
<button class="evidence-retry-btn" :disabled="remoteLoading" @tap="refreshTask">刷新任务</button>
|
||||
</view>
|
||||
|
||||
<view v-if="activeStage === 'picking' || !loadingStageReady" class="submit-section">
|
||||
<view v-if="activeStage === 'picking'" class="submit-section">
|
||||
<view class="submit-info">
|
||||
<view class="submit-pending"><text class="submit-pending-number">{{ summary.pending }}</text><text class="submit-pending-label"> 箱待拣</text></view>
|
||||
<text class="submit-status-hint">{{ submitStatusHint }}</text>
|
||||
@@ -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) })
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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/)
|
||||
|
||||
Reference in New Issue
Block a user