优化
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<view v-if="user" class="page-shell portal-page"><PortalHeader title="作业记录" :user="user" /><view class="portal-content"><view class="summary-card"><view><text class="summary-number">{{ overview.completedCount }}</text><text class="summary-label">今日完成</text></view><view><text class="summary-number pending-color">{{ overview.pendingBoxCount }}</text><text class="summary-label">待处理</text></view></view><text class="group-title">最近作业</text><view v-if="completedEntries.length === 0" class="history-empty"><text class="history-empty-icon">▤</text><text class="history-empty-title">暂无作业记录</text><text class="history-empty-desc">完成的现场任务会显示在这里</text></view><view v-else class="operation-list"><view v-for="entry in completedEntries" :key="entry.box.taskId || entry.index" class="history-item"><view class="history-check">✓</view><view class="operation-info"><text class="operation-name">集装箱拣货 · {{ displayBusinessValue(entry.box.currentContainerNo || entry.box.boxNo) }}</text><text class="operation-desc">{{ businessReference(entry.box) }}</text></view><text class="history-status">已完成</text></view></view></view></view>
|
||||
<view v-if="user" class="page-shell portal-page"><PortalHeader title="作业记录" :user="user" /><view class="portal-content"><view class="summary-card"><view><text class="summary-number">{{ overview.completedCount }}</text><text class="summary-label">今日完成</text></view><view><text class="summary-number pending-color">{{ overview.pendingBoxCount }}</text><text class="summary-label">待处理</text></view></view><text class="group-title">最近作业</text><view v-if="completedEntries.length === 0" class="history-empty"><text class="history-empty-icon">▤</text><text class="history-empty-title">暂无作业记录</text><text class="history-empty-desc">完成的现场任务会显示在这里</text></view><view v-else class="operation-list"><view v-for="entry in completedEntries" :key="entry.box.taskId || entry.index" class="history-item"><view class="history-check">✓</view><view class="operation-info"><text class="operation-name">集装箱拣货 · {{ containerReference(entry.box) }}</text></view><text class="history-status">已完成</text></view></view></view></view>
|
||||
</template>
|
||||
<script>
|
||||
import PortalHeader from '@/components/PortalHeader.vue'; import { session, requireSession } from '@/stores/session.js'; import { getPickingOverview } from '@/stores/picking.js'; import { businessReference, displayBusinessValue } from '@/utils/user-facing.js'
|
||||
export default { components: { PortalHeader }, data() { return { user: session.currentUser, overview: getPickingOverview() } }, computed: { completedEntries() { return this.overview.boxes.map((box, index) => ({ box, index })).filter((entry) => this.overview.completedBoxes[entry.index]) } }, onShow() { if (!requireSession()) return; this.user = session.currentUser; this.overview = getPickingOverview() }, methods: { businessReference, displayBusinessValue } }
|
||||
import PortalHeader from '@/components/PortalHeader.vue'; import { session, requireSession } from '@/stores/session.js'; import { getPickingOverview } from '@/stores/picking.js'; import { containerReference } from '@/utils/user-facing.js'
|
||||
export default { components: { PortalHeader }, data() { return { user: session.currentUser, overview: getPickingOverview() } }, computed: { completedEntries() { return this.overview.boxes.map((box, index) => ({ box, index })).filter((entry) => this.overview.completedBoxes[entry.index]) } }, onShow() { if (!requireSession()) return; this.user = session.currentUser; this.overview = getPickingOverview() }, methods: { containerReference } }
|
||||
</script>
|
||||
|
||||
+10
-16
@@ -14,7 +14,7 @@
|
||||
<view class="work-content" :class="{ 'evidence-active': activeStage === 'evidence' }">
|
||||
<view class="box-select-wrap">
|
||||
<button class="box-select-btn" @tap="toggleBoxDropdown">
|
||||
<text class="box-select-summary">集装箱号 {{ displayBusinessValue(currentBox.currentContainerNo) }} · 柜次号 {{ displayBusinessValue(currentBox.cabinetNo) }} · {{ businessReference(currentBox) }}</text>
|
||||
<text class="box-select-summary">{{ containerReference(currentBox) }}</text>
|
||||
<text class="box-chevron" :class="{ open: boxDropdownOpen }">▼</text>
|
||||
</button>
|
||||
<view v-if="boxDropdownOpen" class="box-dropdown">
|
||||
@@ -25,7 +25,7 @@
|
||||
:class="{ active: index === currentBoxIndex, completed: completedBoxes[index] }"
|
||||
@tap="requestBoxSwitch(index)"
|
||||
>
|
||||
<view><text class="box-option-number">集装箱号 {{ displayBusinessValue(box.currentContainerNo) }}</text><text class="box-option-order">柜次号 {{ displayBusinessValue(box.cabinetNo) }} · {{ businessReference(box) }}</text></view>
|
||||
<view><text class="box-option-number">{{ containerReference(box) }}</text></view>
|
||||
<text v-if="completedBoxes[index]" class="box-option-state">已完成</text>
|
||||
<text v-else-if="hasBoxDraft(index)" class="box-option-state draft">有草稿</text>
|
||||
</view>
|
||||
@@ -78,7 +78,7 @@
|
||||
:class="itemState(item)"
|
||||
>
|
||||
<view class="pick-item-row1">
|
||||
<text class="pick-item-sku">{{ itemBusinessReference(item) }}</text>
|
||||
<text class="pick-item-sku">拣货明细</text>
|
||||
<view class="pick-item-meta">
|
||||
<text v-if="itemState(item) === ITEM_STATUS.PICKED" class="pick-status-tag tag-picked">已拣 ✓</text>
|
||||
<text v-else-if="itemState(item) === ITEM_STATUS.PARTIAL" class="pick-status-tag tag-partial">
|
||||
@@ -190,7 +190,7 @@
|
||||
@tap="selectBoxFromList(entry)"
|
||||
>
|
||||
<view class="box-list-info">
|
||||
<text class="box-list-number">集装箱号 {{ displayBusinessValue(entry.box.currentContainerNo || entry.box.boxNo) }}{{ entry.index === currentBoxIndex ? '(当前)' : '' }}</text>
|
||||
<text class="box-list-number">{{ containerReference(entry.box) }}{{ entry.index === currentBoxIndex ? '(当前)' : '' }}</text>
|
||||
<text class="box-list-detail">{{ boxListDetail(entry) }}</text>
|
||||
</view>
|
||||
<text class="box-list-tag" :class="boxListType === 'done' ? 'box-list-tag-done' : 'box-list-tag-pending'">
|
||||
@@ -299,7 +299,7 @@
|
||||
<view class="complete-modal" role="dialog" aria-modal="true" aria-label="拣货完成" @tap.stop>
|
||||
<PopupHeader title="拣货完成" @close="closeOverlay" />
|
||||
<view class="complete-icon">✓</view>
|
||||
<text class="complete-desc">集装箱号 {{ displayBusinessValue(currentBox.currentContainerNo) }} · 柜次号 {{ displayBusinessValue(currentBox.cabinetNo) }} · {{ businessReference(currentBox) }}</text>
|
||||
<text class="complete-desc">{{ containerReference(currentBox) }}</text>
|
||||
<view class="complete-evidence-status">
|
||||
<text>装柜照片已拍 {{ evidenceCapturedTypeCount }}/{{ evidenceSlots.length }} 类</text>
|
||||
<text>{{ sealNo ? '封签号已填写' : '封签号未填写' }}</text>
|
||||
@@ -347,7 +347,7 @@
|
||||
import { stableOperationId } from '@/utils/idempotency.js'
|
||||
import { stableTaskOperationIdentity } from '@/utils/task-identity.js'
|
||||
import { COMPLETION_PHASE, resolveCompletionPhase, shouldReconcileCompletion } from '@/utils/loading-completion-state.js'
|
||||
import { businessReference, displayBusinessValue, formatApiError, sanitizeUserMessage } from '@/utils/user-facing.js'
|
||||
import { containerReference, formatApiError, sanitizeUserMessage } from '@/utils/user-facing.js'
|
||||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||||
import PopupHeader from '@/components/PopupHeader.vue'
|
||||
|
||||
@@ -562,13 +562,7 @@
|
||||
return false
|
||||
},
|
||||
methods: {
|
||||
displayBusinessValue,
|
||||
businessReference,
|
||||
itemBusinessReference(item) {
|
||||
if (String(item?.inboundCode ?? '').trim()) return `入仓码 ${item.inboundCode}`
|
||||
if (String(item?.orderNo ?? '').trim()) return `订单号 ${item.orderNo}`
|
||||
return '业务编号未关联'
|
||||
},
|
||||
containerReference,
|
||||
isSelectableTask(task) {
|
||||
return Boolean(task?.recordStatus === 'ACTIVE'
|
||||
&& !task?.mergedIntoContainerBookingId
|
||||
@@ -1467,7 +1461,7 @@
|
||||
this.itemStates = { ...this.itemStates,
|
||||
[item.id]: pickedCount >= plannedCount ? ITEM_STATUS.PICKED : ITEM_STATUS.PARTIAL }
|
||||
this.recordScanResult('success', '箱码扫描成功', boxCode,
|
||||
`${item.inboundCode || item.orderNo || '当前明细'} · 已拣 ${pickedCount}/${plannedCount} 箱`)
|
||||
`当前明细 · 已拣 ${pickedCount}/${plannedCount} 箱`)
|
||||
this.showToast(`已拣箱 ${pickedCount}/${plannedCount}`, 'success')
|
||||
this.persistSession()
|
||||
},
|
||||
@@ -1565,11 +1559,11 @@
|
||||
const processed = draft
|
||||
? Object.values(draft.itemStates).filter((status) => status !== ITEM_STATUS.PENDING).length
|
||||
: 0
|
||||
return `${businessReference(entry.box)} · 已处理${processed}件 · 待拣${entry.box.items.length - processed}件`
|
||||
return `已处理${processed}件 · 待拣${entry.box.items.length - processed}件`
|
||||
}
|
||||
const picked = entry.box.items.filter((item) => item.finalState === ITEM_STATUS.PICKED).length
|
||||
const partial = entry.box.items.filter((item) => item.finalState === ITEM_STATUS.PARTIAL).length
|
||||
return `${businessReference(entry.box)} · 拣出${picked}件${partial ? ` · 部分拣${partial}件` : ''}`
|
||||
return `拣出${picked}件${partial ? ` · 部分拣${partial}件` : ''}`
|
||||
},
|
||||
selectBoxFromList(entry) {
|
||||
if (this.boxListType === 'done' || entry.index === this.currentBoxIndex) return
|
||||
|
||||
@@ -14,12 +14,11 @@
|
||||
<view v-else-if="!tasks.length && !error" class="task-state">暂无待处理的拣货任务</view>
|
||||
<view v-if="notice" class="task-notice" role="status">{{ notice }}</view>
|
||||
<view class="task-list">
|
||||
<button v-for="task in tasks" :key="task.taskId" class="task-card" :disabled="!isTaskReady(task)" :aria-label="`${taskActionLabel(task)},集装箱号 ${displayBusinessValue(task.currentContainerNo)}`" @tap="enterTask(task)">
|
||||
<button v-for="task in tasks" :key="task.taskId" class="task-card" :disabled="!isTaskReady(task)" :aria-label="`${taskActionLabel(task)},${containerReference(task)}`" @tap="enterTask(task)">
|
||||
<view class="task-card-head">
|
||||
<view class="task-container-info"><text class="task-label">集装箱号</text><text class="task-code">{{ displayBusinessValue(task.currentContainerNo) }}</text></view>
|
||||
<view class="task-container-info"><text class="task-code">{{ containerReference(task) }}</text></view>
|
||||
<text class="task-status" :class="{ progress: task.workflowStatus === 'IN_PROGRESS', evidence: task.workflowStatus === 'AWAITING_REVIEW', processing: task.workflowStatus === 'PROCESSING', failed: task.workflowStatus === 'COMPLETION_FAILED' }">{{ statusLabel(task.workflowStatus) }}</text>
|
||||
</view>
|
||||
<view class="task-identifiers"><text class="task-cabinet">柜次号 {{ displayBusinessValue(task.cabinetNo) }}</text><text class="task-order">{{ businessReference(task) }}</text></view>
|
||||
<view class="task-card-foot">
|
||||
<view class="task-meta-group"><text class="task-warehouse">仓库 {{ displayBusinessValue(task.warehouseName, '未关联') }}</text><text class="task-pending">待处理 {{ task.pendingItemCount ?? task.itemCount ?? 0 }} 项</text></view>
|
||||
<view class="task-enter"><text>{{ taskActionLabel(task) }}</text><text class="task-enter-arrow">›</text></view>
|
||||
@@ -37,7 +36,7 @@
|
||||
import { session, requireSession } from '@/stores/session.js'
|
||||
import { clearPickingDraft } from '@/stores/picking.js'
|
||||
import { isOnline, watchNetwork } from '@/utils/network.js'
|
||||
import { businessReference, displayBusinessValue, formatApiError } from '@/utils/user-facing.js'
|
||||
import { containerReference, displayBusinessValue, formatApiError } from '@/utils/user-facing.js'
|
||||
|
||||
export default {
|
||||
components: { PortalHeader },
|
||||
@@ -47,7 +46,7 @@
|
||||
onUnload() { if (this._stopNetwork) this._stopNetwork(); clearTimeout(this._toastTimer) },
|
||||
methods: {
|
||||
displayBusinessValue,
|
||||
businessReference,
|
||||
containerReference,
|
||||
statusLabel(status) {
|
||||
if (status === 'PROCESSING') return '完成处理中'
|
||||
if (status === 'COMPLETION_FAILED') return '完成失败'
|
||||
@@ -106,16 +105,12 @@
|
||||
.task-card[disabled] { opacity: .62; }
|
||||
.task-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
|
||||
.task-container-info { display: flex; min-width: 0; flex-direction: column; }
|
||||
.task-label { color: #667085; font-size: 10px; line-height: 15px; }
|
||||
.task-code { margin-top: 1px; color: #12233f; font-size: 17px; font-weight: 700; line-height: 23px; overflow-wrap: anywhere; }
|
||||
.task-status { flex-shrink: 0; padding: 4px 7px; border-radius: 99px; color: #52616f; font-size: 10px; line-height: 16px; background: #eef2f6; }
|
||||
.task-status.progress { color: #155eef; background: #eff4ff; }
|
||||
.task-status.evidence { color: #9a5b13; background: #fff4e5; }
|
||||
.task-status.processing { color: #155eef; background: #eff4ff; }
|
||||
.task-status.failed { color: #b42318; background: #fff1f0; }
|
||||
.task-identifiers { display: flex; flex-wrap: wrap; gap: 5px 8px; margin-top: 8px; }
|
||||
.task-cabinet, .task-order { padding: 3px 7px; border-radius: 5px; color: #475467; font-size: 11px; line-height: 17px; background: #f2f4f7; }
|
||||
.task-order { max-width: 100%; overflow-wrap: anywhere; white-space: normal; }
|
||||
.task-card-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 10px; padding-top: 9px; border-top: 1px solid #edf1f5; }
|
||||
.task-meta-group { display: flex; min-width: 0; flex-direction: column; gap: 2px; }
|
||||
.task-warehouse { color: #667085; font-size: 10px; line-height: 15px; overflow-wrap: anywhere; }
|
||||
|
||||
@@ -63,9 +63,8 @@ test('picking task list uses remote loading orders and never falls back to demo
|
||||
assert.match(tasks, /\['UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW', 'PROCESSING', 'COMPLETION_FAILED'\]\.includes\(task\.workflowStatus\)/)
|
||||
assert.match(tasks, /\['AWAITING_REVIEW', 'COMPLETION_FAILED'\]\.includes\(task\.workflowStatus\) \|\| \(task\.assignmentVersion !== null/)
|
||||
assert.match(tasks, /暂无待处理的拣货任务/)
|
||||
assert.match(tasks, /class="task-label">集装箱号<\/text><text class="task-code">\{\{ displayBusinessValue\(task\.currentContainerNo\) \}\}/)
|
||||
assert.match(tasks, /柜次号 \{\{ displayBusinessValue\(task\.cabinetNo\) \}\}/)
|
||||
assert.match(tasks, /\{\{ businessReference\(task\) \}\}/)
|
||||
assert.match(tasks, /class="task-code">\{\{ containerReference\(task\) \}\}/)
|
||||
assert.doesNotMatch(tasks, /task-identifiers|businessReference\(task\)/)
|
||||
assert.match(tasks, /button v-for="task in tasks"[\s\S]*?class="task-card"[\s\S]*?@tap="enterTask\(task\)"/)
|
||||
assert.match(tasks, /records\.filter\(\(task\) => this\.isTaskVisible\(task\)\)/)
|
||||
assert.match(tasks, /isTaskReady\(task\)[\s\S]*?task\.recordStatus === 'ACTIVE'[\s\S]*?!task\.mergedIntoContainerBookingId/)
|
||||
|
||||
@@ -281,7 +281,7 @@ test('picking mutations stay closed until the canonical task has been claimed',
|
||||
test('completion shows a Chinese server failure with a request correlation id', () => {
|
||||
assert.match(page, /remoteFailureMessage\(error, fallback = '操作失败'\)/)
|
||||
assert.match(page, /formatApiError\(error, fallback\)/)
|
||||
assert.match(page, /import \{ businessReference, displayBusinessValue, formatApiError, sanitizeUserMessage \}/)
|
||||
assert.match(page, /import \{ containerReference, formatApiError, sanitizeUserMessage \}/)
|
||||
assert.match(page, /requestIdFromServer: error\.requestId/)
|
||||
assert.match(page, /this\.remoteFailureMessage\(error, '装柜完成失败'\)/)
|
||||
})
|
||||
|
||||
@@ -11,7 +11,7 @@ test('picking task list exposes only operator-facing production information', as
|
||||
])
|
||||
|
||||
assert.match(tasks, /共 \$\{tasks\.length\} 个待处理任务/)
|
||||
assert.match(tasks, /\{\{ businessReference\(task\) \}\}/)
|
||||
assert.match(tasks, /\{\{ containerReference\(task\) \}\}/)
|
||||
assert.match(tasks, /displayBusinessValue\(task\.warehouseName, '未关联'\)/)
|
||||
assert.doesNotMatch(tasks, /接口返回|当前接口|apiEnvironment|rawTaskCount|账号:|未命名订单|缺少仓库信息|服务端就绪校验|console\.info/)
|
||||
assert.doesNotMatch(adapter, /业务单号待同步/)
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { businessReference, displayBusinessValue, formatApiError, sanitizeUserMessage } from '../utils/user-facing.js'
|
||||
import { containerReference, displayBusinessValue, formatApiError, sanitizeUserMessage } from '../utils/user-facing.js'
|
||||
|
||||
const read = (path) => readFile(new URL(path, import.meta.url), 'utf8')
|
||||
|
||||
test('business identifiers never fall back to an internal id', () => {
|
||||
test('PDA task labels prefer the container number and only fall back to the cabinet number', () => {
|
||||
assert.equal(displayBusinessValue(null), '—')
|
||||
assert.equal(displayBusinessValue('', '未关联'), '未关联')
|
||||
assert.equal(businessReference({ orderNo: 'SO-2026-001', inboundCode: 'RK-2026-002', taskId: '2092889048731414530' }), '订单号 SO-2026-001 · 入仓码 RK-2026-002')
|
||||
assert.equal(businessReference({ taskId: '2092889048731414530', inboundId: '2092889048731414531' }), '业务编号未关联')
|
||||
assert.equal(containerReference({ currentContainerNo: 'TSTU0000760', cabinetNo: 'CAB-260830-0008', orderNo: 'FL-1' }), '集装箱号 TSTU0000760')
|
||||
assert.equal(containerReference({ currentContainerNo: '', boxNo: 'TSTU0000708', cabinetNo: 'CAB-260829-0037' }), '集装箱号 TSTU0000708')
|
||||
assert.equal(containerReference({ cabinetNo: 'CAB-260830-0008', taskId: '2092889048731414530' }), '柜次号 CAB-260830-0008')
|
||||
assert.equal(containerReference({ boxNo: '同步中', cabinetNo: 'CAB-260830-0008' }), '柜次号 CAB-260830-0008')
|
||||
assert.equal(containerReference({ taskId: '2092889048731414530', orderId: '2092889048731414531' }), '集装箱号 —')
|
||||
})
|
||||
|
||||
test('operator errors hide internal ids but retain request and trace evidence', () => {
|
||||
@@ -37,9 +40,10 @@ test('priority PDA templates do not interpolate internal ids', async () => {
|
||||
for (const page of pages) {
|
||||
const template = page.match(/<template>[\s\S]*?<\/template>/)?.[0] || ''
|
||||
assert.doesNotMatch(template, /\{\{[^}\n]*(?:taskId|inboundId|orderId|loadingPlanId|containerBookingId)[^}\n]*\}\}/)
|
||||
assert.doesNotMatch(template, /订单号|入仓码|业务编号未关联/)
|
||||
}
|
||||
assert.match(pages[0], /formatApiError\(error, '登录失败'\)/)
|
||||
assert.match(pages[1], /businessReference\(task\)/)
|
||||
assert.match(pages[2], /businessReference\(currentBox\)/)
|
||||
assert.match(pages[3], /businessReference\(entry\.box\)/)
|
||||
assert.match(pages[1], /containerReference\(task\)/)
|
||||
assert.match(pages[2], /containerReference\(currentBox\)/)
|
||||
assert.match(pages[3], /containerReference\(entry\.box\)/)
|
||||
})
|
||||
|
||||
@@ -6,13 +6,13 @@ export function displayBusinessValue(value, missing = '—') {
|
||||
return text || missing
|
||||
}
|
||||
|
||||
export function businessReference(source = {}) {
|
||||
const parts = []
|
||||
const orderNo = String(source?.orderNo ?? '').trim()
|
||||
const inboundCode = String(source?.inboundCode ?? '').trim()
|
||||
if (orderNo) parts.push(`订单号 ${orderNo}`)
|
||||
if (inboundCode && inboundCode !== orderNo) parts.push(`入仓码 ${inboundCode}`)
|
||||
return parts.length ? parts.join(' · ') : '业务编号未关联'
|
||||
export function containerReference(source = {}) {
|
||||
const containerNo = [source?.currentContainerNo, source?.containerNo, source?.boxNo]
|
||||
.map((value) => String(value ?? '').trim())
|
||||
.find((value) => value && value !== '同步中') || ''
|
||||
if (containerNo) return `集装箱号 ${containerNo}`
|
||||
const cabinetNo = String(source?.cabinetNo ?? '').trim()
|
||||
return cabinetNo ? `柜次号 ${cabinetNo}` : '集装箱号 —'
|
||||
}
|
||||
|
||||
export function sanitizeUserMessage(message, fallback = '操作失败') {
|
||||
|
||||
Reference in New Issue
Block a user