diff --git a/AGENTS.md b/AGENTS.md
index d6479fd..87d7728 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -27,7 +27,7 @@
- 工作台定义:`src/data/overseas-workbench.js`。
- 登录与会话:`src/api/auth.js`、`src/stores/session.js`。
- 扫码设备层:`src/utils/scan-broadcast.js`;广播 Action=`android.intent.action.SCANRESULT`,Extra=`value`。
-- 当前通用作业页中的入仓作业已接柜号级真实海外 API:账号锁柜、三类照片签收、FBA 按 SKU 与箱数收货、OVERSEAS 逐箱收货并逐入仓码核平;其他库内、换标与查询模块仍是静态原型,不得将原型提示改成模拟远程成功。
+- 当前通用作业页中的入仓作业已接柜号级真实海外 API:账号锁柜、三类照片签收、FBA 按 SKU 与箱数收货、OVERSEAS 逐箱收货并逐入仓码核平;库内、换标与查询入口在海外 PDA 专用 Controller、权限和 Token 冻结仓库范围契约齐备前必须禁用,不得调用平台/客户/内部接口或模拟成功。
## 页面与能力快速索引
@@ -45,7 +45,7 @@
- `src/api/inbound.js` 已接入待卸柜候选、柜详情/prepare、current 恢复、三类照片签收、入仓码下 SKU/箱唛解析、实物托盘解析、FBA bulk/OVERSEAS carton 收货及整柜完成;响应与 404/409 判定集中在 `src/api/receiving-adapter.js`,双 Token 刷新与海外 401 单次重放由 `src/api/overseas.js` 统一处理。
- 请求身份和资源范围只取 Token 与 current 会话;前端不得提交 `customerId`、`planNo`、`allocationId`、`warehouseId` 或 `locationId`。409 必须引导账号恢复当前柜,完成前不得切柜。
-- 库内、换标与现场查询仍是静态原型;新增真实作业必须建立明确的业务 API/DTO、错误态和契约测试,不得把原型任务改成模拟成功。
+- 库内、换标与现场查询只有视觉原型依据,当前工作台入口保持禁用;新增真实作业必须建立 `/api/pda/**` 业务 API/DTO、`lastleg:pda:operate` 权限、Token 冻结仓库范围、错误态和契约测试,不得把平台、客户或内部接口直接暴露给 PDA。
- 已有海外入仓 PDA Controller 统一位于 Lastleg `/api/pda/v1/last-leg/inbound-plans/{planNo}/**`;浏览器或设备请求仍必须经 `gateway-overseas` 的显式 PDA 路由,不得直连 Lastleg,也不得改走国内 Gateway。
- 海外业务 Token、PDA 权限和可信身份最终由 Gateway 与服务端校验;前端不得用工作台 `available`、本地客户 ID 或仓库 ID 代替授权和资源归属校验。
- 四期仍止于入仓、库存与库内操作。后端历史 `dispatch/`、出库和派送代码不构成海外 PDA 页面入口,也不得加入工作台。
@@ -53,6 +53,6 @@
## 质量门禁
- 安装:`npm install`;检查:`npm test`。
-- H5 生产编译:`npm run build:h5`;App 资源编译:`npm run build:app`。
+- 正式产品形态是海外 PDA App;`npm run build:h5` 仅用于浏览器调试和模板编译门禁,App 资源编译使用 `npm run build:app`。
- APK/IPA 必须在 HBuilderX 或正式原生打包环境中使用独立 AppID 与签名验证;仅通过 App 资源编译不得声称 APK 真机验收完成。
- 不提交 `node_modules/`、`dist/`、`unpackage/`、环境文件、签名证书或打包产物。
diff --git a/README.md b/README.md
index a749c68..3be9296 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# 柏悦物流海外版 PDA
-独立的 uni-app Vue 3 工程,面向海外仓现场人员。工程不依赖国内 PDA 源码或仓库外共享目录,可独立安装依赖、运行、编译和发布。
+独立的 uni-app Vue 3 App 工程,正式产品形态是安装在海外仓手持 PDA 上的 App。工程不依赖国内 PDA 源码或仓库外共享目录,可独立安装依赖、运行、编译和发布;H5 只用于浏览器调试和编译门禁,不是最终交付形态。
首版工作台按四期边界提供柜号级真实入仓作业,以及库内作业、换标作业和现场查询原型。登录使用国内 Auth 统一签发的双 Token;海外入仓请求统一经 `gateway-overseas`,不得请求国内 Firstleg/Warehouse 业务接口。
@@ -21,8 +21,8 @@ npm run build:h5
npm run build:app
```
-- `build:h5` 生成 H5 发布文件;
-- `build:app` 生成 App 离线打包资源;
+- `build:h5` 生成浏览器调试资源,用于页面和模板编译检查;
+- `build:app` 生成正式 App 的离线打包资源;
- 生成可安装 APK/IPA 需使用 HBuilderX App 开发版执行云打包或配置原生离线打包环境。
## 正式 App 打包前必填
@@ -40,5 +40,5 @@ npm run build:app
- 入仓页面已接待卸柜候选、柜详情与账号锁柜、三类角色照片签收、current 恢复、SKU/箱唛与既有物理托盘解析、FBA 按 SKU 和箱数收货、OVERSEAS 逐箱收货、逐入仓码核平及整柜完成的真实 API;409 会引导返回当前柜,错误会保留业务码、requestId 与 traceId。
- 入仓请求不手填客户、计划、分配、仓库或库位标识;身份与当前柜范围由 Token、Gateway 和服务端会话确定。页面同时支持 PDA 广播扫码与 H5 人工输入。
- 登录、恢复和刷新会 fail-closed 校验国内/海外双 Token 的同一平台操作主体、`WAREHOUSE_PICKER`、`lastleg:pda:operate` 及同一冻结海外仓;并发海外 401 共享一次国内 refresh,校验完成后原子替换双 Token并将原请求或照片上传重放一次。
-- 库内、换标和现场查询仍是可运行的静态原型,不会模拟海外服务提交成功。
+- 库存盘点、商品移库、托盘管理、换标收货/执行/复核以及库存/库位/托盘查询当前缺少海外 PDA 专用 Controller、权限和 Token 冻结仓库范围契约,入口已禁用;不会调用平台、客户或内部接口,也不会模拟成功。
- 本地契约测试与 H5/App 资源编译不等于真实开发环境 HTTP 或 PDA 真机验收。
diff --git a/src/data/overseas-workbench.js b/src/data/overseas-workbench.js
index d488797..5163a81 100644
--- a/src/data/overseas-workbench.js
+++ b/src/data/overseas-workbench.js
@@ -3,19 +3,19 @@ export const MODULES = [
{ name: '拆柜收货', icon: '卸', iconType: 'download', description: '三类照片签收,FBA 按 SKU 箱数、海外仓逐箱收货', available: true, badge: '真实接口' }
] },
{ key: 'inventory', name: '库内作业', icon: '库', description: '盘点、移库与托盘管理', operations: [
- { name: '库存盘点', icon: '盘', iconType: 'list', description: '按任务扫描库位和库存', available: true, badge: '原型' },
- { name: '商品移库', icon: '移', iconType: 'loop', description: '扫描来源与目标库位完成移库', available: true, badge: '原型' },
- { name: '托盘管理', icon: '托', iconType: 'grid', description: '组托、拆托及托盘状态核对', available: true, badge: '原型' }
+ { name: '库存盘点', icon: '盘', iconType: 'list', description: '按任务扫描库位和库存', available: false, badge: '待 PDA 接口' },
+ { name: '商品移库', icon: '移', iconType: 'loop', description: '扫描来源与目标库位完成移库', available: false, badge: '待 PDA 接口' },
+ { name: '托盘管理', icon: '托', iconType: 'grid', description: '组托、拆托及托盘状态核对', available: false, badge: '待 PDA 接口' }
] },
{ key: 'relabel', name: '换标作业', icon: '换', description: '换标收货、执行与复核', operations: [
- { name: '换标收货', icon: '收', iconType: 'download', description: '扫描换标单并确认待处理库存', available: true, badge: '原型' },
- { name: '换标执行', icon: '换', iconType: 'compose', description: '扫描旧标和新标记录换标结果', available: true, badge: '原型' },
- { name: '换标复核', icon: '核', iconType: 'checkbox', description: '复核标签和商品关联关系', available: true, badge: '原型' }
+ { name: '换标收货', icon: '收', iconType: 'download', description: '扫描换标单并确认待处理库存', available: false, badge: '待 PDA 接口' },
+ { name: '换标执行', icon: '换', iconType: 'compose', description: '扫描旧标和新标记录换标结果', available: false, badge: '待 PDA 接口' },
+ { name: '换标复核', icon: '核', iconType: 'checkbox', description: '复核标签和商品关联关系', available: false, badge: '待 PDA 接口' }
] },
{ key: 'query', name: '现场查询', icon: '查', description: '库存、库位与托盘查询', operations: [
- { name: '库存查询', icon: '存', iconType: 'search', description: '扫描商品或箱码查询可用库存', available: true, badge: '原型' },
- { name: '库位查询', icon: '位', iconType: 'location', description: '扫描库位查看容量和在库明细', available: true, badge: '原型' },
- { name: '托盘查询', icon: '托', iconType: 'flag', description: '扫描托盘码查看绑定库存', available: true, badge: '原型' }
+ { name: '库存查询', icon: '存', iconType: 'search', description: '扫描商品或箱码查询可用库存', available: false, badge: '待 PDA 接口' },
+ { name: '库位查询', icon: '位', iconType: 'location', description: '扫描库位查看容量和在库明细', available: false, badge: '待 PDA 接口' },
+ { name: '托盘查询', icon: '托', iconType: 'flag', description: '扫描托盘码查看绑定库存', available: false, badge: '待 PDA 接口' }
] }
]
diff --git a/src/manifest.json b/src/manifest.json
index 3487754..298aa81 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -1,49 +1,74 @@
{
- "name": "柏悦物流海外版",
- "appid": "",
- "description": "柏悦物流海外仓 PDA 作业端",
- "versionName": "0.1.0",
- "versionCode": "10",
- "transformPx": false,
- "app-plus": {
- "usingComponents": true,
- "nvueCompiler": "uni-app",
- "nvueStyleCompiler": "uni-app",
- "compilerVersion": 3,
- "splashscreen": {
- "alwaysShowBeforeRender": true,
- "waiting": true,
- "autoclose": true,
- "delay": 0
- },
- "modules": {},
- "distribute": {
- "android": {
- "packagename": "com.baiyuelogistics.pda.overseas",
- "permissions": [],
- "abiFilters": ["armeabi-v7a", "arm64-v8a"]
- },
- "ios": {},
- "sdkConfigs": {},
- "icons": {
- "android": {
- "hdpi": "static/app-icons/72x72.png",
- "xhdpi": "static/app-icons/96x96.png",
- "xxhdpi": "static/app-icons/144x144.png",
- "xxxhdpi": "static/app-icons/192x192.png"
+ "name" : "柏悦物流 PDA 海外版",
+ "appid" : "__UNI__936D079",
+ "description" : "柏悦物流海外仓 PDA 作业端",
+ "versionName" : "0.0.1",
+ "versionCode" : 1,
+ "transformPx" : false,
+ "app-plus" : {
+ "usingComponents" : true,
+ "nvueCompiler" : "uni-app",
+ "nvueStyleCompiler" : "uni-app",
+ "compilerVersion" : 3,
+ "splashscreen" : {
+ "alwaysShowBeforeRender" : true,
+ "waiting" : true,
+ "autoclose" : true,
+ "delay" : 0
},
- "ios": {
- "appstore": "static/app-icons/1024x1024.png"
+ "modules" : {},
+ "distribute" : {
+ "android" : {
+ "packagename" : "com.baiyuelogistics.pda.overseas",
+ "permissions" : [],
+ "abiFilters" : [ "armeabi-v7a", "arm64-v8a" ]
+ },
+ "ios" : {},
+ "sdkConfigs" : {},
+ "icons" : {
+ "android" : {
+ "hdpi" : "static/app-icons/72x72.png",
+ "xhdpi" : "static/app-icons/96x96.png",
+ "xxhdpi" : "static/app-icons/144x144.png",
+ "xxxhdpi" : "static/app-icons/192x192.png"
+ },
+ "ios" : {
+ "appstore" : "static/app-icons/1024x1024.png",
+ "ipad" : {
+ "app" : "static/app-icons/76x76.png",
+ "app@2x" : "static/app-icons/152x152.png",
+ "notification" : "static/app-icons/20x20.png",
+ "notification@2x" : "static/app-icons/40x40.png",
+ "proapp@2x" : "static/app-icons/167x167.png",
+ "settings" : "static/app-icons/29x29.png",
+ "settings@2x" : "static/app-icons/58x58.png",
+ "spotlight" : "static/app-icons/40x40.png",
+ "spotlight@2x" : "static/app-icons/80x80.png"
+ },
+ "iphone" : {
+ "app@2x" : "static/app-icons/120x120.png",
+ "app@3x" : "static/app-icons/180x180.png",
+ "notification@2x" : "static/app-icons/40x40.png",
+ "notification@3x" : "static/app-icons/60x60.png",
+ "settings@2x" : "static/app-icons/58x58.png",
+ "settings@3x" : "static/app-icons/87x87.png",
+ "spotlight@2x" : "static/app-icons/80x80.png",
+ "spotlight@3x" : "static/app-icons/120x120.png"
+ }
+ }
+ }
}
- }
- }
- },
- "quickapp": {},
- "mp-weixin": {
- "appid": "",
- "setting": { "urlCheck": false },
- "usingComponents": true
- },
- "uniStatistics": { "enable": false },
- "vueVersion": "3"
+ },
+ "quickapp" : {},
+ "mp-weixin" : {
+ "appid" : "",
+ "setting" : {
+ "urlCheck" : false
+ },
+ "usingComponents" : true
+ },
+ "uniStatistics" : {
+ "enable" : false
+ },
+ "vueVersion" : "3"
}
diff --git a/src/pages/home/home.vue b/src/pages/home/home.vue
index e6c5d52..a4bddf1 100644
--- a/src/pages/home/home.vue
+++ b/src/pages/home/home.vue
@@ -9,7 +9,7 @@
{{ operation.name }}
{{ operation.badge }}
- 未开放
+ {{ operation.badge || '未开放' }}
diff --git a/src/pages/module/module.vue b/src/pages/module/module.vue
index 6c9e967..d7034d7 100644
--- a/src/pages/module/module.vue
+++ b/src/pages/module/module.vue
@@ -4,8 +4,8 @@
{{ currentModule.icon }}{{ currentModule.name }}{{ currentModule.description }}
业务功能
-
- 拆柜收货已经海外网关接入真实接口;标记“原型”的库内与换标功能不会模拟提交成功。
+
+ 只有拆柜收货已接海外 PDA 真实接口;其余入口在专用接口、权限与仓库范围契约完成前保持禁用。
diff --git a/src/pages/operation/operation.vue b/src/pages/operation/operation.vue
index 856fd82..8377202 100644
--- a/src/pages/operation/operation.vue
+++ b/src/pages/operation/operation.vue
@@ -102,8 +102,8 @@
- {{ operationName }}{{ prototype.subtitle }}静态原型
- 该库内模块尚未接入真实业务接口,不会模拟提交成功。
+ {{ operationName }}当前功能尚无海外 PDA 专用业务契约待 PDA 接口
+ 功能暂未开放现有平台、客户或内部接口不能由仓库作业员直接调用。扫码不会提交,也不会产生模拟成功结果。
{{ toast.message }}
@@ -114,7 +114,6 @@
import PortalHeader from '@/components/PortalHeader.vue'
import ScanTargetInput from '@/components/ScanTargetInput.vue'
import { isNoCurrentReceiving, isReceivingConflict, RECEIVING_PHOTO_ROLES } from '@/api/receiving-adapter.js'
- import { getOperationPrototype } from '@/data/overseas-workbench.js'
import { session, requireSession } from '@/stores/session.js'
import { idempotencyKey, receiving, receivingPhase, receivingProgress, releaseIdempotencyKey, resetReceivingDraft, selectedInbound, setCurrentReceiving } from '@/stores/receiving.js'
import { startScanBroadcast } from '@/utils/scan-broadcast.js'
@@ -134,9 +133,9 @@
export default {
components: { PortalHeader, ScanTargetInput, SessionSummary, InboundList },
- data() { return { user: session.currentUser, moduleKey: 'query', operationName: '库存查询', loading: false, busy: false, detailOpen: false, containerInput: '', cartonInput: '', skuInput: '', palletInput: '', packageCount: 1, prototypeInput: '', error: null, toast: { visible: false, message: '', type: '' }, photoSteps: RECEIVING_PHOTO_ROLES.map((role) => ({ role, label: ({ LICENSE_PLATE: '车牌照片', CONTAINER_SEAL: '集装箱封签', DOOR_OPEN: '开柜现场' })[role] })) } },
+ data() { return { user: session.currentUser, moduleKey: 'query', operationName: '库存查询', loading: false, busy: false, detailOpen: false, containerInput: '', cartonInput: '', skuInput: '', palletInput: '', packageCount: 1, error: null, toast: { visible: false, message: '', type: '' }, photoSteps: RECEIVING_PHOTO_ROLES.map((role) => ({ role, label: ({ LICENSE_PLATE: '车牌照片', CONTAINER_SEAL: '集装箱封签', DOOR_OPEN: '开柜现场' })[role] })) } },
computed: {
- prototype() { return getOperationPrototype(this.operationName) }, inboundMode() { return this.moduleKey === 'inbound' },
+ inboundMode() { return this.moduleKey === 'inbound' },
candidates() { return receiving.candidates }, selected() { return receiving.selected }, current() { return receiving.current }, photoSlots() { return receiving.photoSlots },
selectedInboundCode: { get() { return receiving.selectedInboundCode }, set(value) { receiving.selectedInboundCode = value } },
resolvedCarton() { return receiving.resolvedCarton }, resolvedSku() { return receiving.resolvedSku }, resolvedPallet() { return receiving.resolvedPallet }, lastReceipt() { return receiving.lastReceipt },
@@ -156,7 +155,7 @@
statusTone(status) { return status === 'RECEIVING' ? 'status-current' : status === 'COMPLETED' ? 'status-good' : 'status-wait' }, percent(received, total) { return total ? Math.min(100, Math.round(Number(received || 0) / Number(total) * 100)) : 0 },
destinationLabel(item) { return item?.destinationType === 'FBA' ? (item.fbaWarehouseCode || 'FBA') : (item?.destinationWarehouseName || item?.destinationWarehouseCode || '海外仓') },
startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => this.handleHardwareScan(value), onError: (error) => { this.error = error } }) }, stopScanner() { if (typeof this._stopScanner === 'function') this._stopScanner(); this._stopScanner = null },
- handleHardwareScan(value) { if (!this.inboundMode) { this.prototypeInput = value; return this.prototypeScan() } if (this.phase === 'candidates') { this.containerInput = value; return this.openContainer(value) } if (this.phase !== 'receive') return; if (this.activeTarget === 'carton' && this.activeInbound?.destinationType === 'OVERSEAS') { this.cartonInput = value; return this.resolveCarton(value) } if (this.activeTarget === 'sku' && this.activeInbound?.destinationType === 'FBA') { this.skuInput = value; return this.resolveSku(value) } this.palletInput = value; return this.resolvePallet(value) },
+ handleHardwareScan(value) { if (!this.inboundMode) return this.showToast('该功能缺少海外 PDA 专用接口,扫码未提交', 'warning'); if (this.phase === 'candidates') { this.containerInput = value; return this.openContainer(value) } if (this.phase !== 'receive') return; if (this.activeTarget === 'carton' && this.activeInbound?.destinationType === 'OVERSEAS') { this.cartonInput = value; return this.resolveCarton(value) } if (this.activeTarget === 'sku' && this.activeInbound?.destinationType === 'FBA') { this.skuInput = value; return this.resolveSku(value) } this.palletInput = value; return this.resolvePallet(value) },
async run(action, work) { this.busy = true; this.error = null; try { const result = await work(); this.showToast(`${action}成功`, 'success'); return result } catch (error) { this.error = error; playErrorSound(); this.showToast(error.message || `${action}失败`, 'error'); throw error } finally { this.busy = false } },
setScanTargetForCurrent(value) { receiving.activeScanTarget = value?.status === 'WAIT_SIGN' ? 'container' : (selectedInbound(value)?.destinationType === 'OVERSEAS' ? 'carton' : 'sku') },
async bootstrap() { this.loading = true; this.error = null; this.detailOpen = false; try { const current = await getCurrentReceiving(); setCurrentReceiving(current); this.setScanTargetForCurrent(current) } catch (error) { if (isNoCurrentReceiving(error)) { resetReceivingDraft(); await this.loadCandidates(false) } else this.error = error } finally { this.loading = false } },
@@ -174,7 +173,6 @@
async submitReceipt() { if (!this.canSubmitReceipt) return; const isFba = this.activeInbound.destinationType === 'FBA'; const payload = isFba ? { inboundCode: this.activeInbound.inboundCode, skuCode: this.resolvedSku.skuCode, palletCode: this.resolvedPallet.palletCode, packageCount: Number(this.packageCount) } : { cartonMark: this.resolvedCarton.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()); receiving.resolvedCarton = null; receiving.resolvedSku = null; receiving.resolvedPallet = null; this.cartonInput = ''; this.skuInput = ''; this.palletInput = ''; const inbound = selectedInbound(); this.packageCount = Math.max(1, Number(inbound?.pendingPackages || 1)); receiving.activeScanTarget = inbound?.destinationType === 'OVERSEAS' ? 'carton' : 'sku'; 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 (_) {} },
async finishAndReturn() { resetReceivingDraft(); await this.loadCandidates(false) },
- prototypeScan() { if (!String(this.prototypeInput || '').trim()) return this.showToast('请先扫描或输入编码', 'warning'); this.showToast('该模块尚未接入真实业务接口', 'warning') },
showToast(message, type) { clearTimeout(this._toastTimer); this.toast = { visible: true, message, type }; this._toastTimer = setTimeout(() => { this.toast.visible = false }, 2600) }
}
}
@@ -182,5 +180,6 @@
diff --git a/src/stores/receiving.js b/src/stores/receiving.js
index b70aa92..ef8cd42 100644
--- a/src/stores/receiving.js
+++ b/src/stores/receiving.js
@@ -1,5 +1,5 @@
import { reactive } from 'vue'
-import { RECEIVING_PHOTO_ROLES } from '@/api/receiving-adapter.js'
+import { RECEIVING_PHOTO_ROLES } from '../api/receiving-adapter.js'
export const receiving = reactive({
candidates: [], selected: null, current: null, photoSlots: [null, null, null],
@@ -53,6 +53,11 @@ export function resetReceivingDraft({ keepCurrent = false } = {}) {
idempotencyKeys.clear()
}
+export function resetReceivingSession() {
+ receiving.candidates = []
+ resetReceivingDraft()
+}
+
export function idempotencyKey(action, payload = {}) {
const signature = `${action}:${JSON.stringify(payload)}`
if (!idempotencyKeys.has(signature)) idempotencyKeys.set(signature, `pda-${Date.now()}-${Math.random().toString(16).slice(2)}`)
diff --git a/src/stores/session.js b/src/stores/session.js
index e1238a4..5a4ae74 100644
--- a/src/stores/session.js
+++ b/src/stores/session.js
@@ -1,5 +1,6 @@
import { reactive } from 'vue'
import { OVERSEAS_API_ROOT } from '../config/runtime.js'
+import { resetReceivingSession } from './receiving.js'
const SESSION_KEY = 'overseas-pda-auth-session'
export const PDA_ROLE = 'WAREHOUSE_PICKER'
@@ -158,6 +159,7 @@ export function signOut() {
session.tenantId = ''
session.apiBase = OVERSEAS_API_ROOT
if (typeof uni !== 'undefined' && typeof uni.removeStorageSync === 'function') uni.removeStorageSync(SESSION_KEY)
+ resetReceivingSession()
}
export function requireSession() {
diff --git a/tests/project-contract.test.mjs b/tests/project-contract.test.mjs
index d7b3352..2f59026 100644
--- a/tests/project-contract.test.mjs
+++ b/tests/project-contract.test.mjs
@@ -8,7 +8,7 @@ test('overseas PDA is an independent Vue 3 app with separate packaging identity'
const [manifest, pages, pkg] = await Promise.all([
read('../src/manifest.json'), read('../src/pages.json'), read('../package.json')
])
- assert.match(manifest, /"name"\s*:\s*"柏悦物流海外版"/)
+ assert.match(manifest, /"name"\s*:\s*"柏悦物流\s*PDA\s*海外版"/)
assert.match(manifest, /"packagename"\s*:\s*"com\.baiyuelogistics\.pda\.overseas"/)
assert.match(manifest, /"vueVersion"\s*:\s*"3"/)
assert.doesNotMatch(pages, /pages\/picking|pages\/tasks/)
@@ -41,3 +41,13 @@ test('login has no committed demo credentials and inbound operation implements t
assert.match(overseas, /replaceTokens|refreshOverseasToken/)
assert.match(overseas, /requestId/)
})
+
+test('unimplemented warehouse operations cannot submit prototype scans', async () => {
+ const [operation, home, module] = await Promise.all([
+ read('../src/pages/operation/operation.vue'), read('../src/pages/home/home.vue'), read('../src/pages/module/module.vue')
+ ])
+ assert.doesNotMatch(operation, /prototypeScan|prototypeInput/)
+ assert.match(operation, /功能暂未开放|扫码不会提交/)
+ assert.match(home, /operation\.badge/)
+ assert.match(module, /专用接口、权限与仓库范围契约完成前保持禁用/)
+})
diff --git a/tests/session-auth.test.mjs b/tests/session-auth.test.mjs
index 29f6ff9..db5885b 100644
--- a/tests/session-auth.test.mjs
+++ b/tests/session-auth.test.mjs
@@ -5,6 +5,7 @@ import {
PDA_AUTHORITY, PDA_ROLE, replaceTokens, restoreSession, session,
decodeAccessToken, signInWithTokens, signOut, validatePdaTokenPair
} from '../src/stores/session.js'
+import { receiving } from '../src/stores/receiving.js'
const future = () => Math.floor(Date.now() / 1000) + 3600
const jwt = (claims) => [
@@ -114,3 +115,33 @@ test('restore rebuilds identity from claims and removes expired or tampered stor
assert.equal(restoreSession(), false)
assert.equal(values.has('overseas-pda-auth-session'), false)
})
+
+test('sign out clears tokens, frozen warehouse identity, and every receiving draft', () => {
+ const values = storage(); signOut()
+ signInWithTokens(tokenPair())
+ receiving.candidates = [{ containerNo: 'CONT-1' }]
+ receiving.current = { containerNo: 'CONT-1', status: 'RECEIVING' }
+ receiving.selected = receiving.current
+ receiving.photoSlots = [{ fileId: '1' }, null, null]
+ receiving.selectedInboundCode = 'IN-1'
+ receiving.resolvedCarton = { cartonMark: 'BOX-1' }
+ receiving.resolvedSku = { skuCode: 'SKU-1' }
+ receiving.resolvedPallet = { palletCode: 'PALLET-1' }
+ receiving.lastReceipt = { acceptedPackages: 1 }
+
+ signOut()
+ assert.equal(session.currentUser, null)
+ assert.equal(session.token, '')
+ assert.equal(session.domesticToken, '')
+ assert.equal(session.refreshToken, '')
+ assert.equal(values.has('overseas-pda-auth-session'), false)
+ assert.deepEqual(receiving.candidates, [])
+ assert.equal(receiving.current, null)
+ assert.equal(receiving.selected, null)
+ assert.deepEqual(receiving.photoSlots, [null, null, null])
+ assert.equal(receiving.selectedInboundCode, '')
+ assert.equal(receiving.resolvedCarton, null)
+ assert.equal(receiving.resolvedSku, null)
+ assert.equal(receiving.resolvedPallet, null)
+ assert.equal(receiving.lastReceipt, null)
+})
diff --git a/tests/workbench-availability.test.mjs b/tests/workbench-availability.test.mjs
new file mode 100644
index 0000000..750e800
--- /dev/null
+++ b/tests/workbench-availability.test.mjs
@@ -0,0 +1,17 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { MODULES } from '../src/data/overseas-workbench.js'
+
+test('only operations backed by a dedicated overseas PDA contract are enabled', () => {
+ const operations = MODULES.flatMap((module) => module.operations.map((operation) => ({ module: module.key, ...operation })))
+ const enabled = operations.filter((operation) => operation.available)
+ assert.deepEqual(enabled.map(({ module, name }) => ({ module, name })), [{ module: 'inbound', name: '拆柜收货' }])
+
+ const pendingNames = ['库存盘点', '商品移库', '托盘管理', '换标收货', '换标执行', '换标复核', '库存查询', '库位查询', '托盘查询']
+ for (const name of pendingNames) {
+ const operation = operations.find((item) => item.name === name)
+ assert.equal(operation?.available, false, `${name} must fail closed until its PDA API exists`)
+ assert.equal(operation?.badge, '待 PDA 接口')
+ }
+})
+