优化
This commit is contained in:
@@ -37,7 +37,7 @@
|
||||
| AppID、平台图标与打包资源 | `src/manifest.json`、`static/app-icons/` | Android/iOS 尺寸映射由 `tests/app-icons.test.mjs` 锁定;独立 AppID、签名和发布通道不得复用国内 PDA |
|
||||
| 登录、刷新与会话 | `src/pages/login/login.vue`、`src/api/auth.js`、`src/stores/session.js` | 国内 Auth 签发 Token;API 根见 `src/config/runtime.js` |
|
||||
| 海外仓工作台与模块入口 | `src/pages/home/home.vue`、`src/pages/module/module.vue` | `src/data/overseas-workbench.js` 是模块、作业可用性和原型数据入口 |
|
||||
| 柜号签收与入仓真实作业 | `src/pages/operation/operation.vue`、`src/api/inbound.js`、`src/api/receiving-adapter.js`、`src/api/overseas.js` | Backend `lastleg-service/inboundplan/pda/controller/ReceivingContainerController.java`、`ReceivingContainerService.java`;公网前缀 `/api/pda/v1/last-leg/receiving-containers/**` |
|
||||
| 柜号签收、照片与入仓真实作业 | `src/pages/operation/operation.vue`、`src/api/inbound.js`、`src/api/receiving-adapter.js`、`src/api/overseas.js`、`src/stores/receiving.js` | 三个固定照片角色各一张,支持拍照/相册、鉴权预览及更换失败保留原图;Backend `lastleg-service/inboundplan/pda/controller/ReceivingContainerController.java`、`ReceivingContainerService.java`,公网前缀 `/api/pda/v1/last-leg/receiving-containers/**` |
|
||||
| 库内、托盘、换标与现场查询待接前端 | `src/data/overseas-workbench.js` 中对应 `available: false` 入口 | Backend 已有 `pda/stocktake/`、`pda/move/`、`pda/warehouse/`、`palletops/`、`relabel/pda/` 与 `overseas-pda-operations-v1.yaml`;本应用新增专用 API/状态恢复和测试前不得启用 |
|
||||
| 扫码设备与诊断 | `src/utils/scan-broadcast.js`、`src/pages/scanner-test/scanner-test.vue` | `src/components/ScanTargetInput.vue`、声音反馈工具;广播 Action/Extra 见本文件服务边界 |
|
||||
| 作业记录与人员 | `src/pages/history/history.vue`、`src/pages/profile/profile.vue` | 会话隔离、退出清理和后续真实作业历史契约 |
|
||||
@@ -47,6 +47,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 必须引导账号恢复当前柜,完成前不得切柜。
|
||||
- 签收照片固定为三个角色且每个角色只有一个当前文件;上传成功后使用服务端 `downloadPath` 在当前会话内鉴权读取。更换失败时保留原 `fileId` 和可提交状态,无原图的失败槽不得计入 `photosReady`;本地临时路径仅用于本次设备预览,不作为跨会话文件事实。
|
||||
- 库内、换标与现场查询只有视觉原型依据,当前工作台入口保持禁用;新增真实作业必须建立 `/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 代替授权和资源归属校验。
|
||||
|
||||
+7
-1
@@ -2,12 +2,17 @@ import { API_ROOT } from '../config/runtime.js'
|
||||
|
||||
function authRequest(path, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${API_ROOT}${path}`
|
||||
console.info('[Overseas PDA auth] request', { method: 'POST', url })
|
||||
uni.request({
|
||||
url: `${API_ROOT}${path}`,
|
||||
url,
|
||||
method: 'POST',
|
||||
data,
|
||||
success(response) {
|
||||
const body = response.data || {}
|
||||
console.info('[Overseas PDA auth] response', { url, statusCode: response.statusCode, code: body.code,
|
||||
requestId: body.requestId || response.header?.['X-Request-Id'] || response.header?.['x-request-id'],
|
||||
traceId: body.traceId || response.header?.['X-Trace-Id'] || response.header?.['x-trace-id'] })
|
||||
if (String(body.code ?? response.statusCode) !== '200') {
|
||||
const error = new Error(body.message || '认证服务请求失败')
|
||||
error.code = body.code || response.statusCode
|
||||
@@ -19,6 +24,7 @@ function authRequest(path, data) {
|
||||
resolve(body.data)
|
||||
},
|
||||
fail(error) {
|
||||
console.error('[Overseas PDA auth] network failure', { url, errMsg: error.errMsg || 'unknown' })
|
||||
const next = new Error(error.errMsg || '网络连接失败')
|
||||
next.network = true
|
||||
reject(next)
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
import { overseasRequest, overseasUpload } from '@/api/overseas.js'
|
||||
import { overseasDownload, overseasRequest, overseasUpload } from '@/api/overseas.js'
|
||||
import { normalizeReceivingCandidate, normalizeReceivingSession } from '@/api/receiving-adapter.js'
|
||||
|
||||
const RECEIVING_BASE = '/pda/v1/last-leg/receiving-containers'
|
||||
@@ -8,6 +8,7 @@ export const listReceivingCandidates = async () => (await overseasRequest('GET',
|
||||
export const getReceivingContainer = async (containerNo) => normalizeReceivingSession(await overseasRequest('GET', `${RECEIVING_BASE}/${encodeURIComponent(containerNo)}`))
|
||||
export const prepareReceivingContainer = async (containerNo, request) => normalizeReceivingSession(await overseasRequest('POST', `${RECEIVING_BASE}/${encodeURIComponent(containerNo)}/prepare`, request))
|
||||
export const uploadCurrentUnloadPhoto = (filePath) => overseasUpload(current('/unload-photos/files'), filePath)
|
||||
export const downloadCurrentUnloadPhoto = (downloadPath) => overseasDownload(downloadPath)
|
||||
export const signCurrentReceiving = async (request) => normalizeReceivingSession(await overseasRequest('POST', current('/sign'), request))
|
||||
export const getCurrentReceiving = async () => normalizeReceivingSession(await overseasRequest('GET', current()))
|
||||
export const resolveCurrentCarton = (cartonMark) => overseasRequest('POST', current('/cartons/resolve'), { cartonMark })
|
||||
|
||||
@@ -78,6 +78,25 @@ function rawUpload(path, filePath, name, token) {
|
||||
})
|
||||
}
|
||||
|
||||
function overseasUrl(path) {
|
||||
const value = String(path || '')
|
||||
const normalized = /\/(?:api|overseas-api)$/.test(session.apiBase)
|
||||
? value.replace(/^\/api(?=\/)/, '')
|
||||
: value
|
||||
return `${session.apiBase}${normalized}`
|
||||
}
|
||||
|
||||
function rawDownload(path, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.downloadFile({
|
||||
url: overseasUrl(path),
|
||||
header: { Authorization: `Bearer ${token}` },
|
||||
success: resolve,
|
||||
fail: (cause) => reject(Object.assign(new Error(cause.errMsg || '签收照片下载失败'), { network: true }))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function overseasUpload(path, filePath, name = 'file', retry = true) {
|
||||
const attemptedToken = session.token
|
||||
let response = await rawUpload(path, filePath, name, attemptedToken)
|
||||
@@ -92,4 +111,17 @@ export async function overseasUpload(path, filePath, name = 'file', retry = true
|
||||
return body.data
|
||||
}
|
||||
|
||||
export async function overseasDownload(path, retry = true) {
|
||||
const attemptedToken = session.token
|
||||
let response = await rawDownload(path, attemptedToken)
|
||||
if (retry && unauthorized(response)) {
|
||||
const replacementToken = await tokenAfterUnauthorized(attemptedToken)
|
||||
response = await rawDownload(path, replacementToken)
|
||||
}
|
||||
if (response?.statusCode !== 200 || !response?.tempFilePath) {
|
||||
throw errorFrom({ ...response, data: {} }, '签收照片下载失败')
|
||||
}
|
||||
return response.tempFilePath
|
||||
}
|
||||
|
||||
export function resetOverseasRequestStateForTest() { refreshPromise = null }
|
||||
|
||||
@@ -30,7 +30,7 @@ export function normalizeReceivingSession(value) {
|
||||
: []
|
||||
return {
|
||||
...value,
|
||||
photos: RECEIVING_PHOTO_ROLES.map((role) => photos.find((photo) => photo?.role === role)).filter(Boolean),
|
||||
photos: RECEIVING_PHOTO_ROLES.map((role) => photos.find((photo) => photo?.role === role && photo?.fileId)).filter(Boolean),
|
||||
totalPackages: number(value.totalPackages),
|
||||
receivedPackages: number(value.receivedPackages),
|
||||
pendingPackages: number(value.pendingPackages),
|
||||
|
||||
@@ -5,6 +5,8 @@ const development = Boolean(runtimeEnvironment.DEV)
|
||||
|
||||
let selectedAuthApiRoot = configuredAuthApiRoot || '/api'
|
||||
let selectedOverseasApiRoot = configuredOverseasApiRoot || '/overseas-api'
|
||||
const nativeRuntime = typeof plus !== 'undefined'
|
||||
|| (typeof location !== 'undefined' && location.protocol === 'file:')
|
||||
|
||||
// H5 must stay same-origin and use Vite's CORS-safe proxy; App builds keep the absolute API roots above.
|
||||
// #ifdef H5
|
||||
@@ -12,6 +14,13 @@ selectedAuthApiRoot = runtimeEnvironment.VITE_H5_AUTH_API_BASE_URL || '/api'
|
||||
selectedOverseasApiRoot = runtimeEnvironment.VITE_H5_OVERSEAS_API_BASE_URL || '/overseas-api'
|
||||
// #endif
|
||||
|
||||
// HBuilderX/App 基座 can expose a file:// location; never allow a relative API
|
||||
// root there, even if a stale H5 bundle or compiler flag was reused.
|
||||
if (nativeRuntime) {
|
||||
selectedAuthApiRoot = configuredAuthApiRoot || selectedAuthApiRoot
|
||||
selectedOverseasApiRoot = configuredOverseasApiRoot || selectedOverseasApiRoot
|
||||
}
|
||||
|
||||
export const API_ROOT = String(selectedAuthApiRoot).replace(/\/$/, '')
|
||||
export const OVERSEAS_API_ROOT = String(selectedOverseasApiRoot).replace(/\/$/, '')
|
||||
export const DEV_LOGIN = Object.freeze({
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
export const MODULES = [
|
||||
{ key: 'inbound', name: '入仓作业', icon: '入', description: '单账号锁定一柜,完成柜号级入库闭环', operations: [
|
||||
{ name: '拆柜收货', icon: '卸', iconType: 'download', description: '三类照片签收,FBA 按 SKU 箱数、海外仓逐箱收货', available: true, badge: '真实接口' }
|
||||
{ name: '拆柜收货', icon: '卸', iconType: 'download', description: '三类照片签收,FBA 按 SKU 箱数、海外仓逐箱收货', available: true }
|
||||
] },
|
||||
{ key: 'inventory', name: '库内作业', icon: '库', description: '盘点、移库与托盘管理', operations: [
|
||||
{ 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 接口' }
|
||||
{ name: '库存盘点', icon: '盘', iconType: 'list', description: '按任务扫描库位和库存', available: false },
|
||||
{ name: '商品移库', icon: '移', iconType: 'loop', description: '扫描来源与目标库位完成移库', available: false },
|
||||
{ name: '托盘管理', icon: '托', iconType: 'grid', description: '组托、拆托及托盘状态核对', available: false }
|
||||
] },
|
||||
{ key: 'relabel', name: '换标作业', icon: '换', description: '换标收货、执行与复核', operations: [
|
||||
{ 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 接口' }
|
||||
{ name: '换标收货', icon: '收', iconType: 'download', description: '扫描换标单并确认待处理库存', available: false },
|
||||
{ name: '换标执行', icon: '换', iconType: 'compose', description: '扫描旧标和新标记录换标结果', available: false },
|
||||
{ name: '换标复核', icon: '核', iconType: 'checkbox', description: '复核标签和商品关联关系', available: false }
|
||||
] },
|
||||
{ key: 'query', name: '现场查询', icon: '查', description: '库存、库位与托盘查询', operations: [
|
||||
{ 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 接口' }
|
||||
{ name: '库存查询', icon: '存', iconType: 'search', description: '扫描商品或箱码查询可用库存', available: false },
|
||||
{ name: '库位查询', icon: '位', iconType: 'location', description: '扫描库位查看容量和在库明细', available: false },
|
||||
{ name: '托盘查询', icon: '托', iconType: 'flag', description: '扫描托盘码查看绑定库存', available: false }
|
||||
] }
|
||||
]
|
||||
|
||||
|
||||
+5
-1
@@ -81,7 +81,11 @@
|
||||
"vueVersion" : "3",
|
||||
"h5" : {
|
||||
"devServer" : {
|
||||
"port" : 5176
|
||||
"proxy": {
|
||||
|
||||
},
|
||||
"port" : 5176,
|
||||
"https": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
<button v-for="operation in group.operations" :key="operation.name" class="home-operation" :class="{ 'home-operation-primary': operation.available, 'home-operation-disabled': !operation.available }" :disabled="!operation.available" @tap="openOperation(operation)">
|
||||
<view class="home-operation-icon" aria-hidden="true"><uni-icons :type="operation.iconType" :color="operation.available ? '#135ee8' : '#9aa6b2'" size="28" /></view>
|
||||
<text class="home-operation-name">{{ operation.name }}</text>
|
||||
<text v-if="operation.available" class="home-operation-pending">{{ operation.badge }}</text>
|
||||
<text v-else class="home-operation-unavailable">{{ operation.badge || '未开放' }}</text>
|
||||
<text v-if="operation.available" class="home-operation-pending">可处理</text>
|
||||
<text v-else class="home-operation-unavailable">暂未开放</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
signInWithTokens(tokens)
|
||||
uni.reLaunch({ url: '/pages/home/home' })
|
||||
} catch (error) {
|
||||
console.error('[Overseas PDA login] failed', { message: error.message, network: error.network === true,
|
||||
code: error.code, requestId: error.requestId, traceId: error.traceId })
|
||||
this.error = error.network ? '当前网络不可用,请联网后重试' : (error.message || '登录失败')
|
||||
} finally {
|
||||
this.submitting = false
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<view class="portal-content">
|
||||
<view class="module-hero" :class="`module-hero-${currentModule.key}`"><view class="module-hero-icon">{{ currentModule.icon }}</view><view><text class="module-hero-title">{{ currentModule.name }}</text><text class="module-hero-desc">{{ currentModule.description }}</text></view></view>
|
||||
<text class="group-title">业务功能</text>
|
||||
<view class="operation-list"><button v-for="operation in currentModule.operations" :key="operation.name" class="operation-item" :class="{ 'operation-item-disabled': !operation.available }" :disabled="!operation.available" @tap="openOperation(operation)"><view class="operation-icon">{{ operation.icon }}</view><view class="operation-info"><text class="operation-name">{{ operation.name }}</text><text class="operation-desc">{{ operation.description }}</text></view><text class="operation-prototype">{{ operation.badge || '未开放' }}</text><text v-if="operation.available" class="operation-arrow">›</text></button></view>
|
||||
<view class="module-empty-note">只有拆柜收货已接海外 PDA 真实接口;其余入口在专用接口、权限与仓库范围契约完成前保持禁用。</view>
|
||||
<view class="operation-list"><button v-for="operation in currentModule.operations" :key="operation.name" class="operation-item" :class="{ 'operation-item-disabled': !operation.available }" :disabled="!operation.available" @tap="openOperation(operation)"><view class="operation-icon">{{ operation.icon }}</view><view class="operation-info"><text class="operation-name">{{ operation.name }}</text><text class="operation-desc">{{ operation.description }}</text></view><text v-if="operation.available" class="operation-arrow">›</text></button></view>
|
||||
<view class="module-empty-note">当前仅开放拆柜收货,其他作业暂未开放。</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -35,8 +35,8 @@ export function setCurrentReceiving(value) {
|
||||
receiving.selectedInboundCode = (value?.inbounds || []).find((item) => Number(item.pendingPackages || 0) > 0)?.inboundCode || value?.inbounds?.[0]?.inboundCode || ''
|
||||
}
|
||||
receiving.photoSlots = RECEIVING_PHOTO_ROLES.map((role) => {
|
||||
const photo = value?.photos?.find((item) => item.role === role)
|
||||
return photo ? { ...photo, fileId: String(photo.fileId), role, uploaded: true } : null
|
||||
const photo = (value?.photos || []).find((item) => item.role === role)
|
||||
return photo ? { ...photo, fileId: String(photo.fileId), role, uploaded: true, status: 'success' } : null
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ function ensureListening() {
|
||||
if (typeof uni.onNetworkStatusChange === 'function') {
|
||||
uni.onNetworkStatusChange((result) => {
|
||||
online = Boolean(result.isConnected)
|
||||
console.info('[Overseas PDA network] status change', { isConnected: result.isConnected, networkType: result.networkType })
|
||||
notify()
|
||||
})
|
||||
}
|
||||
@@ -19,6 +20,7 @@ function ensureListening() {
|
||||
uni.getNetworkType({
|
||||
success: (result) => {
|
||||
online = result.networkType !== 'none'
|
||||
console.info('[Overseas PDA network] initial status', { networkType: result.networkType, online })
|
||||
notify()
|
||||
}
|
||||
})
|
||||
@@ -31,6 +33,7 @@ export function isOnline() {
|
||||
}
|
||||
|
||||
export function requireOnline(message = '当前网络不可用,请联网后刷新重试') {
|
||||
console.info('[Overseas PDA network] preflight', { online })
|
||||
if (!isOnline()) {
|
||||
const error = new Error(message)
|
||||
error.network = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import { overseasRequest, overseasUpload, resetOverseasRequestStateForTest } from '../src/api/overseas.js'
|
||||
import { overseasDownload, overseasRequest, overseasUpload, resetOverseasRequestStateForTest } from '../src/api/overseas.js'
|
||||
import { PDA_AUTHORITY, PDA_ROLE, session, signInWithTokens, signOut } from '../src/stores/session.js'
|
||||
|
||||
const future = () => Math.floor(Date.now() / 1000) + 3600
|
||||
@@ -128,6 +128,18 @@ test('photo upload refreshes and replays once while retaining the file payload',
|
||||
])
|
||||
})
|
||||
|
||||
test('authenticated photo download maps the public api path through the H5 overseas proxy', async () => {
|
||||
const value = prepareSession(); const calls = []
|
||||
globalThis.uni.downloadFile = (options) => {
|
||||
calls.push(options)
|
||||
options.success({ statusCode: 200, tempFilePath: '/tmp/restored.jpg', header: {} })
|
||||
}
|
||||
assert.equal(await overseasDownload('/api/pda/v1/last-leg/receiving-containers/current/unload-photos/files/99/download'), '/tmp/restored.jpg')
|
||||
assert.equal(calls.length, 1)
|
||||
assert.equal(calls[0].url, '/overseas-api/pda/v1/last-leg/receiving-containers/current/unload-photos/files/99/download')
|
||||
assert.equal(calls[0].header.Authorization, `Bearer ${value.overseasAccessToken}`)
|
||||
})
|
||||
|
||||
test('business and network errors retain diagnostics without refreshing or clearing a valid session', async () => {
|
||||
prepareSession(); let refreshCount = 0
|
||||
globalThis.uni.request = (options) => {
|
||||
@@ -147,4 +159,3 @@ test('business and network errors retain diagnostics without refreshing or clear
|
||||
await assert.rejects(overseasRequest('GET', '/pda/network'), (error) => error.network === true && /socket/.test(error.message))
|
||||
assert.ok(session.currentUser)
|
||||
})
|
||||
|
||||
|
||||
@@ -57,6 +57,6 @@ test('unimplemented warehouse operations cannot submit prototype scans', async (
|
||||
])
|
||||
assert.doesNotMatch(operation, /prototypeScan|prototypeInput/)
|
||||
assert.match(operation, /功能暂未开放|扫码不会提交/)
|
||||
assert.match(home, /operation\.badge/)
|
||||
assert.match(module, /专用接口、权限与仓库范围契约完成前保持禁用/)
|
||||
assert.match(home, /暂未开放/)
|
||||
assert.match(module, /当前仅开放拆柜收货,其他作业暂未开放/)
|
||||
})
|
||||
|
||||
@@ -6,11 +6,12 @@ test('receiving adapter preserves the three signed photo roles in workflow order
|
||||
const snowflakeFileId = '2092476334483931138'
|
||||
const session = normalizeReceivingSession({
|
||||
status: 'WAIT_SIGN', totalPackages: 8, receivedPackages: 2, pendingPackages: 6,
|
||||
photos: [{ role: 'DOOR_OPEN', fileId: '3' }, { role: 'LICENSE_PLATE', fileId: snowflakeFileId }, { role: 'CONTAINER_SEAL', fileId: '2' }],
|
||||
photos: [{ role: 'DOOR_OPEN', fileId: '3' }, { role: 'LICENSE_PLATE', fileId: snowflakeFileId }, { role: 'CONTAINER_SEAL', fileId: '2' }, { role: 'DOOR_OPEN', fileId: '4' }],
|
||||
inbounds: [{ inboundCode: 'IN-1', plannedPalletCount: 2, totalPackages: 8, receivedPackages: 2, pendingPackages: 6 }]
|
||||
})
|
||||
assert.deepEqual(session.photos.map(({ role }) => role), RECEIVING_PHOTO_ROLES)
|
||||
assert.equal(session.photos[0].fileId, snowflakeFileId)
|
||||
assert.equal(session.photos.length, 3)
|
||||
assert.equal(session.inbounds[0].plannedPalletCount, 2)
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const read = (relative) => readFileSync(new URL(relative, import.meta.url), 'utf8')
|
||||
const page = read('../src/pages/operation/operation.vue')
|
||||
const adapter = read('../src/api/receiving-adapter.js')
|
||||
const inbound = read('../src/api/inbound.js')
|
||||
const controller = read('../../../baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/controller/ReceivingContainerController.java')
|
||||
const openapi = read('../../../baiyue-firstleg/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml')
|
||||
|
||||
function photoUiBlockers() {
|
||||
const values = []
|
||||
const hasThumbnail = /<image[\s\S]*photoSlots\[index\]/.test(page)
|
||||
const hasPreview = /uni\.previewImage\(/.test(page)
|
||||
const hasRestoreDownload = /@GetMapping\([^\n]*unload-photos/.test(controller)
|
||||
|| /receiving-containers\/current\/unload-photos\/files\/\{fileId\}/.test(openapi)
|
||||
const hasRestoreClient = /downloadCurrentUnloadPhoto/.test(inbound) && /downloadPath/.test(page)
|
||||
const developmentLabels = ['真实接口', '待 PDA 接口', 'Mock']
|
||||
.filter((label) => page.toLowerCase().includes(label.toLowerCase()))
|
||||
if (!hasThumbnail || !hasPreview) values.push({
|
||||
code: 'PHOTO_THUMBNAIL_PREVIEW_MISSING', deploymentBlocker: true,
|
||||
reason: '上传成功后只显示完成图标,尚未渲染缩略图或调用uni.previewImage',
|
||||
})
|
||||
if (!hasRestoreDownload) values.push({
|
||||
code: 'PHOTO_RESTORE_DOWNLOAD_ENDPOINT_MISSING', deploymentBlocker: true,
|
||||
reason: 'current恢复只返回fileId/role,后端没有签收照片鉴权下载端点,无法恢复后回显',
|
||||
})
|
||||
else if (!hasRestoreClient) values.push({
|
||||
code: 'PHOTO_RESTORE_CLIENT_PENDING', deploymentBlocker: true,
|
||||
reason: '后端下载契约仍在冻结中,前端尚未接入重载后的鉴权照片回显',
|
||||
})
|
||||
if (developmentLabels.length) values.push({
|
||||
code: 'DEVELOPMENT_LABELS_VISIBLE', deploymentBlocker: true,
|
||||
reason: `页面仍含开发标识:${developmentLabels.join(',')}`,
|
||||
})
|
||||
return values
|
||||
}
|
||||
|
||||
test('three fixed receiving photo roles and string file IDs are frozen', () => {
|
||||
assert.match(adapter, /RECEIVING_PHOTO_ROLES = \['LICENSE_PLATE', 'CONTAINER_SEAL', 'DOOR_OPEN'\]/)
|
||||
assert.match(adapter, /fileId: String\(photo\.fileId\)/)
|
||||
assert.match(page, /fileId: String\(file\.fileId\)/)
|
||||
assert.match(page, /fileId: String\(item\.fileId\)/)
|
||||
assert.doesNotMatch(inbound, /customerId|planNo|allocationId|warehouseId|locationId/)
|
||||
})
|
||||
|
||||
test('camera or album replaces only one fixed slot and failed replacement preserves its previous success', () => {
|
||||
assert.match(page, /choosePhotos\(index, sourceType\)[\s\S]*uni\.chooseImage\(\{ count: 1, sourceType: \[sourceType\]/)
|
||||
assert.match(page, /choosePhotos\(index, 'camera'\)/)
|
||||
assert.match(page, /choosePhotos\(index, 'album'\)/)
|
||||
assert.match(page, /async replacePhoto\(index, path\)[\s\S]*const previous = receiving\.photoSlots\[index\]/)
|
||||
assert.match(page, /await uploadCurrentUnloadPhoto\(path\)[\s\S]*fileId: String\(file\.fileId\)[\s\S]*localPath: path/)
|
||||
assert.match(page, /previous\?\.fileId \? \{ \.\.\.previous, status: 'error'/)
|
||||
assert.match(page, /status: 'uploading'/)
|
||||
assert.match(page, /status: 'error'/)
|
||||
assert.match(page, /已保留原图/)
|
||||
assert.match(page, /photoSlots\[index\]\?\.fileId \? '重拍' : '拍照'/)
|
||||
assert.match(page, /photoSlots\[index\]\?\.fileId \? '重新选择' : '从相册选择'/)
|
||||
assert.match(page, /previewPhoto\(index\)/)
|
||||
assert.match(page, /uploadedPhotoCount\(\) \{ return this\.photoSlots\.filter\(\(item\) => item\?\.fileId\)\.length \}/)
|
||||
assert.match(page, /photosReady\(\) \{ return this\.uploadedPhotoCount === 3 && !this\.photoSlots\.some/)
|
||||
assert.match(page, /:disabled="busy \|\| !photosReady" @tap="signContainer"/)
|
||||
assert.match(page, /async signContainer\(\) \{ if \(!this\.photosReady\) return;/)
|
||||
assert.match(page, /const photos = this\.photoSlots\.map\(/)
|
||||
assert.doesNotMatch(page, /count:\s*9|flatMap\(|photoSlots\[index\]\.push/)
|
||||
})
|
||||
|
||||
test('local thumbnail, preview, and authenticated session restore are wired to the frozen contract', () => {
|
||||
assert.match(page, /restorePhotoPreviews\(\)[\s\S]*photo\?\.downloadPath[\s\S]*downloadCurrentUnloadPhoto\(photo\.downloadPath\)/)
|
||||
assert.deepEqual(photoUiBlockers().map((item) => item.code), [])
|
||||
assert.ok(photoUiBlockers().every((item) => item.deploymentBlocker === true && item.reason))
|
||||
})
|
||||
@@ -11,7 +11,6 @@ test('only operations backed by a dedicated overseas PDA contract are enabled',
|
||||
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 接口')
|
||||
assert.equal(operation?.badge, undefined)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user