Merge remote-tracking branch 'origin/release/test' into release/test
This commit is contained in:
@@ -5,8 +5,6 @@
|
||||
<text class="heading-progress">{{ busy ? '同步中' : snapshot ? `${completedCategories}/5 类已保存` : '待同步' }}</text>
|
||||
<button class="heading-refresh" :disabled="busy" @tap="refresh">刷新</button>
|
||||
</view>
|
||||
<text class="section-hint">每类至少 1 张、最多 5 张,上传后自动保存,可逐张更换。</text>
|
||||
<text class="section-hint">司机驾驶证、车牌照片在理货审核通过后自动带入,可补拍或更换;PDF 驾驶证可点击查看。</text>
|
||||
<text v-if="needsRefresh" class="section-hint">保存状态尚未核实,请先刷新资料后再上传。</text>
|
||||
<view v-if="error" class="error" role="alert"><text>{{ error.message }}</text><text v-if="error.requestId">requestId:{{ error.requestId }}</text><text v-if="error.traceId">traceId:{{ error.traceId }}</text></view>
|
||||
<view class="photo-list">
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<view class="header-menu-sheet">
|
||||
<view class="header-menu-content">
|
||||
<PopupHeader title="功能菜单" @close="closeMenu" />
|
||||
<button class="header-menu-item" @tap="goHistory"><uni-icons type="list" color="#334155" size="22" /><text>作业记录</text><uni-icons type="right" color="#94a3b8" size="18" /></button>
|
||||
<button class="header-menu-item" @tap="goProfile"><uni-icons type="person" color="#334155" size="22" /><text>个人中心</text><uni-icons type="right" color="#94a3b8" size="18" /></button>
|
||||
<button class="header-menu-close" @tap="closeMenu">关闭</button>
|
||||
</view>
|
||||
@@ -39,7 +38,6 @@
|
||||
methods: {
|
||||
goBack() { navigateBackOr(this.fallbackUrl) },
|
||||
goProfile() { this.closeMenu(); navigateToUnique('/pages/profile/profile') },
|
||||
goHistory() { this.closeMenu(); navigateToUnique('/pages/history/history') },
|
||||
openMenu() { this.$refs.menuPopup.open() },
|
||||
closeMenu() { if (this.$refs.menuPopup) this.$refs.menuPopup.close() },
|
||||
handleMenuChange(event) { this.$emit('menu-change', Boolean(event.show)) }
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</view>
|
||||
<view v-if="!current || activeTab === 'picking'" class="fulfillment-card dispatch-scan-card">
|
||||
<text class="card-title">{{ current ? '扫码拣货' : '扫码查询排车' }}</text>
|
||||
<ScanTargetInput name="dispatch" :model-value="scanCode" label="排车单号/实物托盘码/来源箱唛" placeholder="扫描业务单号或实物码" :active="activeTab === 'picking'" @update:modelValue="scanCode = $event" @submit="handleScan" />
|
||||
<ScanTargetInput name="dispatch" :model-value="scanCode" label="排车单号/实物托盘码/来源箱唛" placeholder="扫托盘码" :active="activeTab === 'picking'" @update:modelValue="scanCode = $event" @submit="handleScan" />
|
||||
<button class="secondary-button" :disabled="busy || loading || !scanCode" @tap="handleScan(scanCode)">匹配扫描</button>
|
||||
</view>
|
||||
<view v-if="loading" class="fulfillment-state"><text>正在加载排车事实…</text></view>
|
||||
@@ -32,9 +32,7 @@
|
||||
<DispatchHandoverStatus v-if="departed" :dispatch-no="current.dispatchNo" :record-version="current.recordVersion" />
|
||||
<view class="fulfillment-card summary-card">
|
||||
<text class="card-title">任务摘要</text>
|
||||
<view class="task-head"><text class="business-code">{{ current.dispatchNo }}</text><text class="status-badge">{{ stageLabel(current) }}</text></view>
|
||||
<text class="task-meta">任务号 {{ current.taskNo || '—' }}</text>
|
||||
<text v-if="!gate('PICK') && current.stage === 'LOADING'" class="gate-warning">{{ current.actions.PICK?.blockReason || '拣货资格尚未允许,请核对两级审核及当前作业状态' }}</text>
|
||||
<view class="task-head"><text class="status-badge">{{ stageLabel(current) }}</text></view>
|
||||
<text class="picking-section-label">拣货进度</text>
|
||||
<view class="metrics" aria-label="拣货进度"><view><text class="metric-value">{{ plannedPackages }}</text><text>计划箱数</text></view><view><text class="metric-value success">{{ pickedPackages }}</text><text>真实已拣</text></view><view><text class="metric-value warning">{{ remainingPackages }}</text><text>未拣箱数</text></view></view>
|
||||
</view>
|
||||
@@ -185,7 +183,7 @@ export default {
|
||||
this.sourceChoices = []; this.choiceCartonCode = ''; this.error = null
|
||||
},
|
||||
async submitPick() { if (!this.canPick) return; try { const body = { recordVersion: requireDispatchVersion(this.current.recordVersion), positionId: this.selectedReturnCarton ? null : this.selectedPosition.positionId, packages: this.selectedReturnCarton ? 1 : Number(this.packages), productQuantity: this.selectedReturnCarton?.productQuantity || 0, cartonMark: this.selectedReturnCarton?.cartonMark || null, remark: null }; await this.submitCommand(this.current.dispatchNo, 'PICK', { ...body, dispatchItemId: this.selectedItem.dispatchItemId }) } catch (cause) { this.setError(cause, '拣货未提交') } },
|
||||
async prepareLoading() { if (!this.canComplete) return; await this.run(async () => { const data = await getDispatchLoading(this.current.dispatchNo); if (data.dispatchNo !== this.current.dispatchNo || data.pickingReconciled !== true || data.loadingComplete?.allowed !== true) throw new Error(data.loadingComplete?.blockReason || '装车前置未满足'); requireDispatchVersion(data.recordVersion); this.loadingPreview = data; this.loadingRows = data.pallets }) },
|
||||
async prepareLoading() { if (!this.canComplete || this.busy || this.loading) return; await this.run(async () => { const data = await getDispatchLoading(this.current.dispatchNo); if (data.dispatchNo !== this.current.dispatchNo || data.pickingReconciled !== true || data.loadingComplete?.allowed !== true) throw new Error(data.loadingComplete?.blockReason || '装车前置未满足'); requireDispatchVersion(data.recordVersion); this.loadingPreview = data; this.loadingRows = data.pallets }) },
|
||||
async completeLoading() { if (!this.loadingPreview || this.busy) return; try { if (!this.loadingRows.length || this.loadingRows.some(row => row.deliveryIndex == null || !Number.isInteger(row.deliveryIndex) || row.deliveryIndex < 1)) throw new Error('托盘卸货站尚未匹配,请后台核对'); await this.submitCommand(this.loadingPreview.dispatchNo, 'LOADING_COMPLETE', { recordVersion: requireDispatchVersion(this.loadingPreview.recordVersion), manifestHash: this.loadingPreview.manifestHash, remark: null }) } catch (cause) { this.setError(cause, '装车完成未确认') } },
|
||||
updateReviewReason(event) { if (this.busy || this.loading) return; this.reviewReason = String(event.detail.value || '') },
|
||||
async submitReview(decision) {
|
||||
@@ -227,7 +225,7 @@ export default {
|
||||
},
|
||||
async retry() { if (this.current?.dispatchNo) await this.openTask(this.current.dispatchNo); else await this.loadTasks() },
|
||||
async run(work, loading = false) { if (this.busy || this.loading) return; this.clearScanNotice(); this.error = null; this.successMessage = ''; if (loading) this.loading = true; else this.requestBusy = true; try { await work() } catch (cause) { this.setError(cause, '排车操作失败') } finally { this.loading = false; this.requestBusy = false } },
|
||||
setError(cause, fallback) { this.clearScanNotice(); this.error = toDispatchError(cause, fallback); playErrorSound() },
|
||||
setError(cause, fallback) { this.clearScanNotice(); this.error = toDispatchError(cause, fallback); playErrorSound(); if (typeof uni !== 'undefined') uni.showToast?.({ title: this.error.message || fallback, icon: 'none', duration: 3000 }) },
|
||||
stageLabel(task) { if (task.status === 'CANCELLED') return '已取消'; if (task.stage === 'BEFORE_LOADING') { if (task.actions?.LOADING_REVIEW?.allowed === true) return '待理货审核'; if ([task.reviews?.dispatchReview, task.reviews?.loadingReview].some(review => review?.status === 'REJECTED')) return '已退回客服处理' }; return ({ BEFORE_LOADING: '装车前', LOADING: '装车中', AFTER_LOADING: '装车后', DELIVERING: '派送中', DELIVERED: '派送完成' })[task.stage] || '状态待核实' },
|
||||
reviewTime(value) {
|
||||
if (!value) return '未填写'
|
||||
@@ -288,7 +286,7 @@ export default {
|
||||
.review-actions button { flex: 1; min-width: 0; margin: 0; padding: 0 16rpx; min-height: 88rpx; line-height: 88rpx; font-size: 28rpx; }
|
||||
.review-actions .primary-button { min-height: 88rpx; line-height: 88rpx; font-size: 28rpx; }
|
||||
.review-card textarea { box-sizing: border-box; width: 100%; height: 160rpx; min-height: 160rpx; margin-top: 20rpx; padding: 16rpx; border: 1px solid #cbd5e1; border-radius: 8rpx; }
|
||||
.loading-overlay { position: fixed; inset: 0; background: rgba(15, 23, 42, .46); display: flex; align-items: center; justify-content: center; z-index: 100; padding: 40rpx; box-sizing: border-box; }
|
||||
.loading-overlay { position: fixed; top: 0; right: 0; bottom: 0; left: 0; background: rgba(15, 23, 42, .46); display: flex; align-items: center; justify-content: center; z-index: 100; padding: 40rpx; box-sizing: border-box; }
|
||||
.loading-dialog { width: 100%; max-width: 620rpx; max-height: 88vh; overflow-y: auto; box-sizing: border-box; background: #fff; border-radius: 28rpx; padding: 36rpx 32rpx 32rpx; box-shadow: 0 24rpx 80rpx rgba(15, 23, 42, .18); }
|
||||
.loading-dialog-heading { display: flex; flex-direction: column; gap: 10rpx; }
|
||||
.loading-dialog-title { color: #142c49; font-size: 36rpx; font-weight: 700; line-height: 1.4; }
|
||||
|
||||
@@ -310,7 +310,9 @@
|
||||
@media(max-width:360px){.photo-role{grid-template-columns:68px minmax(0,1fr) 64px;gap:7px;padding:8px}.photo-preview{width:68px;height:64px}.photo-actions button{min-height:34px;padding:0 2px;line-height:34px}.photo-state{font-size:9px;line-height:13px}.domestic-photo-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}
|
||||
.receiving-page{padding-bottom:env(safe-area-inset-bottom,0px)}
|
||||
.receiving-content{box-sizing:border-box;padding-bottom:calc(24px + env(safe-area-inset-bottom,0px))}
|
||||
.receiving-content-with-action{padding-bottom:calc(152px + env(safe-area-inset-bottom,0px))}
|
||||
.receiving-content-with-action{padding-bottom:calc(24px + env(safe-area-inset-bottom,0px))}
|
||||
.sticky-action-dock{position:static;max-width:none;margin:12px 0 0;padding:0;background:transparent}
|
||||
.sticky-action{padding:10px;box-shadow:none}
|
||||
.primary-button,.secondary-button,.ghost-button{display:flex;box-sizing:border-box;align-items:center;justify-content:center;padding-top:0;padding-bottom:0;line-height:normal;text-align:center}
|
||||
.sticky-action button{display:flex;align-items:center;justify-content:center;line-height:normal;text-align:center}
|
||||
.receiving-list-page{background:#f1f5fd}
|
||||
|
||||
@@ -48,12 +48,14 @@ export function receivingProgress(session = receiving.current) {
|
||||
export function setCurrentReceiving(value, owner = '') {
|
||||
const nextOwner = photoOwner(value, owner)
|
||||
const previous = previewOwner && previewOwner === nextOwner ? receiving.photoSlots : []
|
||||
const keepUnsigned = receiving.current?.status === 'WAIT_SIGN' && value?.status === 'WAIT_SIGN'
|
||||
previewOwner = nextOwner
|
||||
receiving.current = value || null
|
||||
receiving.selected = value || null
|
||||
receiving.photoSlots = RECEIVING_PHOTO_ROLES.map((role) => {
|
||||
const photo = (value?.photos || []).find((item) => item.role === role)
|
||||
if (!photo) return null
|
||||
// 相机返回会重新读取 current;签收前已上传的文件尚未绑定,保留同一身份、同一柜会话的草稿。
|
||||
if (!photo) return keepUnsigned ? previous.find(item => item?.role === role && item.fileId) || null : null
|
||||
const old = previous.find(item => item?.role === role && String(item.fileId) === String(photo.fileId)
|
||||
&& item.downloadPath === photo.downloadPath)
|
||||
return old || { ...photo, fileId: String(photo.fileId), role, localPath: '', uploaded: true, status: 'idle' }
|
||||
|
||||
@@ -2,7 +2,7 @@ import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import * as store from '../src/stores/receiving.js'
|
||||
import { normalizeReceivingSession, toReceivingError } from '../src/api/receiving-adapter.js'
|
||||
import { normalizeReceivingSession, toReceivingError, RECEIVING_PHOTO_ROLES } from '../src/api/receiving-adapter.js'
|
||||
|
||||
const source = readFileSync(new URL('../src/pages/operation/operation.vue', import.meta.url), 'utf8').match(/<script>([\s\S]*?)<\/script>/)[1]
|
||||
const defer = () => { let resolve, reject; const promise = new Promise((a, b) => { resolve = a; reject = b }); return { promise, resolve, reject } }
|
||||
@@ -13,7 +13,7 @@ const sessionValue = (patch = {}) => normalizeReceivingSession({ containerNo: 'C
|
||||
|
||||
function page(overrides = {}) {
|
||||
const deps = { ...store, session: { tenantId: '100', currentUser: { id: 'operator', warehouseId: '900' } },
|
||||
PortalHeader: {}, ReceivingSessionSummary: {}, ScanTargetInput: {}, requireSession: () => true,
|
||||
PortalHeader: {}, ReceivingSessionSummary: {}, ScanTargetInput: {}, RECEIVING_PHOTO_ROLES, requireSession: () => true,
|
||||
startScanBroadcast: () => () => {}, playErrorSound() {}, playSuccessSound() {},
|
||||
receivingPhase: store.receivingPhase, toReceivingError: error => error, ...overrides }
|
||||
const definition = new Function(...Object.keys(deps), source.replace(/^\s*import .*$/gm, '').replace('export default', 'return'))(...Object.values(deps))
|
||||
@@ -25,6 +25,35 @@ function page(overrides = {}) {
|
||||
return { vm, definition, deps }
|
||||
}
|
||||
|
||||
test('camera resume retains all unsigned uploads by role and clears them across session or identity changes', async () => {
|
||||
store.resetReceivingSession()
|
||||
const unsigned = sessionValue({ status: 'WAIT_SIGN', photos: [], domesticLoadingPhotos: [] })
|
||||
let nextFile = 10
|
||||
const { vm, deps } = page({ uploadCurrentUnloadPhoto: async () => ({ fileId: String(++nextFile) }) })
|
||||
await vm.applyCurrentReceiving(unsigned)
|
||||
await vm.replacePhoto(0, 'camera:plate')
|
||||
vm.suspendReceiving(); vm._pageActive = true
|
||||
await vm.applyCurrentReceiving(structuredClone(unsigned))
|
||||
await vm.replacePhoto(1, 'camera:seal')
|
||||
vm.suspendReceiving(); vm._pageActive = true
|
||||
await vm.applyCurrentReceiving(structuredClone(unsigned))
|
||||
await vm.replacePhoto(2, 'camera:door')
|
||||
assert.deepEqual(vm.photoSlots.map(photo => photo.fileId), ['11', '12', '13'])
|
||||
assert.equal(vm.photosReady, true)
|
||||
await vm.replacePhoto(1, 'camera:seal-again')
|
||||
assert.deepEqual(vm.photoSlots.map(photo => photo.fileId), ['11', '14', '13'])
|
||||
deps.session.currentUser = { ...deps.session.currentUser, id: 'other-operator' }; vm.user = deps.session.currentUser
|
||||
await vm.applyCurrentReceiving(structuredClone(unsigned))
|
||||
assert.deepEqual(vm.photoSlots, [null, null, null])
|
||||
await vm.replacePhoto(0, 'camera:new-owner')
|
||||
await vm.applyCurrentReceiving({ ...unsigned, preparedAt: '2026-11-05T10:00:00' })
|
||||
assert.deepEqual(vm.photoSlots, [null, null, null])
|
||||
await vm.replacePhoto(0, 'camera:new-session')
|
||||
await vm.applyCurrentReceiving({ ...unsigned, preparedAt: '2026-11-05T10:00:00', status: 'SIGNED' })
|
||||
assert.deepEqual(vm.photoSlots, [null, null, null])
|
||||
vm.suspendReceiving()
|
||||
})
|
||||
|
||||
test('same current readback retains three signed previews; collapsed unversioned domestic images issue no downloads', async () => {
|
||||
store.resetReceivingSession(); const calls = []
|
||||
const download = async path => { calls.push(path); return `local:${path}` }
|
||||
|
||||
@@ -90,10 +90,11 @@ test('domestic cabinet photos are read-only, previewable and non-blocking in sig
|
||||
assert.doesNotMatch(page, /domesticLoadingPhotos[\s\S]{0,120}customer/)
|
||||
})
|
||||
|
||||
test('fixed receiving action leaves enough scroll-safe space above the PDA bottom bar', () => {
|
||||
test('receiving finish action remains in document flow after the scan and save controls', () => {
|
||||
assert.match(page, /v-if="canFinishOnsite" class="sticky-action-dock"><view class="sticky-action one"/)
|
||||
assert.match(page, /allInboundsReconciled \? '完成整柜入库' : '现场收货结束(标记差异)'/)
|
||||
assert.match(page, /receiving-content-with-action\{[^}]*padding-bottom:calc\(152px \+ env\(safe-area-inset-bottom,0px\)\)/)
|
||||
assert.match(page, /sticky-action-dock\{position:static;max-width:none/)
|
||||
assert.match(page, /receiving-content-with-action\{[^}]*padding-bottom:calc\(24px \+ env\(safe-area-inset-bottom,0px\)\)/)
|
||||
assert.match(page, /primary-button,\.secondary-button,\.ghost-button\{[^}]*align-items:center;justify-content:center[^}]*line-height:normal;text-align:center/)
|
||||
assert.match(page, /sticky-action button\{[^}]*align-items:center;justify-content:center;line-height:normal;text-align:center/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user