Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1acd33347a | ||
|
|
63fac79814 | ||
|
|
e341551190 |
@@ -266,7 +266,7 @@
|
|||||||
.section-title { display: block; color: #12233f; font-size: 18px; font-weight: 700; }.section-heading { align-items: center; }.refresh-button { display: flex; align-items: center; justify-content: center; flex-shrink: 0; box-sizing: border-box; min-width: 88px; height: 44px; margin: 0; padding: 0 16px; border: 1px solid #9dc0f4; border-radius: 8px; color: #155eef; font-size: 16px; font-weight: 700; line-height: normal; background: #eaf2ff; }.refresh-button::after { border: 0; }.refresh-button[disabled] { color: #718095; border-color: #dce5f0; background: #e8edf3; }
|
.section-title { display: block; color: #12233f; font-size: 18px; font-weight: 700; }.section-heading { align-items: center; }.refresh-button { display: flex; align-items: center; justify-content: center; flex-shrink: 0; box-sizing: border-box; min-width: 88px; height: 44px; margin: 0; padding: 0 16px; border: 1px solid #9dc0f4; border-radius: 8px; color: #155eef; font-size: 16px; font-weight: 700; line-height: normal; background: #eaf2ff; }.refresh-button::after { border: 0; }.refresh-button[disabled] { color: #718095; border-color: #dce5f0; background: #e8edf3; }
|
||||||
.primary-button, .secondary-button { box-sizing: border-box; width: 100%; height: 46px; margin-top: 10px; border-radius: 9px; font-size: 14px; font-weight: 700; line-height: 44px; }.primary-button { border: 0; color: #fff; background: #1a73e8; }.secondary-button { border: 1px solid #8ab8ef; color: #155eef; background: #f6f9ff; }.primary-button[disabled],.secondary-button[disabled] { opacity: .45; }
|
.primary-button, .secondary-button { box-sizing: border-box; width: 100%; height: 46px; margin-top: 10px; border-radius: 9px; font-size: 14px; font-weight: 700; line-height: 44px; }.primary-button { border: 0; color: #fff; background: #1a73e8; }.secondary-button { border: 1px solid #8ab8ef; color: #155eef; background: #f6f9ff; }.primary-button[disabled],.secondary-button[disabled] { opacity: .45; }
|
||||||
.unseal-button { border-color: #fda29b; color: #b42318; background: #fff5f4; }.pallet-hint.blocked { color: #b42318; }
|
.unseal-button { border-color: #fda29b; color: #b42318; background: #fff5f4; }.pallet-hint.blocked { color: #b42318; }
|
||||||
.unseal-dialog { box-sizing: border-box; width: min(350px, calc(100vw - 32px)); overflow: hidden; border-radius: 12px; background: #fff; }.unseal-copy { display: block; padding: 18px 16px; color: #475467; font-size: 14px; line-height: 21px; }.unseal-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; padding: 0 16px 16px; }.unseal-actions .secondary-button { margin-top: 0; }.danger-button { height: 46px; margin: 0; border: 0; border-radius: 9px; color: #fff; font-size: 14px; font-weight: 700; background: #d92d20; }.danger-button[disabled] { opacity: .45; }
|
.unseal-dialog { box-sizing: border-box; width: min(350px, calc(100vw - 32px)); overflow: hidden; border-radius: 12px; background: #fff; }.unseal-copy { display: block; padding: 18px 16px; color: #475467; font-size: 14px; line-height: 21px; }.unseal-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; padding: 0 16px 16px; }.unseal-actions .secondary-button { margin-top: 0; }.danger-button { display: flex; align-items: center; justify-content: center; padding: 0; line-height: 46px; height: 46px; margin: 0; border: 0; border-radius: 9px; color: #fff; font-size: 14px; font-weight: 700; background: #d92d20; }.danger-button[disabled] { opacity: .45; }
|
||||||
.count-entry { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 6px; padding: 5px 7px; border: 0; border-radius: 0; color: #667085; font-size: 13px; background: #f6f9ff; }.compact-action { width: 86px; height: 34px; margin: 0; border: 0; border-radius: 6px; color: #fff; font-size: 13px; line-height: 34px; background: #1a73e8; }.compact-action[disabled] { opacity: .35; }.count-unavailable { padding: 12px 8px; color: #b42318; font-size: 13px; line-height: 18px; text-align: center; background: #fff1f0; }.pallet-scan-block { margin-top: 8px; padding-top: 8px; border-top: 1px solid #e6edf5; }.field-label { display: block; margin-bottom: 5px; color: #344054; font-size: 15px; font-weight: 700; }.field-label text { margin-left: 3px; color: #98a2b3; font-size: 12px; font-weight: 400; }
|
.count-entry { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 6px; padding: 5px 7px; border: 0; border-radius: 0; color: #667085; font-size: 13px; background: #f6f9ff; }.compact-action { width: 86px; height: 34px; margin: 0; border: 0; border-radius: 6px; color: #fff; font-size: 13px; line-height: 34px; background: #1a73e8; }.compact-action[disabled] { opacity: .35; }.count-unavailable { padding: 12px 8px; color: #b42318; font-size: 13px; line-height: 18px; text-align: center; background: #fff1f0; }.pallet-scan-block { margin-top: 8px; padding-top: 8px; border-top: 1px solid #e6edf5; }.field-label { display: block; margin-bottom: 5px; color: #344054; font-size: 15px; font-weight: 700; }.field-label text { margin-left: 3px; color: #98a2b3; font-size: 12px; font-weight: 400; }
|
||||||
.carton-card, .pallet-card { margin-top: 8px; padding: 8px 0 0; border: 0; border-top: 1px solid #e3eaf2; border-radius: 0; background: transparent; }.sku-count-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 8px; padding: 8px 0; border-bottom: 1px solid #edf1f5; }.sku-copy { min-width: 0; }.sku-label { display: block; color: #667085; font-size: 11px; }.sku-code { display: block; margin-top: 2px; color: #12233f; font-size: 16px; font-weight: 700; overflow-wrap: anywhere; }.pallet-meta { display: block; margin-top: 5px; color: #667085; font-size: 14px; line-height: 20px; }
|
.carton-card, .pallet-card { margin-top: 8px; padding: 8px 0 0; border: 0; border-top: 1px solid #e3eaf2; border-radius: 0; background: transparent; }.sku-count-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 8px; padding: 8px 0; border-bottom: 1px solid #edf1f5; }.sku-copy { min-width: 0; }.sku-label { display: block; color: #667085; font-size: 11px; }.sku-code { display: block; margin-top: 2px; color: #12233f; font-size: 16px; font-weight: 700; overflow-wrap: anywhere; }.pallet-meta { display: block; margin-top: 5px; color: #667085; font-size: 14px; line-height: 20px; }
|
||||||
.quantity-stepper { display: grid; grid-template-columns: 38px 56px 38px; overflow: hidden; border: 1px solid #b8c6d8; border-radius: 8px; background: #fff; }.quantity-stepper button,.quantity-stepper input { box-sizing: border-box; height: 40px; margin: 0; border: 0; border-radius: 0; text-align: center; background: #fff; }.quantity-stepper button { color: #155eef; font-size: 20px; line-height: 40px; }.quantity-stepper input { width: 56px; border-right: 1px solid #dce4ed; border-left: 1px solid #dce4ed; color: #12233f; font-size: 15px; }.difference-field { grid-column: 1 / -1; margin-top: 0; }.pallet-hint { display: block; margin-top: 10px; color: #155eef; font-size: 11px; text-align: center; }
|
.quantity-stepper { display: grid; grid-template-columns: 38px 56px 38px; overflow: hidden; border: 1px solid #b8c6d8; border-radius: 8px; background: #fff; }.quantity-stepper button,.quantity-stepper input { box-sizing: border-box; height: 40px; margin: 0; border: 0; border-radius: 0; text-align: center; background: #fff; }.quantity-stepper button { color: #155eef; font-size: 20px; line-height: 40px; }.quantity-stepper input { width: 56px; border-right: 1px solid #dce4ed; border-left: 1px solid #dce4ed; color: #12233f; font-size: 15px; }.difference-field { grid-column: 1 / -1; margin-top: 0; }.pallet-hint { display: block; margin-top: 10px; color: #155eef; font-size: 11px; text-align: center; }
|
||||||
|
|||||||
@@ -34,16 +34,22 @@
|
|||||||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||||||
import { listDropshipPalletizationTasks, resolveDropshipWorkspacePallet } from '@/api/pda-dropship.js'
|
import { listDropshipPalletizationTasks, resolveDropshipWorkspacePallet } from '@/api/pda-dropship.js'
|
||||||
import { session, requireSession } from '@/stores/session.js'
|
import { session, requireSession } from '@/stores/session.js'
|
||||||
|
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
|
||||||
import { formatApiError } from '@/utils/user-facing.js'
|
import { formatApiError } from '@/utils/user-facing.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { PortalHeader, ScanTargetInput },
|
components: { PortalHeader, ScanTargetInput },
|
||||||
data() { return { user: session.currentUser, keyword: '', tasks: [], loading: false, error: '', palletNo: '' } },
|
data() { return { user: session.currentUser, keyword: '', tasks: [], loading: false, error: '', palletNo: '' } },
|
||||||
onLoad(options) { this.palletNo = String(options.palletNo || '') },
|
onLoad(options) { this.palletNo = String(options.palletNo || '') },
|
||||||
onShow() { if (!requireSession()) return; this.user = session.currentUser; this.loadTasks() },
|
onShow() { if (!requireSession()) return; this.user = session.currentUser; this.startScanner(); this.loadTasks() },
|
||||||
|
onHide() { this.stopScanner() }, onUnload() { this.stopScanner() }, beforeUnmount() { this.stopScanner() },
|
||||||
methods: {
|
methods: {
|
||||||
|
// 入仓码广播直接驱动查询,不依赖输入框焦点。
|
||||||
|
startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: value => { if (this.loading) return; this.keyword = String(value).trim(); this.loadTasks() }, onError: () => { this.error = '扫码头连接失败,可输入入仓码查询' } }) },
|
||||||
|
stopScanner() { this._stopScanner?.(); this._stopScanner = null },
|
||||||
statusLabel(status) { return status === 'CLAIMED' ? '作业中' : '待领取' },
|
statusLabel(status) { return status === 'CLAIMED' ? '作业中' : '待领取' },
|
||||||
async loadTasks() {
|
async loadTasks(value) {
|
||||||
|
if (typeof value === 'string') this.keyword = value.trim()
|
||||||
if (this.loading) return
|
if (this.loading) return
|
||||||
if (!this.user?.warehouseId) { this.error = '请返回首页选择作业仓库'; return }
|
if (!this.user?.warehouseId) { this.error = '请返回首页选择作业仓库'; return }
|
||||||
this.loading = true; this.error = ''
|
this.loading = true; this.error = ''
|
||||||
|
|||||||
@@ -693,7 +693,8 @@
|
|||||||
return String(error?.code || '') === 'A0409' || Number(error?.statusCode) === 409
|
return String(error?.code || '') === 'A0409' || Number(error?.statusCode) === 409
|
||||||
},
|
},
|
||||||
remoteFailureMessage(error, fallback = '操作失败') {
|
remoteFailureMessage(error, fallback = '操作失败') {
|
||||||
return formatApiError(error, fallback)
|
console.error('[PDA picking] failed', { message: error?.message, code: error?.code, requestId: error?.requestId, traceId: error?.traceId })
|
||||||
|
return formatApiError(error, fallback, false)
|
||||||
},
|
},
|
||||||
async refreshAfterTaskConflict(message) {
|
async refreshAfterTaskConflict(message) {
|
||||||
const requestedTaskId = this.remoteTaskId || this.currentBox.taskId
|
const requestedTaskId = this.remoteTaskId || this.currentBox.taskId
|
||||||
|
|||||||
@@ -130,3 +130,27 @@ test('task list stops carrying a pallet sealed while another ticket was being wo
|
|||||||
assert.equal(state.palletNo, '')
|
assert.equal(state.palletNo, '')
|
||||||
assert.ok(!navigations[1].includes('DP-261019-0014'))
|
assert.ok(!navigations[1].includes('DP-261019-0014'))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('task list hardware scan searches inbound code and releases receiver when hidden', async () => {
|
||||||
|
const taskSource = await readFile(new URL('../pages/dropship-tasks/dropship-tasks.vue', import.meta.url), 'utf8')
|
||||||
|
const script = taskSource.match(/<script>([\s\S]*?)<\/script>/)[1].replace(/^\s*import .*$/gm, '').replace('export default', 'globalThis.page =')
|
||||||
|
let receiver, stopped = 0
|
||||||
|
const searches = []
|
||||||
|
const context = { PortalHeader: {}, ScanTargetInput: {}, session: { currentUser: { warehouseId: '100' } }, requireSession: () => true,
|
||||||
|
startScanBroadcast: options => { receiver = options; return () => { stopped++ } },
|
||||||
|
listDropshipPalletizationTasks: async (code, warehouse) => { searches.push([code, warehouse]); return [] },
|
||||||
|
formatApiError: error => error.message }
|
||||||
|
vm.runInNewContext(script, context)
|
||||||
|
const state = Object.assign(context.page.data(), context.page.methods)
|
||||||
|
context.page.onShow.call(state)
|
||||||
|
await new Promise(resolve => setImmediate(resolve))
|
||||||
|
receiver.onScan(' IN-2611050001 ')
|
||||||
|
await new Promise(resolve => setImmediate(resolve))
|
||||||
|
assert.deepEqual(searches.at(-1), ['IN-2611050001', '100'])
|
||||||
|
state.loading = true
|
||||||
|
receiver.onScan('SECOND')
|
||||||
|
assert.equal(searches.length, 2)
|
||||||
|
context.page.onHide.call(state)
|
||||||
|
context.page.onUnload.call(state)
|
||||||
|
assert.equal(stopped, 1)
|
||||||
|
})
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ test('picking page exposes five evidence slots, retry and remote completion stat
|
|||||||
for (const text of ['PHOTO_STEP_1', 'PHOTO_STEP_4', 'SEAL', '网络失败', '确认装柜完成']) {
|
for (const text of ['PHOTO_STEP_1', 'PHOTO_STEP_4', 'SEAL', '网络失败', '确认装柜完成']) {
|
||||||
assert.match(`${source}\n${page}`, new RegExp(text))
|
assert.match(`${source}\n${page}`, new RegExp(text))
|
||||||
}
|
}
|
||||||
assert.match(page, /createEvidenceUploadQueue/)
|
assert.match(page, /async queueEvidenceUpload\(type, path, sealNo = null\)/)
|
||||||
assert.match(page, /retryEvidence/)
|
assert.match(page, /@tap="refreshTask"/)
|
||||||
assert.match(page, /uploadEvidenceFile/)
|
assert.match(page, /uploadEvidenceFile/)
|
||||||
assert.match(page, /submitting \|\| \(!pickingCompleted && !summary\.canSubmit\)/)
|
assert.match(page, /submitting \|\| \(!pickingCompleted && !summary\.canSubmit\)/)
|
||||||
assert.doesNotMatch(page, /completePicking\(/)
|
assert.doesNotMatch(page, /completePicking\(/)
|
||||||
@@ -135,8 +135,8 @@ test('cabinet evidence never selects an inbound owner or builds a container rout
|
|||||||
const evidenceAdapter = source.match(/export function getEvidence\(taskId, warehouseId\)[\s\S]*?export function completeContainerPickingTask/)?.[0] || ''
|
const evidenceAdapter = source.match(/export function getEvidence\(taskId, warehouseId\)[\s\S]*?export function completeContainerPickingTask/)?.[0] || ''
|
||||||
for (const field of ['inboundId', 'tenantId', 'containerBookingId']) assert.doesNotMatch(evidenceAdapter, new RegExp(field))
|
for (const field of ['inboundId', 'tenantId', 'containerBookingId']) assert.doesNotMatch(evidenceAdapter, new RegExp(field))
|
||||||
assert.match(page, /captureEvidenceIdentity\(\) \{[\s\S]*?taskId:/)
|
assert.match(page, /captureEvidenceIdentity\(\) \{[\s\S]*?taskId:/)
|
||||||
assert.match(page, /uploadEvidenceFile\(item\.taskId, item\.warehouseId, item, item\.localPath/)
|
assert.match(page, /uploadEvidenceFile\(identity\.taskId, identity\.warehouseId,[\s\S]*?\}, path, progress/)
|
||||||
assert.match(page, /queue\.add\(\{ taskId: identity\.taskId, warehouseId: identity\.warehouseId, evidenceType: type, expectedVersion/)
|
assert.match(page, /evidenceType: type, expectedVersion: current\?\.version \?\? 0, sealNo/)
|
||||||
assert.doesNotMatch(page, /evidenceIdentity\?\.inboundId|identity\.inboundId/)
|
assert.doesNotMatch(page, /evidenceIdentity\?\.inboundId|identity\.inboundId/)
|
||||||
assert.doesNotMatch(page, /currentBox\.warehouseId \|\| session\.currentUser/)
|
assert.doesNotMatch(page, /currentBox\.warehouseId \|\| session\.currentUser/)
|
||||||
assert.match(source, /warehouseId=\$\{encodeURIComponent\(warehouseId\)\}/)
|
assert.match(source, /warehouseId=\$\{encodeURIComponent\(warehouseId\)\}/)
|
||||||
|
|||||||
@@ -266,9 +266,9 @@ test('picking mutations stay closed until the canonical task has been claimed',
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test('completion shows a Chinese server failure with a request correlation id', () => {
|
test('completion shows a concise Chinese failure and retains correlation ids in diagnostics', () => {
|
||||||
assert.match(page, /remoteFailureMessage\(error, fallback = '操作失败'\)/)
|
assert.match(page, /remoteFailureMessage\(error, fallback = '操作失败'\)/)
|
||||||
assert.match(page, /formatApiError\(error, fallback\)/)
|
assert.match(page, /formatApiError\(error, fallback, false\)/)
|
||||||
assert.match(page, /import \{ containerReference, formatApiError, sanitizeUserMessage \}/)
|
assert.match(page, /import \{ containerReference, formatApiError, sanitizeUserMessage \}/)
|
||||||
assert.match(page, /requestIdFromServer: error\.requestId/)
|
assert.match(page, /requestIdFromServer: error\.requestId/)
|
||||||
assert.match(page, /this\.remoteFailureMessage\(error, '装柜完成失败'\)/)
|
assert.match(page, /this\.remoteFailureMessage\(error, '装柜完成失败'\)/)
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ test('operator errors hide internal ids but retain request and trace evidence',
|
|||||||
assert.match(message, /错误码:S0500/)
|
assert.match(message, /错误码:S0500/)
|
||||||
assert.match(message, /requestId:dbde0d81/)
|
assert.match(message, /requestId:dbde0d81/)
|
||||||
assert.match(message, /traceId:5b0ba4f3/)
|
assert.match(message, /traceId:5b0ba4f3/)
|
||||||
|
const concise = formatApiError(error, '任务处理失败', false)
|
||||||
|
assert.doesNotMatch(concise, /错误码|requestId|traceId|S0500/)
|
||||||
|
assert.match(concise, /处理失败/)
|
||||||
assert.equal(sanitizeUserMessage('operation 2092889048731414530 failed'), 'operation 内部标识已隐藏 failed')
|
assert.equal(sanitizeUserMessage('operation 2092889048731414530 failed'), 'operation 内部标识已隐藏 failed')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -47,3 +50,8 @@ test('priority PDA templates do not interpolate internal ids', async () => {
|
|||||||
assert.match(pages[2], /containerReference\(currentBox\)/)
|
assert.match(pages[2], /containerReference\(currentBox\)/)
|
||||||
assert.match(pages[3], /containerReference\(entry\.box\)/)
|
assert.match(pages[3], /containerReference\(entry\.box\)/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('operator messages strip inline diagnostic suffixes', () => {
|
||||||
|
assert.equal(formatApiError({ message: '装柜证据文件上传失败(错误码:S0503;requestId:request-1;traceId:trace-1)' }, '上传失败', false), '装柜证据文件上传失败')
|
||||||
|
assert.equal(formatApiError({ message: '请先完成车辆取货送仓\nrequestId: request-1' }, '操作失败', false), '请先完成车辆取货送仓')
|
||||||
|
})
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function containerReference(source = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sanitizeUserMessage(message, fallback = '操作失败') {
|
export function sanitizeUserMessage(message, fallback = '操作失败') {
|
||||||
const raw = String(message ?? '').trim()
|
const raw = String(message ?? '').replace(/[((]\s*(?:错误码|code|requestId|traceId)\s*[::=][\s\S]*$/i, '').replace(/(?:requestId|traceId)\s*[::=][\s\S]*$/i, '').trim()
|
||||||
if (!raw) return fallback
|
if (!raw) return fallback
|
||||||
if (/^internal server error$/i.test(raw)) return '服务端处理异常,请联系管理员'
|
if (/^internal server error$/i.test(raw)) return '服务端处理异常,请联系管理员'
|
||||||
const sanitized = raw
|
const sanitized = raw
|
||||||
@@ -25,8 +25,9 @@ export function sanitizeUserMessage(message, fallback = '操作失败') {
|
|||||||
return sanitized || fallback
|
return sanitized || fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatApiError(error, fallback = '操作失败') {
|
export function formatApiError(error, fallback = '操作失败', includeDiagnostics = true) {
|
||||||
const message = sanitizeUserMessage(error?.message, fallback)
|
const message = sanitizeUserMessage(error?.message, fallback)
|
||||||
|
if (!includeDiagnostics) return message
|
||||||
const code = String(error?.code ?? '').trim()
|
const code = String(error?.code ?? '').trim()
|
||||||
const requestId = String(error?.requestId ?? '').trim()
|
const requestId = String(error?.requestId ?? '').trim()
|
||||||
const traceId = String(error?.traceId ?? '').trim()
|
const traceId = String(error?.traceId ?? '').trim()
|
||||||
|
|||||||
Reference in New Issue
Block a user