diff --git a/AGENTS.md b/AGENTS.md index 4434b34..122e731 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ - 并发401仅一次refresh,完整校验Token/快照再原子替换;主体/仓变化清会话重登录,原请求/照片上传最多重放一次。刷新失败清会话;业务/网络错留code、requestId、traceId或network诊断。 - 国内/海外 AppID、Android 包名、签名、版本号、产物、更新通道独立;`src/manifest.json` AppID须正式打包前由海外所属DCloud账号创建,禁伪造/复用国内AppID。 - 环境地址仅配 `.env.local` 等未提交环境文件,不入源码。 -- Android启动仅在 `APP-PLUS` 调 `src/utils/app-update.js`,以独立 `VITE_PDA_UPDATE_URL` 查HTTPS APK/WGT;强更禁跳过,WGT安装即重启。更新服务/标识/签名/版本序列不得与国内 PDA 混用。 +- Android仅在 `APP-PLUS` 调 `src/utils/app-update.js`,用独立 `VITE_PDA_UPDATE_URL` 查HTTPS APK/WGT;强更禁跳过,WGT安装即重启,不得与国内 PDA 混用。发布先装APK再发兼容WGT;原生变化先升APK。步骤及版本见 `README.md`“App 在线更新”。 - 源码根 `src/`;扫码广播 Action=`android.intent.action.SCANRESULT`,Extra=`value`。 - 退货号/SKU候选进入,按本包实收生成退货箱/计费批次;其他作业范围见下表及对应接口边界。 diff --git a/README.md b/README.md index 9b0f087..315f4b9 100644 --- a/README.md +++ b/README.md @@ -72,10 +72,11 @@ HBuilderX 真机调试及标准基座使用当前工程 `src/manifest.json` 中 `packageType` 只允许 `apk` 或 `wgt`,`appid` 必须与当前安装包一致。WGT 清单还须提供 `minAppVersionCode`(最低兼容 APK 整数版本),不满足时客户端拒绝安装并提示升级 APK。`mandatory=true` 时员工不能跳过,并在下载或安装失败后提供重试;wgt 安装完成会立即重启,APK 交由 Android 安装器完成覆盖安装。正式发布要求: -1. 海外 PDA 每次整包发布同时递增 `src/manifest.json` 的 `versionName` 与整数 `versionCode`,使用海外独立 AppID、包名和签名打包 APK。 -2. wgt 发布至少递增 `versionName`,并使用与已安装原生包兼容的 uni-app 编译器生成;新增原生模块、权限或不兼容运行时能力时必须发布 APK,不能只发 wgt。 -3. 更新清单与下载地址使用 HTTPS,先在测试设备验证可选更新、强制更新、断网重试、APK 覆盖安装、wgt 重启及扫码作业回归,再切换正式清单。 -4. 海外和国内更新记录、文件目录及发布权限保持隔离,服务端必须按请求中的 `appid` 拒绝跨应用更新包。 +1. 发布顺序固定为“先安装 APK 基线,再发布后续 WGT 热更新”。首次安装先使用海外独立 AppID、包名和签名打包 APK,在目标 PDA 安装后确认登录、扫码、拍照和“个人中心 → 检查更新”正常,再以该 APK 作为后续热更新基线。 +2. 后续兼容的页面、交互和业务逻辑修改发布 WGT。递增 `src/manifest.json` 的 `versionName`,使其高于设备当前的 `wgtVersion`,沿用 APK 基线的整数 `versionCode`,并使用与基线 APK 兼容的 uni-app 编译器生成。更新记录的 `minAppVersionCode` 填该 WGT 要求的 APK 基线版本码;设备低于基线时先安装新版 APK。 +3. 例如 APK 基线为 `0.0.5(versionCode=5)`,下一次 WGT 可发布 `0.0.6`,`minAppVersionCode=5`。安装基线 APK 后首次检查同版本资源会判定为最新版本;后续 WGT 使用更高的资源版本号。 +4. 新增原生模块、权限或不兼容运行时能力时,先同时递增 `versionName` 与整数 `versionCode` 并发布、安装新版 APK,完成真机验证后再基于新 APK 发布后续 WGT。 +5. 更新清单与下载地址使用 HTTPS。每次发布先在已安装对应 APK 基线的测试设备验证实际升级路径、断网重试、WGT 安装重启及本次受影响作业,再切换正式清单;新 APK 基线同时验证覆盖安装和签名。海外和国内更新记录、文件目录及发布权限保持隔离,服务端必须按请求中的 `appid` 拒绝跨应用更新包。 ## H5 静态托管(TEST 与灰度) diff --git a/src/manifest.json b/src/manifest.json index f15f42a..6ba6975 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -2,8 +2,8 @@ "name" : "柏悦物流 PDA 海外版", "appid" : "__UNI__936D079", "description" : "柏悦物流海外仓 PDA 作业端", - "versionName" : "0.0.4", - "versionCode" : 4, + "versionName" : "0.0.5", + "versionCode" : 5, "transformPx" : false, "app-plus" : { "usingComponents" : true, diff --git a/src/pages/dispatch/dispatch.vue b/src/pages/dispatch/dispatch.vue index b5ab505..f6aa225 100644 --- a/src/pages/dispatch/dispatch.vue +++ b/src/pages/dispatch/dispatch.vue @@ -78,7 +78,7 @@ 拣货箱数本次最多可拣 {{ selectedRemaining }} 箱 - {{ current.actions.LOADING_COMPLETE?.blockReason || '尚未获得装车完成资格' }} + {{ current.actions.LOADING_COMPLETE?.blockReason || '尚未获得装车完成资格' }} {{ successMessage }} @@ -90,13 +90,6 @@ {{ scanNotice.packages }} 箱 3 秒后自动关闭 - - - - 确认装车 - {{ loadingRows.length }} 托已拣托盘{{ loadingRows.reduce((sum, row) => sum + Number(row.pickedPackages || 0), 0) }} 箱已拣箱数 - - @@ -131,7 +124,7 @@ export default { }, onLoad(options) { this.resumeOnEntry = options?.resume === '1' }, onShow() { if (!requireSession()) return; this.user = session.currentUser; restoreFulfillmentDraft(`${this.user?.id || ''}:${this.user?.warehouseId || ''}`); this.startScanner(); if (!this.bootstrapped) { this.bootstrapped = true; if (this.resumeOnEntry && fulfillment.dispatch.currentNo) this.openTask(fulfillment.dispatch.currentNo); else this.loadTasks() } else if (this.current?.dispatchNo) this.openTask(this.current.dispatchNo); else this.loadTasks() }, - onHide() { this.stopScanner(); this.clearScanNotice(); this.generation++ }, beforeUnmount() { this.stopScanner(); this.clearScanNotice(); this.generation++ }, + onHide() { this.stopScanner(); this.clearScanNotice(); this.loadingPreview = null; this.generation++ }, beforeUnmount() { this.stopScanner(); this.clearScanNotice(); this.loadingPreview = null; this.generation++ }, methods: { clearScanNotice() { clearTimeout(this._scanNoticeTimer); this._scanNoticeTimer = null; this.scanNotice = null }, showScanNotice(item) { @@ -148,7 +141,7 @@ export default { closeTask() { if (this.busy) return; this.current = null; this.activeTab = 'picking'; this.reviewReason = ''; this.loadingPreview = null; this.scanCode = ''; this.error = null; this.successMessage = ''; updateFulfillmentDraft('dispatch', { currentNo: '', palletCode: '', dispatchItemId: '', sourceKey: '', cartonCode: '', packages: 1 }); this.loadTasks() }, handleScan(raw) { const code = String(raw?.detail?.value ?? raw ?? this.scanCode).trim() - if (!code || this.busy || this.loading || this.activeTab !== 'picking') return + if (!code || this.busy || this.loading || this.loadingPreview || this.activeTab !== 'picking') return this.clearScanNotice() this.error = null; this.successMessage = ''; this.sourceChoices = []; this.choiceCartonCode = '' updateFulfillmentDraft('dispatch', { palletCode: '', dispatchItemId: '', sourceKey: '', cartonCode: '', packages: 1 }) @@ -183,7 +176,42 @@ 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 || 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 prepareLoading() { + if (!this.canComplete || this.busy || this.loading || this.loadingPreview) return + const no = this.current.dispatchNo, generation = this.generation + await this.run(async () => { + const data = await getDispatchLoading(no) + if (generation !== this.generation || this.current?.dispatchNo !== no) return + if (data.dispatchNo !== no || data.pickingReconciled !== true || data.loadingComplete?.allowed !== true) throw new Error(data.loadingComplete?.blockReason || '装车前置未满足') + requireDispatchVersion(data.recordVersion) + if (!Array.isArray(data.pallets) || !data.pallets.length) throw new Error('装车清单未返回有效托盘,请重新核对') + this.loadingRows = data.pallets + this.loadingPreview = data + }) + const preview = this.loadingPreview + if (!preview) return + const pickedPackages = this.loadingRows.reduce((sum, row) => sum + Number(row.pickedPackages || 0), 0) + uni.showModal({ + title: '确认装车', + content: `已拣托盘 ${this.loadingRows.length} 托\n已拣箱数 ${pickedPackages} 箱`, + confirmText: '确认', + cancelText: '取消', + success: async result => { + if (this.loadingPreview !== preview) return + if (!result.confirm || generation !== this.generation || this.current?.dispatchNo !== no) { + this.loadingPreview = null + return + } + await this.completeLoading() + if (this.loadingPreview === preview) this.loadingPreview = null + }, + fail: cause => { + if (this.loadingPreview !== preview) return + this.loadingPreview = null + this.setError(new Error(cause.errMsg || '无法打开装车确认,请重试'), '无法打开装车确认') + } + }) + }, 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) { @@ -288,20 +316,8 @@ export default { .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; 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; } -.loading-counts { display: flex; margin: 28rpx 0; padding: 24rpx 0; border-radius: 20rpx; background: #f3f7ff; } -.loading-count { flex: 1; display: flex; align-items: center; flex-direction: column; gap: 8rpx; } -.loading-count + .loading-count { border-left: 1px solid #dce6f6; } -.loading-count-value { color: #1764ec; font-size: 48rpx; font-weight: 700; line-height: 1.2; } .loading-count-unit { font-size: 26rpx; font-weight: 500; } -.loading-count-label { color: #72839b; font-size: 24rpx; } -.loading-dialog-actions { display: flex; gap: 20rpx; margin-top: 32rpx; } -.loading-dialog-actions button { flex: 1; min-width: 0; margin: 0; height: 88rpx; line-height: 88rpx; padding: 0 12rpx; border-radius: 16rpx; font-size: 30rpx; font-weight: 600; } -.loading-dialog-actions button::after { border: 0; } -.loading-cancel { color: #52657d; background: #f0f3f8; } -.loading-confirm { color: #fff; background: #1764ec; } -.loading-dialog-actions button[disabled] { opacity: .5; } .scan-notice-overlay { pointer-events: none; } .scan-notice-dialog { text-align: center; } .scan-notice-pallet { display: block; margin-top: 20rpx; color: #243c58; font-size: 30rpx; overflow-wrap: anywhere; } diff --git a/src/pages/login/login.vue b/src/pages/login/login.vue index ce38a78..8813430 100644 --- a/src/pages/login/login.vue +++ b/src/pages/login/login.vue @@ -7,6 +7,7 @@ + {{ error }} @@ -16,12 +17,25 @@