This commit is contained in:
2026-08-26 15:59:08 +08:00
parent ee83d87a85
commit 3c283ada46
19 changed files with 196 additions and 46 deletions
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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 })
+32
View File
@@ -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 }
+1 -1
View File
@@ -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),
+9
View File
@@ -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({
+10 -10
View File
@@ -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
View File
@@ -81,7 +81,11 @@
"vueVersion" : "3",
"h5" : {
"devServer" : {
"port" : 5176
"proxy": {
},
"port" : 5176,
"https": false
}
}
}
+2 -2
View File
@@ -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>
+2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
})
}
+3
View File
@@ -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
+13 -2
View File
@@ -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)
})
+2 -2
View File
@@ -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, /当前仅开放拆柜收货,其他作业暂未开放/)
})
+2 -1
View File
@@ -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))
})
+1 -2
View File
@@ -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)
}
})