diff --git a/AGENTS.md b/AGENTS.md
index e67df4c..f464dab 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -13,7 +13,7 @@
- 正式作业已确认为在线模式:断网不能开始正式拣货、提交、完成任务或推进 `LOADED`,不实现离线业务提交队列;恢复网络后提示员工刷新重试。
- 明确使用的工具链和发布流程后,补充准确的依赖版本、安装、调试、测试和打包命令,并同步更新 `README.md` 与本文件。
- 在 `package.json` 与锁文件正式补齐前,`vite.config.js` 只证明目标配置,不证明依赖可安装;维护者只能运行仓库当前可执行的 `node --test tests/*.test.mjs`,不得从海外 PDA 或全局缓存猜测国内 PDA 的 uni-app/Vite 版本。
-- App API 根只读 `VITE_API_BASE_URL`;H5 固定优先使用 `VITE_H5_API_BASE_URL` 的同源 `/api` 并由 `vite.config.js` 将其代理到 `API_PROXY_TARGET`。环境地址不写入源码,切换构建环境后不得沿用登录缓存中的旧 API 地址。
+- App API 根只读 `VITE_API_BASE_URL`;H5 固定优先使用 `VITE_H5_API_BASE_URL=/gateway-api` 并由 `vite.config.js` 同源代理、重写为 Gateway `/api`,避免截获项目自身 `api/` 前端模块。环境地址不写入源码,切换构建环境后不得沿用登录缓存中的旧 API 地址。
- 国内 Android App 启动后仅在 `APP-PLUS` 调用 `utils/app-update.js`,通过独立 `VITE_PDA_UPDATE_URL` 检查更新;更新服务和下载地址必须为 HTTPS。APK 以整数 `versionCode` 判新,WGT 以 `versionName` 判新;强制更新不可跳过,WGT 安装后立即重启。国内 AppID、包名、签名、版本和更新通道不得复用海外 PDA。
## 作业流程原则
@@ -49,13 +49,16 @@
| 目标 | 入口 | 继续查询 |
|---|---|---|
| 页面注册与启动 | `pages.json`、`main.js`、`App.vue` | 全局样式和应用生命周期 |
-| API 环境与 H5 开发代理 | `config/runtime.js`、`.env.example`、`vite.config.js` | App `VITE_API_BASE_URL`;H5 同源 `VITE_H5_API_BASE_URL=/api`,代理上游 `API_PROXY_TARGET`;会话恢复不得覆盖当前构建地址 |
+| API 环境与 H5 开发代理 | `config/runtime.js`、`.env.example`、`vite.config.js` | App `VITE_API_BASE_URL`;H5 同源 `VITE_H5_API_BASE_URL=/gateway-api`,代理上游 `API_PROXY_TARGET` 并重写为 `/api`;会话恢复不得覆盖当前构建地址 |
+| 部分拣货布局与库位展示 | `pages/picking/picking.vue`、`styles/pda.scss` | 库位为空时不渲染空标签或残留分隔符;部分拣货数量必须小于需求且不超过服务端库存/库位可用量,合同 `tests/partial-layout.test.mjs`、`tests/picking-location-display.test.mjs` |
| Android APK/WGT 在线更新 | `App.vue`、`utils/app-update.js`、`.env.example`、`manifest.json` | `VITE_PDA_UPDATE_URL` 使用国内独立 HTTPS 通道;合同 `tests/app-update.test.mjs`,H5 不执行更新逻辑 |
| 登录与会话 | `pages/login/`、`stores/session.js` | 远程鉴权适配和会话清理 |
| 工作台与模块入口 | `pages/home/`、`pages/module/`、`pages/operation/` | `data/demo-data.js` 与页面禁用规则 |
| 待处理任务、服务端就绪与领取 | `pages/tasks/tasks.vue` | `api/pda-loading.js` 的 `listLoadingOrders`、`getContainerPickingTask`、`claimContainerPickingTask`;合并任务和缺少柜号/仓库/版本的任务不可进入 |
+| 业务编号展示与错误脱敏 | `utils/user-facing.js`、`tests/user-facing-identifiers.test.mjs` | 任务和作业页优先显示真实集装箱号,缺失时显示柜次号,再缺失显示“集装箱号 —”;不得展示 taskId、inboundId、orderId、loadingPlanId、containerBookingId 或长整型内部 ID,错误仍保留 code、requestId、traceId |
| 拣货任务与箱码扫码 | `pages/picking/`、`stores/picking.js` | `api/pda-loading.js`、`utils/scan-broadcast.js`、`components/ScanTargetInput.vue`;服务端契约见 `../../baiyue-firstleg/firstleg/src/main/resources/openapi/phase3-pda-convergence-v1.yaml` |
| 装柜取证与完成 | `pages/picking/picking.vue` 的 `evidence` 阶段 | 正式边界是 `api/pda-loading.js` 的证据查询、预览、上传、作废和 `completeContainerPickingTask` 全部按当前任务与仓库进入;不得保留按入仓单/客户作用域的旧证据调用。上传队列 `utils/evidence-upload-queue.js`,合同 `tests/pda-loading-api.test.mjs`、`tests/picking-evidence-stage.test.mjs` |
+| 装柜取证与完成 | `pages/picking/picking.vue` 的 `evidence` 阶段 | 正式边界是 `api/pda-loading.js` 的证据查询、预览、上传、作废和 `completeContainerPickingTask` 全部按当前任务与仓库进入;不得保留按入仓单/客户作用域的旧证据调用。上传队列 `utils/evidence-upload-queue.js`,合同 `tests/pda-loading-api.test.mjs`、`tests/picking-evidence-stage.test.mjs`、`tests/seal-save-idempotency.test.mjs` |
| 扫描诊断 | `pages/scanner-test/` | `utils/scan-broadcast.js` 与广播 Action/Extra |
| 作业记录与人员 | `pages/history/`、`pages/profile/` | session 与本地敏感数据清理 |
| 页面与真实 API 合同 | `tests/page-contract.test.mjs`、`tests/pda-loading-api.test.mjs` | 覆盖部分拣货页面、提交门禁、库位不足反馈和国内 PDA 真实接口路径;统一由 `node --test tests/*.test.mjs` 执行 |
diff --git a/README.md b/README.md
index 832c676..188e728 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@
- `pages/scanner-test/scanner-test`:Android 硬件扫描广播诊断页;
- `pages/picking/picking`:集装箱拣货完整交互。
-登录会话集中在 `stores/session.js`,拣货会话集中在 `stores/picking.js`;当前实现仍是内存态,不写入设备。产品契约要求未提交拣货草稿持久化到设备,并绑定作业员、任务和柜次,其他账号不能继承;成功提交或明确放弃后才清理。页面头部与底部主导航位于 `components/`,演示账号、模块和任务数据位于 `data/demo-data.js`,跨页面 PDA 视觉规则位于 `styles/pda.scss`。
+登录会话集中在 `stores/session.js` 并持久化到设备,拣货会话集中在 `stores/picking.js`。认证请求由 `api/http.js` 统一处理:Access Token 临近过期时使用 Refresh Token 轮换;并发请求共享一次刷新;401 刷新成功后只重放一次;Refresh Token 明确失效时清理会话和作业草稿并返回登录页,网络或服务异常不会误清会话。产品契约要求未提交拣货草稿持久化到设备,并绑定作业员、任务和柜次,其他账号不能继承;成功提交或明确放弃后才清理。页面头部与底部主导航位于 `components/`,演示账号、模块和任务数据位于 `data/demo-data.js`,跨页面 PDA 视觉规则位于 `styles/pda.scss`。
拣货原型会在本次登录会话内按箱保存本地草稿和最近扫码结果。已确认的正式流程是:进入任务即由后端标记“拣货中”;扫码只更新本地草稿,员工点击提交时才将整批明细写入后端,不逐码提交。允许部分拣货或跳拣,但每个未完成项必须填写异常原因,并挂起任务、阻断完成和 `LOADED`。当前草稿尚未持久化到设备,这是实现缺口,不代表允许离线作业。
diff --git a/api/auth.js b/api/auth.js
index 4ed5cb5..3e34e93 100644
--- a/api/auth.js
+++ b/api/auth.js
@@ -1,11 +1,13 @@
import { API_ROOT } from '@/config/runtime.js'
-function authRequest(path, data) {
+function authRequest(path, data, options = {}) {
return new Promise((resolve, reject) => {
uni.request({
url: `${API_ROOT}${path}`,
method: 'POST',
data,
+ header: options.header,
+ timeout: options.timeout || 15000,
success(response) {
const body = response.data || {}
if (String(body.code ?? response.statusCode) !== '200') {
@@ -27,10 +29,17 @@ function authRequest(path, data) {
})
}
-export function login(username, password) {
- return authRequest('/auth/login', { username, password })
+export function login(loginId, password) {
+ return authRequest('/auth/login', { loginId, password })
}
export function refresh(refreshToken) {
return authRequest('/auth/refresh', { refreshToken })
}
+
+export function logout(refreshToken, accessToken) {
+ return authRequest('/auth/logout', { refreshToken }, {
+ header: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
+ timeout: 5000
+ })
+}
diff --git a/api/http.js b/api/http.js
new file mode 100644
index 0000000..eb27260
--- /dev/null
+++ b/api/http.js
@@ -0,0 +1,149 @@
+import { refresh as refreshRequest } from '@/api/auth.js'
+import { isAccessTokenExpiring, session, signOut, updateTokenPair } from '@/stores/session.js'
+import { requireOnline } from '@/utils/network.js'
+
+let refreshPromise = null
+let redirectingToLogin = false
+
+function responseCode(response) {
+ return String(response?.data?.code ?? response?.statusCode ?? '')
+}
+
+function isUnauthorized(responseOrError) {
+ const statusCode = Number(responseOrError?.statusCode ?? responseOrError?.response?.statusCode ?? 0)
+ const code = String(responseOrError?.code ?? responseCode(responseOrError))
+ return statusCode === 401 || code === '401' || code === 'A0401'
+}
+
+function sessionExpiredError(source) {
+ const error = new Error('登录已过期,请重新登录')
+ error.code = source?.code || 'A0401'
+ error.statusCode = source?.statusCode || 401
+ return error
+}
+
+function redirectAfterSessionExpiry() {
+ if (redirectingToLogin) return
+ redirectingToLogin = true
+ if (typeof uni !== 'undefined' && typeof uni.showToast === 'function') {
+ uni.showToast({ title: '登录已过期,请重新登录', icon: 'none', duration: 2200 })
+ }
+ if (typeof uni !== 'undefined' && typeof uni.reLaunch === 'function') {
+ uni.reLaunch({
+ url: '/pages/login/login',
+ complete() { setTimeout(() => { redirectingToLogin = false }, 1000) }
+ })
+ }
+}
+
+function expireSession(source, observedRefreshToken = session.refreshToken) {
+ if (observedRefreshToken && session.refreshToken && observedRefreshToken !== session.refreshToken) return session.token
+ const hadSession = Boolean(session.currentUser || session.token || session.refreshToken)
+ signOut()
+ if (hadSession) redirectAfterSessionExpiry()
+ throw sessionExpiredError(source)
+}
+
+function networkError(error, fallback = '网络连接失败') {
+ const next = new Error(error?.errMsg || fallback)
+ next.network = true
+ return next
+}
+
+function rawRequest(options, accessToken) {
+ const baseUrl = session.apiBase || '/api/platform/v1'
+ const headers = { ...(options.header || {}) }
+ if (accessToken) headers.Authorization = `Bearer ${accessToken}`
+ if (session.requestId) headers['X-Request-Id'] = session.requestId
+ return new Promise((resolve, reject) => {
+ uni.request({
+ ...options,
+ url: `${baseUrl}${options.url}`,
+ header: headers,
+ success: resolve,
+ fail: (error) => reject(networkError(error))
+ })
+ })
+}
+
+async function refreshAccessToken() {
+ const observedRefreshToken = session.refreshToken
+ if (!observedRefreshToken) return expireSession(null, observedRefreshToken)
+ if (!refreshPromise) {
+ refreshPromise = (async () => {
+ if (session.refreshToken !== observedRefreshToken && session.token) return session.token
+ try {
+ const tokens = await refreshRequest(observedRefreshToken)
+ if (session.refreshToken !== observedRefreshToken) {
+ if (session.token) return session.token
+ throw sessionExpiredError()
+ }
+ updateTokenPair(tokens)
+ redirectingToLogin = false
+ return session.token
+ } catch (error) {
+ if (isUnauthorized(error)) return expireSession(error, observedRefreshToken)
+ throw error
+ }
+ })().finally(() => { refreshPromise = null })
+ }
+ return refreshPromise
+}
+
+async function validAccessToken() {
+ if (!session.token || isAccessTokenExpiring(session.token)) return refreshAccessToken()
+ return session.token
+}
+
+export function ensureAccessToken() {
+ return validAccessToken()
+}
+
+async function runAuthenticated(execute) {
+ requireOnline()
+ let accessToken = await validAccessToken()
+ let response = await execute(accessToken)
+ if (!isUnauthorized(response)) return response
+ accessToken = session.token && session.token !== accessToken ? session.token : await refreshAccessToken()
+ const activeRefreshToken = session.refreshToken
+ response = await execute(accessToken)
+ if (isUnauthorized(response)) expireSession(response, activeRefreshToken)
+ return response
+}
+
+export function apiRequest(options) {
+ return runAuthenticated((accessToken) => rawRequest(options, accessToken)).then((response) => {
+ const body = response.data || {}
+ const code = responseCode(response)
+ if (code !== '200') {
+ const error = new Error(body.message || '服务端请求失败')
+ error.code = body.code || response.statusCode
+ error.statusCode = response.statusCode
+ error.requestId = body.requestId || response.header?.['X-Request-Id'] || response.header?.['x-request-id']
+ error.traceId = body.traceId || response.header?.['X-Trace-Id'] || response.header?.['x-trace-id']
+ throw error
+ }
+ return body.data
+ })
+}
+
+export function apiDownload(options) {
+ return runAuthenticated((accessToken) => new Promise((resolve, reject) => {
+ const headers = { ...(options.header || {}) }
+ if (accessToken) headers.Authorization = `Bearer ${accessToken}`
+ if (session.requestId) headers['X-Request-Id'] = session.requestId
+ uni.downloadFile({ ...options, header: headers, success: resolve, fail: (error) => reject(networkError(error, '文件下载失败')) })
+ }))
+}
+
+export function apiUpload(options, onProgress) {
+ return runAuthenticated((accessToken) => new Promise((resolve, reject) => {
+ const headers = { ...(options.header || {}) }
+ if (accessToken) headers.Authorization = `Bearer ${accessToken}`
+ if (session.requestId) headers['X-Request-Id'] = session.requestId
+ const task = uni.uploadFile({ ...options, header: headers, success: resolve, fail: (error) => reject(networkError(error)) })
+ if (task && typeof task.onProgressUpdate === 'function' && typeof onProgress === 'function') task.onProgressUpdate(({ progress }) => onProgress(progress))
+ }))
+}
+
+export { isUnauthorized }
diff --git a/api/pda-loading.js b/api/pda-loading.js
index 7d7cfce..5f62af9 100644
--- a/api/pda-loading.js
+++ b/api/pda-loading.js
@@ -1,5 +1,9 @@
import { session } from '@/stores/session.js'
import { requireOnline } from '@/utils/network.js'
+import { apiDownload, apiRequest, apiUpload } from '@/api/http.js'
+import { resolveEvidencePreviewUrl } from '@/utils/api-url.js'
+
+export { resolveEvidencePreviewUrl } from '@/utils/api-url.js'
const EVIDENCE_TYPES = Object.freeze([
'PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL'
@@ -15,53 +19,14 @@ const EVIDENCE_SLOTS = Object.freeze([
{ type: 'SEAL', label: '带锁码照' }
])
-// C02 supplies inboundId/inboundVersion and loading.pickingVersion. C04 supplies
-// inbound-scoped task and evidence operations. No local task is promoted to a
-// server success when any required identifier or version is absent.
+// Picking and cabinet evidence are both scoped by the canonical PDA task. The
+// task warehouse is always sent as an authorization boundary and never guessed.
function debugLog(event, payload = {}) {
try { console.info(`[PDA] ${event}`, payload) } catch (_) { /* console may be unavailable on a device */ }
}
-function request(options) {
- requireOnline()
- const baseUrl = session.apiBase || '/api/platform/v1'
- const headers = { ...(options.header || {}) }
- if (session.token) headers.Authorization = `Bearer ${session.token}`
- if (session.requestId) headers['X-Request-Id'] = session.requestId
-
- return new Promise((resolve, reject) => {
- uni.request({
- ...options,
- url: `${baseUrl}${options.url}`,
- header: headers,
- success(response) {
- const body = response.data || {}
- const code = String(body.code ?? response.statusCode)
-
- if (code !== '200') {
- const error = new Error(body.message || '服务端请求失败')
- error.code = body.code || response.statusCode
- error.statusCode = response.statusCode
- error.requestId = body.requestId || response.header?.['X-Request-Id'] || response.header?.['x-request-id']
- error.traceId = body.traceId || response.header?.['X-Trace-Id'] || response.header?.['x-trace-id']
- reject(error)
- return
- }
- resolve(body.data)
- },
- fail(error) {
- const next = new Error(error.errMsg || '网络连接失败')
- next.network = true
- reject(next)
- }
- })
- })
-}
-
-function idempotencyKey(operation, inboundId) {
- return `pda-${operation}-${inboundId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
-}
+const request = apiRequest
function stableIdempotencyKey(operationId, suffix = '') {
const raw = `pda-${operationId}${suffix ? `-${suffix}` : ''}`
@@ -74,12 +39,6 @@ function stableIdempotencyKey(operationId, suffix = '') {
return `${prefix}-${(hash >>> 0).toString(36)}-${raw.length}`.slice(0, 128)
}
-function tenantQuery(tenantId) {
- const value = tenantId ?? session.tenantId
- if (!value) throw new Error('缺少客户隔离标识 tenantId,无法请求现场任务')
- return String(value)
-}
-
export function isRemoteReady(inboundId) {
return Boolean(session.token && (session.tenantId || inboundId))
}
@@ -189,7 +148,10 @@ export function normalizePdaOrder(row = {}) {
cabinetNo: row.cabinetNo ?? row.loading?.cabinetNo ?? row.container?.cabinetNo ?? row.containerNo ?? '',
currentContainerNo: row.currentContainerNo ?? row.containerNo ?? row.container?.containerNo ?? row.loadingPlan?.containerNo ?? row.loading?.containerNo ?? null,
warehouseId: row.warehouseId ?? row.warehouse?.id ?? row.loadingPlan?.warehouseId ?? null,
- displayNo: row.orderNo ?? row.inboundCode ?? row.order?.orderNo ?? '业务单号待同步'
+ warehouseName: row.warehouseName ?? row.warehouse?.name ?? row.loadingPlan?.warehouseName ?? '',
+ orderNo: row.orderNo ?? row.order?.orderNo ?? '',
+ inboundCode: row.inboundCode ?? row.inbound?.inboundCode ?? '',
+ displayNo: row.orderNo ?? row.order?.orderNo ?? row.inboundCode ?? row.inbound?.inboundCode ?? ''
}
}
@@ -205,6 +167,9 @@ export function normalizePickingTask(task = {}) {
mergedIntoContainerBookingId: task.mergedIntoContainerBookingId ?? task.container?.mergedIntoContainerBookingId ?? null,
cabinetNo: task.cabinetNo ?? task.loading?.cabinetNo ?? task.containerNo ?? '',
currentContainerNo: task.currentContainerNo ?? task.containerNo ?? null,
+ orderNo: task.orderNo ?? task.order?.orderNo ?? '',
+ inboundCode: task.inboundCode ?? task.inbound?.inboundCode ?? '',
+ warehouseName: task.warehouseName ?? task.warehouse?.name ?? '',
pickingVersion: task.pickingVersion ?? task.version ?? null,
inboundVersion: task.inboundVersion ?? null
}
@@ -214,61 +179,26 @@ export function normalizeInventorySnapshot(snapshot = {}) {
return normalizePickingTask({ ...snapshot, version: snapshot.taskVersion ?? snapshot.version })
}
-export function getPickingTask(inboundId, tenantId) {
+export function getEvidence(taskId, warehouseId) {
+ if (!taskId) throw new Error('缺少装柜任务标识,无法加载装柜证据')
+ if (!warehouseId) throw new Error('缺少任务仓库标识,无法加载装柜证据')
return request({
- url: `/inbounds/${encodeURIComponent(inboundId)}/loading/picking?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`,
+ url: `/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}/evidence?warehouseId=${encodeURIComponent(warehouseId)}`,
method: 'GET'
})
}
-export function startPicking(inboundId, payload, tenantId) {
- return request({
- url: `/inbounds/${encodeURIComponent(inboundId)}/loading/picking/start?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`,
- method: 'POST',
- header: { 'Idempotency-Key': idempotencyKey('start-picking', inboundId) },
- data: payload
- })
-}
-
-export function completePicking(inboundId, payload, tenantId) {
- return request({
- url: `/inbounds/${encodeURIComponent(inboundId)}/loading/picking/complete?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`,
- method: 'POST',
- header: { 'Idempotency-Key': idempotencyKey('complete-picking', inboundId) },
- data: payload
- })
-}
-
-export function getEvidence(inboundId, tenantId) {
- return request({
- url: `/inbounds/${encodeURIComponent(inboundId)}/loading/evidence/pda?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`,
- method: 'GET'
- })
-}
-
-export function getEvidencePreviewUrl(inboundId, evidenceId, tenantId) {
- if (!inboundId || !evidenceId) return ''
- return `/api/platform/v1/inbounds/${encodeURIComponent(inboundId)}/loading/evidence/${encodeURIComponent(evidenceId)}/content/pda?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`
+export function getEvidencePreviewUrl(taskId, evidenceId, warehouseId) {
+ if (!taskId || !evidenceId || !warehouseId) return ''
+ return `/api/platform/v1/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}/evidence/${encodeURIComponent(evidenceId)}/content?warehouseId=${encodeURIComponent(warehouseId)}`
}
export function downloadEvidencePreview(previewUrl) {
requireOnline('当前网络不可用,无法查看装柜照片')
if (!previewUrl) throw new Error('装柜照片地址尚未同步')
- const baseUrl = session.apiBase || '/api/platform/v1'
- let url = String(previewUrl)
- if (!/^https?:\/\//i.test(url) && /^https?:\/\//i.test(baseUrl)) {
- const origin = baseUrl.match(/^(https?:\/\/[^/]+)/i)?.[1] || ''
- url = `${origin}${url.startsWith('/') ? '' : '/'}${url}`
- }
- const headers = {}
- if (session.token) headers.Authorization = `Bearer ${session.token}`
- if (session.requestId) headers['X-Request-Id'] = session.requestId
+ const url = resolveEvidencePreviewUrl(previewUrl, session.apiBase || '/api/platform/v1')
debugLog('evidence.preview.start', { previewPath: String(previewUrl), resolvedUrl: url })
- return new Promise((resolve, reject) => {
- uni.downloadFile({
- url,
- header: headers,
- success(response) {
+ return apiDownload({ url }).then((response) => {
const responseHeaders = response.header || {}
const contentType = responseHeaders['Content-Type'] || responseHeaders['content-type'] || ''
const requestId = responseHeaders['X-Request-Id'] || responseHeaders['x-request-id'] || ''
@@ -279,8 +209,7 @@ export function downloadEvidencePreview(previewUrl) {
error.statusCode = response.statusCode
error.requestId = requestId
error.traceId = traceId
- reject(error)
- return
+ throw error
}
if (contentType && !/^image\//i.test(contentType) && !/^application\/octet-stream/i.test(contentType)) {
const error = new Error('服务端未返回有效照片')
@@ -288,80 +217,65 @@ export function downloadEvidencePreview(previewUrl) {
error.contentType = contentType
error.requestId = requestId
error.traceId = traceId
- reject(error)
- return
+ throw error
}
- resolve(response.tempFilePath)
- },
- fail(error) {
- debugLog('evidence.preview.error', { previewPath: String(previewUrl), message: error.errMsg || '' })
- const next = new Error(error.errMsg || '装柜照片下载失败')
- next.network = true
- reject(next)
- }
- })
+ return response.tempFilePath
+ }).catch((error) => {
+ debugLog('evidence.preview.error', { previewPath: String(previewUrl), message: error.message || error.errMsg || '' })
+ throw error
})
}
-export function upsertEvidence(inboundId, payload, tenantId) {
- if (!EVIDENCE_TYPES.includes(payload.evidenceType)) throw new Error('不支持的装柜证据类型')
- return request({
- url: `/inbounds/${encodeURIComponent(inboundId)}/loading/evidence/pda?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`,
- method: 'POST',
- header: { 'Idempotency-Key': stableIdempotencyKey(payload.operationId || `evidence-${inboundId}-${payload.evidenceType}-${payload.fileId || 'seal'}`) },
- data: payload
- })
-}
-
-export function voidEvidence(inboundId, evidenceId, payload = {}, tenantId) {
- if (!inboundId || !evidenceId) throw new Error('缺少装柜证据标识,无法作废')
+export function voidEvidence(taskId, evidenceId, warehouseId, payload = {}) {
+ if (!taskId || !evidenceId) throw new Error('缺少装柜任务或证据标识,无法作废')
+ if (!warehouseId) throw new Error('缺少任务仓库标识,无法作废装柜证据')
if (!String(payload.reason || '').trim()) throw new Error('作废原因不能为空')
return request({
- url: `/inbounds/${encodeURIComponent(inboundId)}/loading/evidence/${encodeURIComponent(evidenceId)}/void/pda?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`,
+ url: `/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}/evidence/${encodeURIComponent(evidenceId)}/void`,
method: 'POST',
- header: { 'Idempotency-Key': stableIdempotencyKey(payload.operationId || `void-evidence-${inboundId}-${evidenceId}-${payload.expectedVersion ?? 0}`) },
- data: payload
+ data: { warehouseId, expectedVersion: payload.expectedVersion ?? 0, reason: String(payload.reason).trim() }
})
}
/** Upload a local photo to the file service and return its durable fileId. */
-export function uploadEvidenceFile(inboundId, evidenceType, localPath, tenantId, onProgress) {
+export function uploadEvidenceFile(taskId, warehouseId, payload, localPath, onProgress) {
requireOnline('当前网络不可用,无法上传装柜照片')
+ if (!taskId) throw new Error('缺少装柜任务标识,无法上传装柜照片')
+ if (!warehouseId) throw new Error('缺少任务仓库标识,无法上传装柜照片')
+ const evidenceType = payload?.evidenceType
if (!EVIDENCE_TYPES.includes(evidenceType)) throw new Error('不支持的装柜证据类型')
if (!localPath) throw new Error('缺少待上传的照片')
- const tenant = tenantQuery(tenantId)
+ if (payload.expectedVersion == null) throw new Error('缺少装柜证据版本,无法上传装柜照片')
+ if (evidenceType === 'SEAL' && !String(payload.sealNo || '').trim()) throw new Error('带锁码照必须填写封签号')
const baseUrl = session.apiBase || '/api/platform/v1'
- const headers = {}
- if (session.token) headers.Authorization = `Bearer ${session.token}`
- if (session.requestId) headers['X-Request-Id'] = session.requestId
- debugLog('evidence.upload.start', { inboundId, evidenceType, tenantId: tenant, hasLocalPath: Boolean(localPath) })
- return new Promise((resolve, reject) => {
- const task = uni.uploadFile({
- url: `${baseUrl}/files/loading-evidence/${encodeURIComponent(tenant)}/${encodeURIComponent(inboundId)}?evidenceType=${encodeURIComponent(evidenceType)}`,
+ debugLog('evidence.upload.start', { taskId, evidenceType, hasLocalPath: Boolean(localPath) })
+ const query = [
+ `warehouseId=${encodeURIComponent(warehouseId)}`,
+ `expectedVersion=${encodeURIComponent(payload.expectedVersion)}`,
+ `evidenceType=${encodeURIComponent(evidenceType)}`
+ ]
+ if (payload.sealNo) query.push(`sealNo=${encodeURIComponent(String(payload.sealNo).trim())}`)
+ if (payload.remark) query.push(`remark=${encodeURIComponent(payload.remark)}`)
+ return apiUpload({
+ url: `${baseUrl}/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}/evidence?${query.join('&')}`,
filePath: localPath,
name: 'file',
- header: headers,
- success(response) {
+ header: { 'Idempotency-Key': stableIdempotencyKey(payload.operationId || `evidence-${taskId}-${evidenceType}-${payload.expectedVersion}`) }
+ }, onProgress).then((response) => {
let body = response.data
try { body = typeof body === 'string' ? JSON.parse(body) : body } catch (_) { /* handled below */ }
const code = String(body?.code ?? response.statusCode)
- debugLog('evidence.upload.response', { inboundId, evidenceType, statusCode: response.statusCode, code, message: body?.message || '', 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'] || '', fileId: body?.data?.fileId || null })
- if (code !== '200') { const error = new Error(body?.message || '文件上传失败'); error.code = body?.code || response.statusCode; reject(error); return }
- resolve(body?.data || body)
- },
- fail(error) { debugLog('evidence.upload.error', { inboundId, evidenceType, message: error.errMsg || '' }); const next = new Error(error.errMsg || '网络连接失败'); next.network = true; reject(next) }
- })
- if (task && typeof task.onProgressUpdate === 'function' && typeof onProgress === 'function') task.onProgressUpdate(({ progress }) => onProgress(progress))
- })
-}
-
-export function completeLoading(inboundId, payload, tenantId) {
- return request({
- url: `/inbounds/${encodeURIComponent(inboundId)}/loading/complete?tenantId=${encodeURIComponent(tenantQuery(tenantId))}`,
- method: 'POST',
- header: { 'Idempotency-Key': idempotencyKey('complete-loading', inboundId) },
- data: payload
- })
+ debugLog('evidence.upload.response', { taskId, evidenceType, statusCode: response.statusCode, code, message: body?.message || '', 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'] || '', fileId: body?.data?.fileId || null })
+ if (code !== '200') {
+ const error = new Error(body?.message || '文件上传失败')
+ error.code = body?.code || response.statusCode
+ error.statusCode = response.statusCode
+ error.requestId = body?.requestId || response.header?.['X-Request-Id'] || response.header?.['x-request-id'] || ''
+ error.traceId = body?.traceId || response.header?.['X-Trace-Id'] || response.header?.['x-trace-id'] || ''
+ throw error
+ }
+ return body?.data || body
+ }).catch((error) => { debugLog('evidence.upload.error', { taskId, evidenceType, message: error.message || error.errMsg || '' }); throw error })
}
/** Complete the cabinet PDA task after the operator has captured all evidence. */
diff --git a/manifest.json b/manifest.json
index 87618a9..2453bee 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,95 +1,95 @@
{
- "name": "柏悦物流 PDA",
- "appid": "__UNI__9BFFAF2",
- "description": "柏悦物流 PDA 作业端",
- "versionName": "0.0.1",
- "versionCode": 1,
- "transformPx": false,
- "app-plus": {
- /* 5+App特有相关 */
- "usingComponents": true,
- "nvueCompiler": "uni-app",
- "nvueStyleCompiler": "uni-app",
- "splashscreen": {
- "alwaysShowBeforeRender": true,
- "waiting": true,
- "autoclose": true,
- "delay": 0
- },
- "modules": {
- "Barcode": {},
- "Bluetooth": {},
- "Camera": {},
- "Geolocation": {},
- "UIWebview": {}
- },
- /* 模块配置 */
- "distribute": {
- /* 应用发布信息 */
- "android": {
- /* android打包配置 */
- "permissions": []
- },
- "ios": {
- "dSYMs": false
- },
- /* ios打包配置 */
- "sdkConfigs": {
- "geolocation": {
- "system": {
- "__platform__": ["ios", "android"]
- }
- }
- },
- "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"
- }
- }
- }
- }
- },
- /* SDK配置 */
- "quickapp": {},
- /* 快应用特有相关 */
- "mp-weixin": {
- /* 小程序特有相关 */
- "appid": "",
- "setting": {
- "urlCheck": false
- },
- "usingComponents": true
- },
- "vueVersion": "3",
- "h5": {
- "devServer": {
- "port": 5175
- }
- }
+ "name" : "柏悦物流 PDA",
+ "appid" : "__UNI__9BFFAF2",
+ "description" : "柏悦物流 PDA 作业端",
+ "versionName" : "0.0.1",
+ "versionCode" : 1,
+ "transformPx" : false,
+ "app-plus" : {
+ /* 5+App特有相关 */
+ "usingComponents" : true,
+ "nvueCompiler" : "uni-app",
+ "nvueStyleCompiler" : "uni-app",
+ "splashscreen" : {
+ "alwaysShowBeforeRender" : true,
+ "waiting" : true,
+ "autoclose" : true,
+ "delay" : 0
+ },
+ "modules" : {
+ "Barcode" : {},
+ "Bluetooth" : {},
+ "Camera" : {},
+ "Geolocation" : {},
+ "UIWebview" : {}
+ },
+ /* 模块配置 */
+ "distribute" : {
+ /* 应用发布信息 */
+ "android" : {
+ /* android打包配置 */
+ "permissions" : []
+ },
+ "ios" : {
+ "dSYMs" : false
+ },
+ /* ios打包配置 */
+ "sdkConfigs" : {
+ "geolocation" : {
+ "system" : {
+ "__platform__" : [ "ios", "android" ]
+ }
+ }
+ },
+ "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"
+ }
+ }
+ }
+ }
+ },
+ /* SDK配置 */
+ "quickapp" : {},
+ /* 快应用特有相关 */
+ "mp-weixin" : {
+ /* 小程序特有相关 */
+ "appid" : "",
+ "setting" : {
+ "urlCheck" : false
+ },
+ "usingComponents" : true
+ },
+ "vueVersion" : "3",
+ "h5" : {
+ "devServer" : {
+ "port" : 5175
+ }
+ }
}
diff --git a/pages/history/history.vue b/pages/history/history.vue
index 3c88bd2..a40977b 100644
--- a/pages/history/history.vue
+++ b/pages/history/history.vue
@@ -1,7 +1,7 @@
- {{ overview.completedCount }}今日完成{{ overview.pendingBoxCount }}待处理最近作业▤暂无作业记录完成的现场任务会显示在这里✓集装箱拣货{{ entry.box.boxNo }} · {{ entry.box.orderNo }}已完成
+ {{ overview.completedCount }}今日完成{{ overview.pendingBoxCount }}待处理最近作业▤暂无作业记录完成的现场任务会显示在这里✓集装箱拣货 · {{ containerReference(entry.box) }}已完成
diff --git a/pages/home/home.vue b/pages/home/home.vue
index cfcc05a..fa74fef 100644
--- a/pages/home/home.vue
+++ b/pages/home/home.vue
@@ -8,8 +8,8 @@
@@ -21,7 +21,6 @@
import PortalHeader from '@/components/PortalHeader.vue'
import { MODULES } from '@/data/demo-data.js'
import { session, requireSession } from '@/stores/session.js'
- import { getPickingOverview } from '@/stores/picking.js'
const ICON_TYPES = {
'到货收货': 'download', '商品上架': 'upload', '入库质检': 'checkbox',
@@ -39,8 +38,8 @@
export default {
components: { PortalHeader },
- data() { return { user: session.currentUser, groups: HOME_GROUPS, overview: getPickingOverview(), menuOpen: false } },
- onShow() { if (!requireSession()) return; this.user = session.currentUser; this.overview = getPickingOverview() },
+ data() { return { user: session.currentUser, groups: HOME_GROUPS, menuOpen: false } },
+ onShow() { if (!requireSession()) return; this.user = session.currentUser },
onBackPress() {
if (!this.menuOpen) return false
this.$refs.portalHeader.closeMenu()
diff --git a/pages/login/login.vue b/pages/login/login.vue
index adb5acf..626277a 100644
--- a/pages/login/login.vue
+++ b/pages/login/login.vue
@@ -7,6 +7,7 @@
工号 / 用户名
密码
+ 记住账号
{{ error }}
@@ -15,12 +16,33 @@
diff --git a/pages/tasks/tasks.vue b/pages/tasks/tasks.vue
index d11e74d..37cf4de 100644
--- a/pages/tasks/tasks.vue
+++ b/pages/tasks/tasks.vue
@@ -5,24 +5,22 @@
待处理任务
- {{ loading ? '正在同步' : `接口返回 ${rawTaskCount} 个,可处理 ${tasks.length} 个` }}
- {{ apiEnvironment }}
+ {{ loading ? '正在同步任务' : `共 ${tasks.length} 个待处理任务` }}
{{ error }}
正在加载真实任务...
- 暂无可领取的拣货任务。请确认预装已补充集装箱号、已关联放舱订舱且预装已确认。
+ 暂无待处理的拣货任务
{{ notice }}
-