优化
This commit is contained in:
@@ -47,7 +47,7 @@ npm run build:app
|
||||
|
||||
## App 在线更新
|
||||
|
||||
Android App 启动后会读取 `VITE_PDA_UPDATE_URL` 并检查海外 PDA 的独立更新通道;H5 不执行该逻辑。更新服务接收 `appid`、`platform`、`appVersion`、`appVersionCode`、`wgtVersion` 查询参数,无更新返回 `{ "update": false }`,有更新返回:
|
||||
Android App 启动后会读取 `VITE_PDA_UPDATE_URL` 并检查海外 PDA 的独立更新通道,员工也可在“个人中心 → 检查更新”手动触发;H5 仅展示 App 操作提示,不执行更新。更新服务接收 `appid`、`platform`、`appVersion`、`appVersionCode`、`wgtVersion` 查询参数,无更新返回 `{ "update": false }`,有更新返回:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<view v-if="user" class="page-shell portal-page receiving-page">
|
||||
<PortalHeader :title="inboundMode ? '拆柜收货' : operationName" :user="user" :fallback-url="`/pages/module/module?key=${moduleKey}`" />
|
||||
<view class="portal-content receiving-content">
|
||||
<view class="portal-content receiving-content" :class="{ 'receiving-content-with-action': phase === 'detail' || phase === 'sign' || (phase === 'receive' && allInboundsReconciled) }">
|
||||
<template v-if="inboundMode">
|
||||
<view v-if="phase !== 'receive'" class="receiving-heading"><view><text class="receiving-title">{{ pageTitle }}</text><text class="receiving-subtitle">{{ pageSubtitle }}</text></view></view>
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
</template>
|
||||
</view>
|
||||
<view v-if="lastReceipt" class="success-card">本次已收 {{ lastReceipt.acceptedPackages }} 箱,{{ lastReceipt.inboundCode }} 剩余 {{ lastReceipt.pendingPackages }} 箱</view>
|
||||
<view class="sticky-action one"><button class="primary-button complete-button" :disabled="busy || !allInboundsReconciled" @tap="completeContainer">{{ allInboundsReconciled ? '每个入仓码已核平,完成整柜入库' : `尚有 ${progress.pending} 箱未收` }}</button></view>
|
||||
<view v-if="allInboundsReconciled" class="sticky-action one"><button class="primary-button complete-button" :disabled="busy" @tap="completeContainer">完成整柜入库</button></view>
|
||||
</template>
|
||||
|
||||
<template v-else-if="phase === 'completed'">
|
||||
@@ -150,7 +150,7 @@
|
||||
previewPhoto(index) { const path = this.photoSlots[index]?.localPath; if (!path) return; uni.previewImage({ urls: [path], current: path }) },
|
||||
async signContainer() { if (!this.photosReady) return; const photos = this.photoSlots.map((item, index) => ({ role: this.photoSteps[index].role, fileId: String(item.fileId) })); const key = idempotencyKey('sign', { containerNo: this.current.containerNo, photos }); try { const value = await this.run('签收当前柜', () => signCurrentReceiving({ photos, idempotencyKey: key.value })); releaseIdempotencyKey(key.signature); setCurrentReceiving(value); this.clearReceiptContext() } catch (_) {} },
|
||||
clearReceiptContext() { receiving.resolvedCarton = null; receiving.resolvedPallet = null; this.cartonInput = ''; this.palletInput = ''; this.packageCount = 1; receiving.activeScanTarget = 'carton' },
|
||||
async resolveCarton(raw) { const cartonMark = String(raw?.detail?.value ?? raw ?? this.cartonInput).trim(); if (!cartonMark) return; if (this.busy) { console.warn('[Overseas PDA carton] ignored while busy', { cartonMark }); return this.showToast('正在处理上一笔扫描,请稍后重试', 'warning') } console.info('[Overseas PDA carton] resolving', { cartonMark, containerNo: this.current?.containerNo }); try { const value = await this.run('解析箱码', () => resolveCurrentCarton(cartonMark)); console.info('[Overseas PDA carton] resolved', { cartonMark: value.cartonMark, inboundCode: value.inboundCode, destinationType: value.destinationType, pendingPackages: value.pendingPackages }); receiving.resolvedCarton = value; receiving.resolvedPallet = null; this.cartonInput = value.cartonMark; this.palletInput = ''; this.packageCount = 1; receiving.activeScanTarget = 'pallet'; playSuccessSound() } catch (error) { console.error('[Overseas PDA carton] failed', { cartonMark, message: error.message, code: error.code, requestId: error.requestId, traceId: error.traceId }) } },
|
||||
async resolveCarton(raw) { const cartonMark = String(raw?.detail?.value ?? raw ?? this.cartonInput).trim(); if (!cartonMark) return; if (this.busy) { console.warn('[Overseas PDA carton] ignored while busy', { cartonMark }); return this.showToast('正在处理上一笔扫描,请稍后重试', 'warning') } console.info('[Overseas PDA carton] resolving', { cartonMark, containerNo: this.current?.containerNo }); try { const value = await this.run('解析箱码', () => resolveCurrentCarton(cartonMark)); console.info('[Overseas PDA carton] resolved', { cartonMark: value.cartonMark, inboundCode: value.inboundCode, destinationType: value.destinationType, pendingPackages: value.pendingPackages }); receiving.resolvedCarton = value; receiving.resolvedPallet = null; this.cartonInput = value.cartonMark; this.palletInput = ''; this.packageCount = 1; playSuccessSound() } catch (error) { console.error('[Overseas PDA carton] failed', { cartonMark, message: error.message, code: error.code, requestId: error.requestId, traceId: error.traceId }) } },
|
||||
async resolvePallet(raw) { const palletCode = String(raw?.detail?.value ?? raw ?? this.palletInput).trim(); if (!palletCode || !this.activeInbound || this.busy) return; try { const value = await this.run('校验托盘', () => resolveCurrentPallet(palletCode, this.activeInbound.inboundCode)); receiving.resolvedPallet = value; this.palletInput = value.palletCode; if (!value.compatible) throw Object.assign(new Error('托盘不匹配'), { code: 'PALLET_INCOMPATIBLE' }); playSuccessSound() } catch (error) { if (!this.error) this.error = error } },
|
||||
async submitReceipt() { if (!this.canSubmitReceipt) return; const isFba = this.activeInbound.destinationType === 'FBA'; const payload = isFba ? { inboundCode: this.activeInbound.inboundCode, palletCode: this.resolvedPallet.palletCode, packageCount: Number(this.packageCount) } : { cartonMark: this.activeInbound.cartonMark, palletCode: this.resolvedPallet.palletCode }; const key = idempotencyKey(isFba ? 'bulk-receipt' : 'carton-receipt', payload); try { const result = await this.run('提交实收', () => isFba ? receiveCurrentBulk({ ...payload, idempotencyKey: key.value }) : receiveCurrentCarton({ ...payload, idempotencyKey: key.value })); releaseIdempotencyKey(key.signature); receiving.lastReceipt = result; setCurrentReceiving(await getCurrentReceiving()); this.clearReceiptContext(); playSuccessSound() } catch (_) {} },
|
||||
async completeContainer() { if (!this.allInboundsReconciled) return; const key = idempotencyKey('complete', { containerNo: this.current.containerNo }); try { const value = await this.run('完成入库', () => completeCurrentReceiving({ idempotencyKey: key.value })); releaseIdempotencyKey(key.signature); setCurrentReceiving(value) } catch (_) {} },
|
||||
@@ -185,7 +185,8 @@
|
||||
.matched-inbound-progress text{padding:6px 4px;border-radius:6px;text-align:center;background:#edf3f9}
|
||||
@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}}
|
||||
.receiving-page{padding-bottom:env(safe-area-inset-bottom,0px)}
|
||||
.receiving-content{box-sizing:border-box;padding-bottom:calc(152px + 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))}
|
||||
.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}
|
||||
</style>
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
<view class="profile-list">
|
||||
<view class="profile-row"><text>作业仓库</text><text class="profile-value">{{ warehouseDisplay }}</text></view>
|
||||
<view class="profile-row"><text>当前班次</text><text class="profile-value">以海外仓排班为准</text></view>
|
||||
<view class="profile-row"><text>应用版本</text><text class="profile-value">0.1.0</text></view>
|
||||
<view class="profile-row"><text>应用版本</text><text class="profile-value">{{ appVersion }}</text></view>
|
||||
</view>
|
||||
<button class="profile-action" @tap="checkUpdate">检查更新 <text>›</text></button>
|
||||
<button class="profile-action" @tap="showSettings">系统设置 <text>›</text></button>
|
||||
<button class="logout-action" @tap="openLogout">退出登录</button>
|
||||
</view>
|
||||
@@ -20,18 +21,33 @@
|
||||
import PopupHeader from '@/components/PopupHeader.vue'
|
||||
import { getCurrentWarehouseProfile } from '@/api/inbound.js'
|
||||
import { applyWarehouseProfile, session, requireSession, signOut } from '@/stores/session.js'
|
||||
import { checkForAppUpdate } from '@/utils/app-update.js'
|
||||
export default {
|
||||
components: { PopupHeader, PortalHeader },
|
||||
data() { return { user: session.currentUser, logoutOpen: false, toast: false } },
|
||||
data() { return { user: session.currentUser, appVersion: '0.0.1', logoutOpen: false, toast: false } },
|
||||
computed: {
|
||||
displayName() { const name = String(this.user?.name || ''); return name && !/^[a-z0-9._-]+$/i.test(name) ? name : '仓库作业员' },
|
||||
avatar() { return this.displayName.slice(0, 1).toUpperCase() },
|
||||
warehouseDisplay() { return this.user?.warehouseName ? (this.user.warehouseCode ? `${this.user.warehouseName} · ${this.user.warehouseCode}` : this.user.warehouseName) : (this.user?.warehouse || '正在读取仓库主档') }
|
||||
},
|
||||
onShow() { if (!requireSession()) return; this.user = session.currentUser; this.loadWarehouseProfile() },
|
||||
onShow() { if (!requireSession()) return; this.user = session.currentUser; this.loadAppVersion(); this.loadWarehouseProfile() },
|
||||
beforeUnmount() { clearTimeout(this._toastTimer) },
|
||||
onBackPress() { if (!this.logoutOpen) return false; this.closeLogout(); return true },
|
||||
methods: {
|
||||
loadAppVersion() {
|
||||
// #ifdef APP-PLUS
|
||||
this.appVersion = String(plus.runtime.version || this.appVersion)
|
||||
// #endif
|
||||
},
|
||||
checkUpdate() {
|
||||
// #ifdef APP-PLUS
|
||||
checkForAppUpdate({ manual: true })
|
||||
return
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
uni.showToast({ title: '请在 PDA App 中检查更新', icon: 'none' })
|
||||
// #endif
|
||||
},
|
||||
async loadWarehouseProfile() { try { this.user = applyWarehouseProfile(await getCurrentWarehouseProfile()) } catch (_) { this.user = session.currentUser } },
|
||||
openLogout() { this.logoutOpen = true; this.$refs.logoutPopup.open() }, closeLogout() { this.logoutOpen = false; this.$refs.logoutPopup.close() }, handleLogoutChange(event) { this.logoutOpen = Boolean(event.show) },
|
||||
showSettings() { clearTimeout(this._toastTimer); this.toast = true; this._toastTimer = setTimeout(() => { this.toast = false }, 2200) },
|
||||
|
||||
@@ -30,3 +30,13 @@ test('overseas online update supports mandatory APK and wgt installation flows',
|
||||
assert.match(updater, /packageType === 'wgt'[\s\S]*?plus\.runtime\.restart/)
|
||||
assert.match(updater, /if \(retry\) await runUpdate\(update\)/)
|
||||
})
|
||||
|
||||
test('overseas profile exposes manual App update checking and the runtime version', async () => {
|
||||
const profile = await read('../src/pages/profile/profile.vue')
|
||||
|
||||
assert.match(profile, /应用版本[\s\S]*?\{\{ appVersion \}\}/)
|
||||
assert.match(profile, /@tap="checkUpdate">检查更新/)
|
||||
assert.match(profile, /plus\.runtime\.version/)
|
||||
assert.match(profile, /checkForAppUpdate\(\{ manual: true \}\)/)
|
||||
assert.match(profile, /#ifndef APP-PLUS[\s\S]*?请在 PDA App 中检查更新/)
|
||||
})
|
||||
|
||||
@@ -48,6 +48,8 @@ test('login has no committed demo credentials and inbound operation implements t
|
||||
assert.match(operation, /startScanBroadcast/)
|
||||
for (const action of ['prepareReceivingContainer', 'getCurrentReceiving', 'resolveCurrentCarton', 'resolveCurrentPallet', 'receiveCurrentBulk', 'receiveCurrentCarton', 'completeCurrentReceiving']) assert.match(operation, new RegExp(action))
|
||||
for (const path of ['/candidates', '/prepare', '/unload-photos/files', '/sign', '/cartons/resolve', '/pallets/resolve', '/receipts/bulk', '/receipts/carton', '/complete']) assert.match(inbound, new RegExp(path.replaceAll('/', '\\/')))
|
||||
assert.match(inbound, /current\('\/pallets\/resolve'\)/)
|
||||
assert.doesNotMatch(inbound, /platform\/v1\/warehouse-admin/, 'PDA只能调用current/pallets/resolve,不能访问Admin仓储资源接口')
|
||||
assert.doesNotMatch(operation, /resolveCurrentSku|resolvedSku|skuInput|selectedInboundCode|chooseInbound|class="inbound-tabs"/)
|
||||
assert.doesNotMatch(operation, /InboundList|柜内入仓码/)
|
||||
assert.doesNotMatch(inbound, /skus\/resolve/)
|
||||
@@ -55,6 +57,8 @@ test('login has no committed demo credentials and inbound operation implements t
|
||||
assert.match(operation, /const payload = isFba \? \{ inboundCode: this\.activeInbound\.inboundCode, palletCode: this\.resolvedPallet\.palletCode, packageCount: Number\(this\.packageCount\) \}/)
|
||||
assert.doesNotMatch(operation, /skuCode/)
|
||||
assert.match(operation, /clearReceiptContext\(\)[\s\S]*activeScanTarget = 'carton'/)
|
||||
assert.match(operation, /name="pallet"[\s\S]*@activate="activeTarget = 'pallet'"/)
|
||||
assert.doesNotMatch(operation, /receiving\.activeScanTarget = 'pallet'/, '箱码解析成功后必须由作业员手动选择托盘扫码,不得自动跳转')
|
||||
assert.match(operation, /\[Overseas PDA scan\] dispatch/)
|
||||
assert.match(operation, /\[Overseas PDA carton\] resolved/)
|
||||
assert.match(operation, /\[Overseas PDA carton\] failed[\s\S]*requestId[\s\S]*traceId/)
|
||||
|
||||
@@ -76,7 +76,9 @@ test('local thumbnail, preview, and authenticated session restore are wired to t
|
||||
})
|
||||
|
||||
test('fixed receiving action leaves enough scroll-safe space above the PDA bottom bar', () => {
|
||||
assert.match(page, /receiving-content\{[^}]*padding-bottom:calc\(152px \+ env\(safe-area-inset-bottom,0px\)\)/)
|
||||
assert.match(page, /v-if="allInboundsReconciled" class="sticky-action one"/)
|
||||
assert.doesNotMatch(page, /尚有 \$\{progress\.pending\} 箱未收/)
|
||||
assert.match(page, /receiving-content-with-action\{[^}]*padding-bottom:calc\(152px \+ 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