优化
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
- 明确区分整单拣货、部分拣货、异常挂起和完成提交;关键状态转换需服务端确认,不能只依赖本地页面状态。
|
||||
- 作业提交应具备幂等标识,防止弱网、重复点击或重放造成重复扣减与重复完成。
|
||||
- 离线队列只有在业务批准后才能实现;队列必须可观察、可重试、可取消,并能识别冲突而不是静默覆盖服务端状态。
|
||||
- 与管理后台头程订单、仓库任务和 `services/api` 对齐稳定标识、状态机、时间和异常码,禁止在前端自行发明另一套业务状态。
|
||||
- 与管理后台头程订单、仓库任务和 `baiyue-firstleg` Controller/OpenAPI 对齐稳定标识、状态机、时间和异常码,禁止在前端自行发明另一套业务状态。
|
||||
|
||||
## 设备、安全与可用性
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { session } from '@/stores/session.js'
|
||||
|
||||
const EVIDENCE_TYPES = Object.freeze([
|
||||
'PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL'
|
||||
])
|
||||
|
||||
// C02 supplies inboundId/inboundVersion and loading.pickingVersion. C04 supplies
|
||||
// inbound-scoped task and evidence operations. No local task is promoted to a
|
||||
// server success when any required identifier or version is absent.
|
||||
|
||||
function request(options) {
|
||||
const baseUrl = session.apiBase || '/api/platform/v1'
|
||||
const headers = { ...(options.header || {}) }
|
||||
if (session.token) headers.Authorization = `Bearer ${session.token}`
|
||||
if (session.requestId) headers['X-Request-Id'] = session.requestId
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
...options,
|
||||
url: `${baseUrl}${options.url}`,
|
||||
header: headers,
|
||||
success(response) {
|
||||
const body = response.data || {}
|
||||
const code = String(body.code ?? response.statusCode)
|
||||
if (code !== '200') {
|
||||
const error = new Error(body.message || '服务端请求失败')
|
||||
error.code = body.code || response.statusCode
|
||||
error.statusCode = response.statusCode
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
resolve(body.data)
|
||||
},
|
||||
fail(error) {
|
||||
const next = new Error(error.errMsg || '网络连接失败')
|
||||
next.network = true
|
||||
reject(next)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function idempotencyKey(operation, inboundId) {
|
||||
return `pda-${operation}-${inboundId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||
}
|
||||
|
||||
function tenantQuery(tenantId) {
|
||||
const value = tenantId ?? session.tenantId
|
||||
if (!value) throw new Error('缺少客户隔离标识 tenantId,无法请求现场任务')
|
||||
return String(value)
|
||||
}
|
||||
|
||||
export function isRemoteReady(inboundId) {
|
||||
return Boolean(session.token && session.tenantId && inboundId)
|
||||
}
|
||||
|
||||
export function listLoadingOrders({ page = 1, size = 50, keyword = '' } = {}) {
|
||||
const query = [`mainStatus=LOADING`, `page=${page}`, `size=${size}`]
|
||||
if (session.tenantId) query.push(`tenantId=${encodeURIComponent(session.tenantId)}`)
|
||||
if (keyword) query.push(`keyword=${encodeURIComponent(keyword)}`)
|
||||
return request({ url: `/orders?${query.join('&')}`, method: 'GET' }).then((data) => ({
|
||||
...data,
|
||||
records: (data && data.records ? data.records : []).map(normalizePdaOrder)
|
||||
}))
|
||||
}
|
||||
|
||||
export function normalizePdaOrder(row = {}) {
|
||||
row = row || {}
|
||||
return {
|
||||
...row,
|
||||
inboundId: row.inboundId ?? row.inbound?.id ?? null,
|
||||
inboundVersion: row.inboundVersion ?? row.inbound?.version ?? null,
|
||||
pickingVersion: row.pickingVersion ?? row.loading?.pickingVersion ?? null
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizePickingTask(task = {}) {
|
||||
task = task || {}
|
||||
return {
|
||||
...task,
|
||||
pickingVersion: task.pickingVersion ?? task.version ?? null,
|
||||
inboundVersion: task.inboundVersion ?? null
|
||||
}
|
||||
}
|
||||
|
||||
export function getPickingTask(inboundId, tenantId) {
|
||||
return request({
|
||||
url: `/inbounds/${encodeURIComponent(inboundId)}/loading/picking?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
export function startPicking(inboundId, payload, tenantId) {
|
||||
return request({
|
||||
url: `/inbounds/${encodeURIComponent(inboundId)}/loading/picking/start?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`,
|
||||
method: 'POST',
|
||||
header: { 'Idempotency-Key': idempotencyKey('start-picking', inboundId) },
|
||||
data: payload
|
||||
})
|
||||
}
|
||||
|
||||
export function completePicking(inboundId, payload, tenantId) {
|
||||
return request({
|
||||
url: `/inbounds/${encodeURIComponent(inboundId)}/loading/picking/complete?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`,
|
||||
method: 'POST',
|
||||
header: { 'Idempotency-Key': idempotencyKey('complete-picking', inboundId) },
|
||||
data: payload
|
||||
})
|
||||
}
|
||||
|
||||
export function getEvidence(inboundId, tenantId) {
|
||||
return request({
|
||||
url: `/inbounds/${encodeURIComponent(inboundId)}/loading/evidence?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
export function upsertEvidence(inboundId, payload, tenantId) {
|
||||
if (!EVIDENCE_TYPES.includes(payload.evidenceType)) throw new Error('不支持的装柜证据类型')
|
||||
return request({
|
||||
url: `/inbounds/${encodeURIComponent(inboundId)}/loading/evidence?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`,
|
||||
method: 'POST',
|
||||
header: { 'Idempotency-Key': idempotencyKey(`evidence-${payload.evidenceType}`, inboundId) },
|
||||
data: payload
|
||||
})
|
||||
}
|
||||
|
||||
export function completeLoading(inboundId, payload, tenantId) {
|
||||
return request({
|
||||
url: `/inbounds/${encodeURIComponent(inboundId)}/loading/complete?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`,
|
||||
method: 'POST',
|
||||
header: { 'Idempotency-Key': idempotencyKey('complete-loading', inboundId) },
|
||||
data: payload
|
||||
})
|
||||
}
|
||||
|
||||
export { EVIDENCE_TYPES }
|
||||
+107
-3
@@ -117,13 +117,34 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="loading-evidence-section" v-if="currentBox.inboundId">
|
||||
<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>
|
||||
</view>
|
||||
<view class="evidence-grid">
|
||||
<view v-for="slot in evidenceSlots" :key="slot.type" class="evidence-card" :class="{ filled: evidenceEntry(slot.type) }">
|
||||
<text class="evidence-label">{{ slot.label }}</text>
|
||||
<text v-if="evidenceEntry(slot.type)" class="evidence-file">{{ evidenceEntry(slot.type).fileName || '已记录' }}</text>
|
||||
<button class="evidence-btn" @tap="chooseEvidence(slot)">{{ evidenceEntry(slot.type) ? '重新选择' : '上传照片' }}</button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="seal-row">
|
||||
<input v-model="sealNo" class="seal-input" placeholder="输入封签号(可选)" maxlength="64" />
|
||||
<button class="evidence-btn seal-btn" @tap="saveEvidence('SEAL')">保存封签</button>
|
||||
</view>
|
||||
<text v-if="remoteError" class="remote-error" role="alert">{{ remoteError }}</text>
|
||||
<button class="loading-complete-btn" :disabled="submitting || loadingVersion === null" @tap="completeLoadingTask">确认装柜完成</button>
|
||||
<text v-if="loadingVersion === null" class="version-hint">等待统一订单详情提供装柜版本号后可提交完成</text>
|
||||
</view>
|
||||
|
||||
<view 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>
|
||||
</view>
|
||||
<button class="submit-btn" aria-label="提交拣货完成" :disabled="completedBoxes[currentBoxIndex] || !summary.canSubmit" @tap="submitPicking">
|
||||
{{ submitButtonText }}
|
||||
<button class="submit-btn" aria-label="提交拣货完成" :disabled="submitting || completedBoxes[currentBoxIndex] || !summary.canSubmit" @tap="submitPicking">
|
||||
{{ submitting ? '正在提交…' : submitButtonText }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -296,6 +317,7 @@
|
||||
import { navigateBackOr } from '@/utils/navigation.js'
|
||||
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
|
||||
import { playErrorSound, playSuccessSound } from '@/utils/audio-feedback.js'
|
||||
import { completeLoading, completePicking, getEvidence, getPickingTask, isRemoteReady, normalizePickingTask, upsertEvidence } from '@/api/pda-loading.js'
|
||||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||||
import PopupHeader from '@/components/PopupHeader.vue'
|
||||
|
||||
@@ -320,6 +342,12 @@
|
||||
switchTargetIndex: null,
|
||||
selectedIncompleteReason: '',
|
||||
completionSummary: { picked: 0, partial: 0, skipped: 0, autoSkipped: 0 },
|
||||
evidenceSlots: [
|
||||
{ type: 'PHOTO_STEP_1', label: '装柜照片 1' }, { type: 'PHOTO_STEP_2', label: '装柜照片 2' },
|
||||
{ type: 'PHOTO_STEP_3', label: '装柜照片 3' }, { type: 'PHOTO_STEP_4', label: '装柜照片 4' }
|
||||
],
|
||||
evidenceFiles: {}, sealNo: '', loadingVersion: null, pickingVersion: null, remotePick: null,
|
||||
remoteError: '', remoteLoading: false, submitting: false,
|
||||
toast: { visible: false, message: '', type: '' }
|
||||
}
|
||||
},
|
||||
@@ -394,12 +422,20 @@
|
||||
.filter((entry) => this.boxListType === 'done'
|
||||
? Boolean(this.completedBoxes[entry.index])
|
||||
: !this.completedBoxes[entry.index])
|
||||
},
|
||||
remoteSyncState() {
|
||||
if (!this.currentBox.inboundId) return { label: '演示任务', tone: 'local' }
|
||||
if (!isRemoteReady(this.currentBox.inboundId)) return { label: '未连接', tone: 'error' }
|
||||
if (this.remoteLoading) return { label: '同步中', tone: 'loading' }
|
||||
if (this.remoteError) return { label: '同步失败', tone: 'error' }
|
||||
return { label: this.remotePick ? '已同步' : '待同步', tone: this.remotePick ? 'ready' : 'local' }
|
||||
}
|
||||
},
|
||||
onLoad() { requireSession() },
|
||||
onShow() {
|
||||
if (!requireSession()) return
|
||||
this.startHardwareScanner()
|
||||
this.syncRemoteTask()
|
||||
},
|
||||
onHide() { this.stopHardwareScanner() },
|
||||
onUnload() { this.stopHardwareScanner(); this.persistSession(); this.clearTimers() },
|
||||
@@ -550,6 +586,50 @@
|
||||
playSuccessSound()
|
||||
this.pickItem(result.item.id, 'scan')
|
||||
},
|
||||
evidenceEntry(type) { return this.evidenceFiles[type] || null },
|
||||
async syncRemoteTask() {
|
||||
if (!isRemoteReady(this.currentBox.inboundId)) return
|
||||
this.remoteLoading = true
|
||||
this.remoteError = ''
|
||||
try {
|
||||
this.remotePick = normalizePickingTask(await getPickingTask(this.currentBox.inboundId))
|
||||
this.pickingVersion = this.currentBox.pickingVersion ?? this.remotePick.pickingVersion
|
||||
this.loadingVersion = this.currentBox.inboundVersion ?? this.remotePick.inboundVersion ?? null
|
||||
const evidence = await getEvidence(this.currentBox.inboundId)
|
||||
this.evidenceFiles = (evidence && evidence.evidences ? evidence.evidences : []).reduce((all, item) => { all[item.evidenceType] = item; return all }, {})
|
||||
const seal = this.evidenceFiles.SEAL
|
||||
if (seal) this.sealNo = seal.sealNo || ''
|
||||
} catch (error) {
|
||||
this.remoteError = error.network ? '网络不可用,当前保留本地草稿,恢复网络后可重试' : (error.message || '任务同步失败')
|
||||
} finally { this.remoteLoading = false }
|
||||
},
|
||||
chooseEvidence(slot) {
|
||||
if (typeof uni.chooseImage !== 'function') { this.showToast('当前环境不支持选择照片', 'warning'); return }
|
||||
uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['camera', 'album'], success: (result) => {
|
||||
const path = result.tempFilePaths && result.tempFilePaths[0]
|
||||
if (!path) return
|
||||
this.evidenceFiles = { ...this.evidenceFiles, [slot.type]: { ...(this.evidenceFiles[slot.type] || {}), evidenceType: slot.type, fileName: path, localPath: path } }
|
||||
this.saveEvidence(slot.type)
|
||||
}, fail: () => this.showToast('未选择照片,可稍后重试', 'warning') })
|
||||
},
|
||||
async saveEvidence(type) {
|
||||
if (!this.currentBox.inboundId) { this.showToast('演示任务已记录本地证据', 'success'); return }
|
||||
if (!isRemoteReady(this.currentBox.inboundId)) { this.showToast('当前会话未连接服务端,证据保留本地', 'warning'); return }
|
||||
const current = this.evidenceFiles[type] || {}
|
||||
const payload = { expectedVersion: current.version || 0, evidenceType: type, fileId: current.fileId || null, fileName: current.fileName || null, sealNo: type === 'SEAL' ? this.sealNo : null, remark: null }
|
||||
try {
|
||||
const result = await upsertEvidence(this.currentBox.inboundId, payload)
|
||||
this.evidenceFiles = (result && result.evidences ? result.evidences : []).reduce((all, item) => { all[item.evidenceType] = item; return all }, this.evidenceFiles)
|
||||
this.showToast(`${type === 'SEAL' ? '封签' : '照片'}已保存`, 'success')
|
||||
} catch (error) { this.remoteError = error.network ? '网络失败,证据未提交,请重试' : (error.message || '证据保存失败'); this.showToast('证据保存失败', 'error') }
|
||||
},
|
||||
async completeLoadingTask() {
|
||||
if (!isRemoteReady(this.currentBox.inboundId) || this.loadingVersion === null) return
|
||||
this.submitting = true
|
||||
try { await completeLoading(this.currentBox.inboundId, { expectedVersion: this.loadingVersion, remark: 'PDA确认装柜完成' }); this.showToast('已确认装柜完成', 'success') }
|
||||
catch (error) { this.remoteError = error.network ? '网络失败,装柜完成未提交' : (error.message || '装柜完成失败'); this.showToast('装柜完成失败', 'error') }
|
||||
finally { this.submitting = false }
|
||||
},
|
||||
pickItem(id, source = 'manual') {
|
||||
const item = this.currentBox.items.find((entry) => entry.id === id)
|
||||
this.itemStates = { ...this.itemStates, [id]: ITEM_STATUS.PICKED }
|
||||
@@ -648,8 +728,32 @@
|
||||
if (!this.selectedIncompleteReason) return
|
||||
this.performSubmit(this.selectedIncompleteReason)
|
||||
},
|
||||
performSubmit(incompleteReason = '') {
|
||||
async performSubmit(incompleteReason = '') {
|
||||
const beforeSubmit = this.summary
|
||||
if (this.currentBox.inboundId && !isRemoteReady(this.currentBox.inboundId)) {
|
||||
this.remoteError = '缺少服务端会话或 tenantId,未提交拣货完成'
|
||||
this.showToast('拣货完成未提交', 'error')
|
||||
return
|
||||
}
|
||||
if (this.currentBox.inboundId && isRemoteReady(this.currentBox.inboundId)) {
|
||||
this.submitting = true
|
||||
try {
|
||||
if (this.pickingVersion === null) throw new Error('缺少拣货版本号 pickingVersion,无法提交')
|
||||
const expectedCount = this.currentBox.items.reduce((total, item) => total + Number(item.qty || 0), 0)
|
||||
const pickedCount = this.currentBox.items.reduce((total, item) => {
|
||||
const status = this.itemState(item)
|
||||
return total + (status === ITEM_STATUS.PICKED ? Number(item.qty || 0) : status === ITEM_STATUS.PARTIAL ? Number(this.partialQuantities[item.id] || 0) : 0)
|
||||
}, 0)
|
||||
this.remotePick = await completePicking(this.currentBox.inboundId, {
|
||||
expectedVersion: Number(this.pickingVersion),
|
||||
expectedCount, pickedCount, remark: incompleteReason || null
|
||||
})
|
||||
} catch (error) {
|
||||
this.remoteError = error.network ? '网络失败,拣货完成未提交,请重试' : (error.message || '拣货完成失败')
|
||||
this.showToast('拣货完成未提交', 'error')
|
||||
return
|
||||
} finally { this.submitting = false }
|
||||
}
|
||||
const result = submitStates(this.currentBox.items, this.itemStates)
|
||||
this.itemStates = result.states
|
||||
this.currentBox.items.forEach((item) => {
|
||||
|
||||
+8
-2
@@ -1,15 +1,21 @@
|
||||
import { reactive } from 'vue'
|
||||
import { resetPickingSession } from '@/stores/picking.js'
|
||||
|
||||
export const session = reactive({ currentUser: null })
|
||||
export const session = reactive({ currentUser: null, token: '', tenantId: '', apiBase: '' })
|
||||
|
||||
export function signIn(account, warehouse) {
|
||||
export function signIn(account, warehouse, options = {}) {
|
||||
session.currentUser = { id: account.id, name: account.name, role: account.role, warehouse }
|
||||
session.token = options.token || account.token || ''
|
||||
session.tenantId = options.tenantId || account.tenantId || ''
|
||||
session.apiBase = options.apiBase || account.apiBase || ''
|
||||
return session.currentUser
|
||||
}
|
||||
|
||||
export function signOut() {
|
||||
session.currentUser = null
|
||||
session.token = ''
|
||||
session.tenantId = ''
|
||||
session.apiBase = ''
|
||||
resetPickingSession()
|
||||
}
|
||||
|
||||
|
||||
@@ -1082,6 +1082,35 @@
|
||||
.gray { color: $skip; }
|
||||
.red { color: $danger; }
|
||||
|
||||
.loading-evidence-section {
|
||||
margin: 8px 12px 92px;
|
||||
padding: 14px;
|
||||
border: 1px solid #d8e4f4;
|
||||
border-radius: 10px;
|
||||
background: $card;
|
||||
box-shadow: $shadow;
|
||||
}
|
||||
.section-heading-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
|
||||
.section-title { display: block; color: $text; font-size: 15px; font-weight: 700; }
|
||||
.section-subtitle { display: block; margin-top: 4px; color: $text-secondary; font-size: 11px; line-height: 17px; }
|
||||
.sync-state { flex-shrink: 0; padding: 4px 7px; border-radius: 99px; font-size: 11px; background: #eef2f7; }
|
||||
.sync-state.ready { color: $success; background: #e8f8ef; }
|
||||
.sync-state.error { color: $danger; background: #fff0f0; }
|
||||
.sync-state.loading { color: #8a5a00; background: #fff7df; }
|
||||
.evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 12px; }
|
||||
.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 { display: block; overflow: hidden; margin-top: 5px; color: $text-secondary; font-size: 10px; line-height: 15px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.evidence-btn { min-height: 40px; margin-top: 8px; padding: 0 9px; border: 1px solid #9db7d9; border-radius: 7px; color: #174d91; font-size: 12px; line-height: 40px; background: #ffffff; }
|
||||
.seal-row { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
|
||||
.seal-input { flex: 1; height: 42px; box-sizing: border-box; padding: 0 10px; border: 1px solid $border; border-radius: 7px; font-size: 12px; background: #f8fafc; }
|
||||
.seal-btn { flex-shrink: 0; margin-top: 0; }
|
||||
.remote-error { display: block; margin-top: 9px; color: $danger; font-size: 11px; line-height: 17px; }
|
||||
.loading-complete-btn { width: 100%; min-height: 46px; margin-top: 12px; border-radius: 8px; color: #ffffff; font-size: 13px; font-weight: 700; line-height: 46px; background: $primary; }
|
||||
.loading-complete-btn[disabled] { color: #8c96a3; background: #e5e9ef; }
|
||||
.version-hint { display: block; margin-top: 6px; color: $text-secondary; font-size: 10px; line-height: 15px; text-align: center; }
|
||||
|
||||
.app-toast {
|
||||
position: fixed;
|
||||
z-index: 30;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
|
||||
const source = await readFile(new URL('../api/pda-loading.js', import.meta.url), 'utf8')
|
||||
const page = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8')
|
||||
|
||||
test('PDA loading adapter keeps C04 read and write paths', () => {
|
||||
for (const path of [
|
||||
'/orders?',
|
||||
'/loading/picking?tenantId=',
|
||||
'/loading/picking/start?tenantId=',
|
||||
'/loading/picking/complete?tenantId=',
|
||||
'/loading/evidence?tenantId=',
|
||||
'/loading/complete?tenantId='
|
||||
]) assert.match(source, new RegExp(path.replace(/[?]/g, '\\?')))
|
||||
assert.match(source, /'Idempotency-Key'/)
|
||||
assert.match(source, /PHOTO_STEP_1[\s\S]*PHOTO_STEP_4[\s\S]*SEAL/)
|
||||
})
|
||||
|
||||
test('PDA adapter does not treat non-200 response bodies or network failures as success', () => {
|
||||
assert.match(source, /if \(code !== '200'\)/)
|
||||
assert.match(source, /next\.network = true/)
|
||||
assert.match(source, /throw new Error\('缺少客户隔离标识 tenantId/)
|
||||
})
|
||||
|
||||
test('C02 identifiers and separate picking/inbound versions are normalized explicitly', () => {
|
||||
assert.match(source, /inboundId: row\.inboundId/)
|
||||
assert.match(source, /inboundVersion: row\.inboundVersion/)
|
||||
assert.match(source, /pickingVersion: row\.pickingVersion \?\? row\.loading\?\.pickingVersion/)
|
||||
assert.match(source, /pickingVersion: task\.pickingVersion \?\? task\.version/)
|
||||
assert.match(page, /expectedVersion: Number\(this\.pickingVersion\)/)
|
||||
assert.match(page, /this\.loadingVersion = this\.currentBox\.inboundVersion/)
|
||||
assert.match(page, /缺少拣货版本号 pickingVersion/)
|
||||
})
|
||||
|
||||
test('picking page exposes evidence, retry and remote completion states', () => {
|
||||
for (const text of ['PHOTO_STEP_1', 'PHOTO_STEP_4', 'SEAL', '网络失败', '确认装柜完成', 'loadingVersion']) {
|
||||
assert.match(page, new RegExp(text))
|
||||
}
|
||||
assert.match(page, /submitting \|\| completedBoxes\[currentBoxIndex\]/)
|
||||
assert.match(page, /completePicking\(/)
|
||||
})
|
||||
Reference in New Issue
Block a user