优化
This commit is contained in:
@@ -63,6 +63,6 @@
|
||||
|
||||
## 全量验收脚本
|
||||
|
||||
- PDA 本地自动回归在本目录执行 `node --test tests/*.test.mjs`,覆盖页面契约、导航、在线会话、拣货状态、幂等、证据上传队列、扫码广播和声音反馈。
|
||||
- 自动化通过后仍需按真实 uni-app 页面完成样式、扫码输入、弱网/重试、草稿恢复、批量提交和窄屏验收;在线更新与真机验收按既定安排执行。
|
||||
- 测试环境 API 或真实数据写入仍需当前对话明确授权;不要把本地 store 测试通过视为服务端作业链路已通过。
|
||||
- PDA 不单独定义一套“全量 HTTP”入口;PDA 服务链路由 Admin 全量 HTTP 入口中的 `phase3-pda-convergence`、装柜、证据和出仓模块覆盖。
|
||||
- 本目录 `node --test tests/*.test.mjs` 仅是 PDA 本地页面/状态质量检查,不代表服务端作业链路已通过。
|
||||
- 服务 HTTP 通过后,再按真实 uni-app 页面完成扫码输入、网络阻断/恢复、草稿恢复、批量提交、窄屏和在线更新验收;真机检查属于最后的交互验收。
|
||||
|
||||
+13
-2
@@ -19,6 +19,10 @@ const EVIDENCE_SLOTS = Object.freeze([
|
||||
// inbound-scoped task and evidence operations. No local task is promoted to a
|
||||
// server success when any required identifier or version is absent.
|
||||
|
||||
function debugLog(event, payload = {}) {
|
||||
try { console.info(`[PDA] ${event}`, payload) } catch (_) { /* console may be unavailable on a device */ }
|
||||
}
|
||||
|
||||
function request(options) {
|
||||
requireOnline()
|
||||
const baseUrl = session.apiBase || '/api/platform/v1'
|
||||
@@ -27,6 +31,7 @@ function request(options) {
|
||||
if (session.requestId) headers['X-Request-Id'] = session.requestId
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
debugLog('api.request', { method: options.method || 'GET', url: options.url })
|
||||
uni.request({
|
||||
...options,
|
||||
url: `${baseUrl}${options.url}`,
|
||||
@@ -34,6 +39,12 @@ function request(options) {
|
||||
success(response) {
|
||||
const body = response.data || {}
|
||||
const code = String(body.code ?? response.statusCode)
|
||||
debugLog('api.response', {
|
||||
method: options.method || 'GET', url: options.url, statusCode: response.statusCode, code,
|
||||
message: body.message || '', requestId: body.requestId || response.header?.['X-Request-Id'] || response.header?.['x-request-id'] || '',
|
||||
traceId: body.traceId || response.header?.['X-Trace-Id'] || response.header?.['x-trace-id'] || '',
|
||||
dataKeys: body.data && typeof body.data === 'object' ? Object.keys(body.data) : []
|
||||
})
|
||||
if (code !== '200') {
|
||||
const error = new Error(body.message || '服务端请求失败')
|
||||
error.code = body.code || response.statusCode
|
||||
@@ -46,6 +57,7 @@ function request(options) {
|
||||
resolve(body.data)
|
||||
},
|
||||
fail(error) {
|
||||
debugLog('api.network-error', { method: options.method || 'GET', url: options.url, errMsg: error.errMsg || '' })
|
||||
const next = new Error(error.errMsg || '网络连接失败')
|
||||
next.network = true
|
||||
reject(next)
|
||||
@@ -80,7 +92,7 @@ export function isRemoteReady(inboundId) {
|
||||
}
|
||||
|
||||
export function getContainerPickingTask(identity, warehouseId) {
|
||||
const selector = typeof identity === 'object' && identity !== null ? identity : { containerNo: identity }
|
||||
const selector = typeof identity === 'object' && identity !== null ? identity : {}
|
||||
const taskId = selector.taskId ?? selector.id
|
||||
if (taskId) {
|
||||
if (!warehouseId) throw new Error('缺少作业仓库标识,无法请求现场任务')
|
||||
@@ -92,7 +104,6 @@ export function getContainerPickingTask(identity, warehouseId) {
|
||||
const query = []
|
||||
if (selector.cabinetNo) query.push(`cabinetNo=${encodeURIComponent(selector.cabinetNo)}`)
|
||||
else if (selector.loadingPlanId) query.push(`loadingPlanId=${encodeURIComponent(selector.loadingPlanId)}`)
|
||||
else if (selector.containerNo) query.push(`containerNo=${encodeURIComponent(selector.containerNo)}`)
|
||||
else throw new Error('缺少稳定柜次标识,无法请求现场任务')
|
||||
if (warehouseId) query.push(`warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
if (session.tenantId) query.push(`tenantId=${encodeURIComponent(session.tenantId)}`)
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
import { session, restoreSession, signInWithTokens } from '@/stores/session.js'
|
||||
import { requireOnline } from '@/utils/network.js'
|
||||
export default {
|
||||
data() { return { form: { userId: '', password: '' }, error: '', submitting: false } },
|
||||
data() { return { form: { userId: 'platform_admin', password: 'ChangeMe123!' }, error: '', submitting: false } },
|
||||
onLoad() { if (session.currentUser || restoreSession()) uni.reLaunch({ url: '/pages/home/home' }) },
|
||||
methods: {
|
||||
async login() {
|
||||
|
||||
+50
-11
@@ -153,7 +153,11 @@
|
||||
<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="submitting || completedBoxes[currentBoxIndex] || !summary.canSubmit" @tap="submitPicking">
|
||||
<view v-if="remoteError && !pickingCompleted" class="remote-error-row submit-error-row">
|
||||
<text class="remote-error" role="alert">{{ remoteError }}</text>
|
||||
<button class="evidence-retry-btn" :disabled="remoteLoading" @tap="refreshTask">刷新任务</button>
|
||||
</view>
|
||||
<button class="submit-btn" aria-label="提交拣货完成" :disabled="submitting || completedBoxes[currentBoxIndex] || pickingCompleted || !summary.canSubmit" @tap="submitPicking">
|
||||
{{ submitting ? '正在提交…' : submitButtonText }}
|
||||
</button>
|
||||
</view>
|
||||
@@ -320,7 +324,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ITEM_STATUS, classifyScan, getPickSummary, submitStates } from '@/utils/picking-state.js'
|
||||
import { ITEM_STATUS, classifyScan, createItemStates, getPickSummary, submitStates } from '@/utils/picking-state.js'
|
||||
import { SKIP_REASONS, INCOMPLETE_REASONS } from '@/data/demo-data.js'
|
||||
import { session, requireSession } from '@/stores/session.js'
|
||||
import { clearPickingDraft, getPickingSession, restorePickingSession, savePickingSession } from '@/stores/picking.js'
|
||||
@@ -434,11 +438,11 @@
|
||||
if (available !== null && value > available) return `超过库存/库位可用量,可拣 ${available}`
|
||||
return ''
|
||||
},
|
||||
submitButtonText() {
|
||||
return this.completedBoxes[this.currentBoxIndex] ? '该箱已完成' : this.summary.buttonText
|
||||
submitButtonText() {
|
||||
return this.completedBoxes[this.currentBoxIndex] || this.pickingCompleted ? '该箱已完成' : this.summary.buttonText
|
||||
},
|
||||
submitStatusHint() {
|
||||
if (this.completedBoxes[this.currentBoxIndex]) return '本箱任务已完成'
|
||||
if (this.completedBoxes[this.currentBoxIndex] || this.pickingCompleted) return '本箱任务已完成'
|
||||
if (!this.summary.canSubmit) return '请先处理至少一件'
|
||||
if (this.summary.pending > 0) return '提交时需选择原因'
|
||||
return `已处理 ${this.summary.processed}/${this.summary.total}`
|
||||
@@ -496,6 +500,20 @@
|
||||
return false
|
||||
},
|
||||
methods: {
|
||||
debugSubmit(event, payload = {}) {
|
||||
try {
|
||||
console.info('[PDA] picking.submit', {
|
||||
event,
|
||||
taskId: this.remoteTaskId,
|
||||
loadingPlanId: this.currentBox.loadingPlanId,
|
||||
containerBookingId: this.currentBox.containerBookingId,
|
||||
warehouseId: this.currentBox.warehouseId || session.currentUser?.warehouseId || session.currentUser?.warehouse,
|
||||
pickingVersion: this.pickingVersion,
|
||||
pending: this.summary.pending,
|
||||
...payload
|
||||
})
|
||||
} catch (_) { /* console may be unavailable on a device */ }
|
||||
},
|
||||
isEvidenceContextActive(contextId, inboundId) {
|
||||
return contextId === this.evidenceContextId
|
||||
&& String(this.currentBox.inboundId ?? '') === String(inboundId ?? '')
|
||||
@@ -710,6 +728,7 @@
|
||||
async syncRemoteTask() {
|
||||
if (!isRemoteReady(this.currentBox.inboundId || this.currentBox.boxNo)) return
|
||||
const requestId = ++this.taskRequestId
|
||||
this.debugSubmit('refresh.start', { requestId, inboundId: this.currentBox.inboundId || null, cabinetNo: this.currentBox.cabinetNo || null })
|
||||
this.remoteLoading = true
|
||||
this.remoteError = ''
|
||||
try {
|
||||
@@ -722,7 +741,9 @@
|
||||
this.currentBox.items = remoteItems.map((item) => ({
|
||||
...item,
|
||||
id: item.id ?? item.itemId,
|
||||
taskItemId: item.itemId ?? item.id,
|
||||
// The batch endpoint expects the PDA task-item primary key (`id`),
|
||||
// not a nested loading-plan item alias when both are returned.
|
||||
taskItemId: item.id ?? item.itemId,
|
||||
boxCodes: Array.isArray(item.boxCodes) ? item.boxCodes.map(String) : [],
|
||||
orderNo: item.orderNo ?? '', inboundCode: item.inboundCode ?? '',
|
||||
desc: item.cargoName ?? item.desc ?? item.description ?? '箱级拣货明细',
|
||||
@@ -739,8 +760,17 @@
|
||||
this.currentBox.orderNo = firstItem.orderNo || firstItem.inboundCode || this.currentBox.orderNo
|
||||
this.currentBox.inboundId = firstItem.inboundId || this.currentBox.inboundId
|
||||
this.currentBox.tenantId = firstItem.tenantId || this.currentBox.tenantId
|
||||
this.itemStates = createItemStates(this.currentBox.items)
|
||||
this.partialQuantities = {}
|
||||
const serverStates = createItemStates(this.currentBox.items)
|
||||
const serverPartialQuantities = {}
|
||||
this.currentBox.items.forEach((item) => {
|
||||
if (item.itemStatus === 'PICKED') serverStates[item.id] = ITEM_STATUS.PICKED
|
||||
else if (item.itemStatus === 'PARTIAL') {
|
||||
serverStates[item.id] = ITEM_STATUS.PARTIAL
|
||||
serverPartialQuantities[item.id] = Number(item.pickedPackageCount ?? item.finalPickedPackageCount ?? 0)
|
||||
} else if (item.itemStatus === 'SKIPPED') serverStates[item.id] = ITEM_STATUS.SKIPPED
|
||||
})
|
||||
this.itemStates = serverStates
|
||||
this.partialQuantities = serverPartialQuantities
|
||||
this.scannedBoxCodes = {}
|
||||
}
|
||||
this.remoteTaskId = task.taskId ?? task.id
|
||||
@@ -761,12 +791,14 @@
|
||||
this.remotePick = normalizePickingTask({ ...task, ...(claimed || {}) })
|
||||
this.pickingVersion = this.remotePick.pickingVersion ?? this.remotePick.version ?? null
|
||||
this.loadingVersion = this.currentBox.inboundVersion ?? this.remotePick.inboundVersion ?? null
|
||||
this.debugSubmit('refresh.ready', { requestId, workflowStatus: this.remotePick.workflowStatus, taskStatus: this.remotePick.taskStatus, assignmentVersion: this.assignmentVersion, loadingVersion: this.loadingVersion })
|
||||
this.assignmentVersion = this.remotePick.assignmentVersion ?? this.assignmentVersion
|
||||
this.restoreRemoteDraft()
|
||||
if (!this.pickingCompleted) this.restoreRemoteDraft()
|
||||
this.resetEvidenceState()
|
||||
if (this.pickingCompleted) await this.loadLoadingEvidence()
|
||||
} catch (error) {
|
||||
if (requestId !== this.taskRequestId) return
|
||||
this.debugSubmit('refresh.error', { requestId, message: error.message || '', code: error.code || '', statusCode: error.statusCode || '', requestIdFromServer: error.requestId || '', traceId: error.traceId || '' })
|
||||
this.remoteError = error.network ? '网络不可用,无法开始拣货,恢复网络后请重试' : (error.message || '任务同步失败')
|
||||
} finally { if (requestId === this.taskRequestId) this.remoteLoading = false }
|
||||
},
|
||||
@@ -1021,7 +1053,7 @@
|
||||
this.requestBoxSwitch(entry.index)
|
||||
},
|
||||
submitPicking() {
|
||||
if (!this.summary.canSubmit || this.completedBoxes[this.currentBoxIndex]) return
|
||||
if (!this.summary.canSubmit || this.completedBoxes[this.currentBoxIndex] || this.pickingCompleted) return
|
||||
if (this.summary.pending > 0) {
|
||||
this.selectedIncompleteReason = ''
|
||||
this.openOverlay('submitConfirm')
|
||||
@@ -1036,6 +1068,8 @@
|
||||
async performSubmit(incompleteReason = '') {
|
||||
const beforeSubmit = this.summary
|
||||
const remoteRequired = Boolean(session.token)
|
||||
this.remoteError = ''
|
||||
this.debugSubmit('submit.start', { incompleteReason, remoteRequired, remoteReady: isRemoteReady(this.currentBox.inboundId || this.currentBox.boxNo) })
|
||||
if (remoteRequired && !isRemoteReady(this.currentBox.inboundId || this.currentBox.boxNo)) {
|
||||
this.remoteError = '缺少服务端任务信息,未提交拣货完成'
|
||||
this.showToast('拣货完成未提交', 'error')
|
||||
@@ -1051,6 +1085,8 @@
|
||||
this.submitId = submitId
|
||||
const warehouseId = this.currentBox.warehouseId || session.currentUser?.warehouseId || session.currentUser?.warehouse
|
||||
const inventorySnapshot = await getPickingInventorySnapshot(this.remoteTaskId, warehouseId)
|
||||
this.debugSubmit('submit.inventory-snapshot', { snapshotTokenLength: inventorySnapshot?.snapshotToken ? String(inventorySnapshot.snapshotToken).length : 0, taskVersion: inventorySnapshot?.version ?? null })
|
||||
if (!inventorySnapshot?.snapshotToken) throw new Error('库存快照未同步,请刷新任务后重试')
|
||||
const actions = this.currentBox.items.map((item) => {
|
||||
const status = this.itemState(item)
|
||||
const itemId = item.taskItemId ?? item.remoteItemId ?? item.id
|
||||
@@ -1060,15 +1096,18 @@
|
||||
if (incompleteReason) return { type: 'SKIP', itemId, reason: `${incompleteReason}(提交时确认未拣)`, version: Number(item.version || 0) }
|
||||
return null
|
||||
}).filter(Boolean)
|
||||
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 })
|
||||
// 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) })
|
||||
const versionConflict = error.code === 'A0409' || error.statusCode === 409
|
||||
const inventoryConflict = versionConflict && /库存/.test(error.message || '')
|
||||
const conflictMessage = inventoryConflict ? '库存已变化,请刷新后重试' : '任务版本已变化,请刷新后重试'
|
||||
this.remoteError = versionConflict ? conflictMessage : (error.network ? '网络失败,拣货完成未提交,请重试' : (error.message || '拣货完成失败'))
|
||||
this.showToast(versionConflict ? conflictMessage : '拣货完成未提交', 'error')
|
||||
this.showToast(versionConflict ? conflictMessage : '拣货完成未提交,请查看错误提示', 'error')
|
||||
return
|
||||
} finally { this.submitting = false }
|
||||
}
|
||||
|
||||
@@ -34,7 +34,13 @@ test('C02 identifiers and separate picking/inbound versions are normalized expli
|
||||
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, /expectedVersion: Number\(this\.pickingVersion\)/)
|
||||
assert.match(page, /ITEM_STATUS, classifyScan, createItemStates, getPickSummary, submitStates/)
|
||||
assert.match(page, /taskItemId: item\.id \?\? item\.itemId/)
|
||||
assert.match(page, /submit\.batch/)
|
||||
assert.match(page, /item\.itemStatus === 'PICKED'/)
|
||||
assert.match(page, /item\.itemStatus === 'PARTIAL'/)
|
||||
assert.match(page, /pickingCompleted \|\| !summary\.canSubmit/)
|
||||
assert.match(page, /this\.loadingVersion = this\.currentBox\.inboundVersion/)
|
||||
assert.match(page, /状态尚未同步,请刷新重试/)
|
||||
})
|
||||
@@ -84,6 +90,9 @@ test('PDA cabinet picking adapter claims online and submits one atomic batch', (
|
||||
assert.doesNotMatch(source.match(/export async function submitContainerPickingBatch[\s\S]*?\n\}/)?.[0] || '', /for \(/)
|
||||
assert.match(page, /version: Number\(item\.version \|\| 0\)/)
|
||||
assert.match(page, /refreshTask/)
|
||||
assert.match(page, /remoteError && !pickingCompleted/)
|
||||
assert.match(page, /库存快照未同步,请刷新任务后重试/)
|
||||
assert.match(page, /拣货完成未提交,请查看错误提示/)
|
||||
assert.match(page, /if \(!isPickingCompleteTask\(remoteTask\)\)[\s\S]*claimContainerPickingTask/)
|
||||
assert.match(page, /taskRequestId/)
|
||||
assert.match(page, /resetEvidenceState/)
|
||||
|
||||
Reference in New Issue
Block a user