This commit is contained in:
2026-08-26 15:24:51 +08:00
parent 312519f1ab
commit ee83d87a85
8 changed files with 53 additions and 12 deletions
+8 -2
View File
@@ -1,5 +1,11 @@
VITE_AUTH_API_BASE_URL=/api
VITE_OVERSEAS_API_BASE_URL=/api/overseas
VITE_AUTH_API_BASE_URL=http://127.0.0.1:9000/api
VITE_OVERSEAS_API_BASE_URL=http://127.0.0.1:9300/api
# H5 同源前缀,由 Vite 代理转发;App 构建使用上面的绝对域名直连。
VITE_H5_AUTH_API_BASE_URL=/api
VITE_H5_OVERSEAS_API_BASE_URL=/overseas-api
# H5 开发代理上游;正式 App 构建请在本机环境文件中改为可访问的绝对 API 根地址。
API_PROXY_TARGET=http://127.0.0.1:9000
OVERSEAS_API_PROXY_TARGET=http://127.0.0.1:9300
# 仅开发服务器自动预填;请在不提交的 .env.local 中填写真实开发账号。
VITE_PDA_DEV_USERNAME=
VITE_PDA_DEV_PASSWORD=
+3 -1
View File
@@ -25,7 +25,9 @@ export function normalizeReceivingCandidate(value = {}) {
export function normalizeReceivingSession(value) {
if (!value) return null
const photos = Array.isArray(value.photos) ? value.photos : []
const photos = Array.isArray(value.photos)
? value.photos.map((photo) => ({ ...photo, fileId: String(photo.fileId) }))
: []
return {
...value,
photos: RECEIVING_PHOTO_ROLES.map((role) => photos.find((photo) => photo?.role === role)).filter(Boolean),
+11 -2
View File
@@ -3,8 +3,17 @@ const configuredAuthApiRoot = runtimeEnvironment.VITE_AUTH_API_BASE_URL || ''
const configuredOverseasApiRoot = runtimeEnvironment.VITE_OVERSEAS_API_BASE_URL || ''
const development = Boolean(runtimeEnvironment.DEV)
export const API_ROOT = String(configuredAuthApiRoot || '/api').replace(/\/$/, '')
export const OVERSEAS_API_ROOT = String(configuredOverseasApiRoot || '/api/overseas').replace(/\/$/, '')
let selectedAuthApiRoot = configuredAuthApiRoot || '/api'
let selectedOverseasApiRoot = configuredOverseasApiRoot || '/overseas-api'
// H5 must stay same-origin and use Vite's CORS-safe proxy; App builds keep the absolute API roots above.
// #ifdef H5
selectedAuthApiRoot = runtimeEnvironment.VITE_H5_AUTH_API_BASE_URL || '/api'
selectedOverseasApiRoot = runtimeEnvironment.VITE_H5_OVERSEAS_API_BASE_URL || '/overseas-api'
// #endif
export const API_ROOT = String(selectedAuthApiRoot).replace(/\/$/, '')
export const OVERSEAS_API_ROOT = String(selectedOverseasApiRoot).replace(/\/$/, '')
export const DEV_LOGIN = Object.freeze({
username: development ? String(runtimeEnvironment.VITE_PDA_DEV_USERNAME || '') : '',
password: development ? String(runtimeEnvironment.VITE_PDA_DEV_PASSWORD || '') : ''
+1 -1
View File
@@ -165,7 +165,7 @@
cancelDetail() { this.detailOpen = false; receiving.selected = null },
async prepareContainer() { if (!this.selected?.containerNo) return; const key = idempotencyKey('prepare', { containerNo: this.selected.containerNo }); try { const value = await this.run('选择当前柜', () => prepareReceivingContainer(this.selected.containerNo, { idempotencyKey: key.value })); releaseIdempotencyKey(key.signature); setCurrentReceiving(value); this.setScanTargetForCurrent(value); this.detailOpen = false } catch (error) { await this.recoverCurrentAfterConflict(error) } },
choosePhoto(index) { if (this.busy) return; uni.chooseImage({ count: 1, sourceType: ['camera', 'album'], success: async ({ tempFilePaths }) => { const path = tempFilePaths?.[0]; if (!path) return; try { const file = await this.run('上传签收照片', () => uploadCurrentUnloadPhoto(path)); receiving.photoSlots.splice(index, 1, { ...file, fileId: String(file.fileId), role: this.photoSteps[index].role, localPath: path, uploaded: true }) } catch (_) {} } }) },
async signContainer() { if (!this.photosReady) return; const photos = this.photoSlots.map((item, index) => ({ role: this.photoSteps[index].role, fileId: Number(item.fileId) })); const key = idempotencyKey('sign', { containerNo: this.current.containerNo, photos }); try { const value = await this.run('签收当前柜', () => signCurrentReceiving({ photos, idempotencyKey: key.value })); releaseIdempotencyKey(key.signature); setCurrentReceiving(value); receiving.activeScanTarget = selectedInbound(value)?.destinationType === 'OVERSEAS' ? 'carton' : 'sku' } catch (_) {} },
async signContainer() { if (!this.photosReady) return; const photos = this.photoSlots.map((item, index) => ({ role: this.photoSteps[index].role, fileId: String(item.fileId) })); const key = idempotencyKey('sign', { containerNo: this.current.containerNo, photos }); try { const value = await this.run('签收当前柜', () => signCurrentReceiving({ photos, idempotencyKey: key.value })); releaseIdempotencyKey(key.signature); setCurrentReceiving(value); receiving.activeScanTarget = selectedInbound(value)?.destinationType === 'OVERSEAS' ? 'carton' : 'sku' } catch (_) {} },
chooseInbound(item) { receiving.selectedInboundCode = item.inboundCode; receiving.resolvedCarton = null; receiving.resolvedSku = null; receiving.resolvedPallet = null; this.cartonInput = ''; this.skuInput = ''; this.palletInput = ''; this.packageCount = Math.max(1, Number(item.pendingPackages || 1)); receiving.activeScanTarget = item.destinationType === 'OVERSEAS' ? 'carton' : 'sku' },
async resolveCarton(raw) { const cartonMark = String(raw?.detail?.value ?? raw ?? this.cartonInput).trim(); if (!cartonMark || this.busy) return; try { const value = await this.run('解析箱唛', () => resolveCurrentCarton(cartonMark)); receiving.resolvedCarton = value; receiving.selectedInboundCode = value.inboundCode; receiving.resolvedPallet = null; this.cartonInput = value.cartonMark; this.palletInput = ''; receiving.activeScanTarget = 'pallet'; playSuccessSound() } catch (_) {} },
async resolveSku(raw) { const skuCode = String(raw?.detail?.value ?? raw ?? this.skuInput).trim(); if (!skuCode || this.busy) return; try { const value = await this.run('解析 SKU', () => resolveCurrentSku(skuCode)); receiving.resolvedSku = value; receiving.selectedInboundCode = value.inboundCode; receiving.resolvedPallet = null; this.skuInput = value.skuCode; this.palletInput = ''; receiving.activeScanTarget = 'pallet'; playSuccessSound() } catch (_) {} },
+1 -1
View File
@@ -142,7 +142,7 @@ export function restoreSession() {
const pair = validatePdaTokenPair(saved.domesticToken, saved.token)
Object.assign(session, saved, {
currentUser: userFromClaims(pair), tenantId: claimId(pair.domestic, 'tenant_id'),
apiBase: saved.apiBase || OVERSEAS_API_ROOT
apiBase: OVERSEAS_API_ROOT
})
return true
} catch (_) {
+9 -2
View File
@@ -5,14 +5,18 @@ import { readFile } from 'node:fs/promises'
const read = (path) => readFile(new URL(path, import.meta.url), 'utf8')
test('overseas PDA is an independent Vue 3 app with separate packaging identity', async () => {
const [manifest, pages, pkg] = await Promise.all([
read('../src/manifest.json'), read('../src/pages.json'), read('../package.json')
const [manifest, pages, pkg, vite] = await Promise.all([
read('../src/manifest.json'), read('../src/pages.json'), read('../package.json'), read('../vite.config.js')
])
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/)
assert.match(pkg, /"build:app"\s*:\s*"uni build -p app"/)
assert.match(vite, /API_PROXY_TARGET/)
assert.match(vite, /OVERSEAS_API_PROXY_TARGET/)
assert.match(vite, /\/overseas-api/)
assert.match(vite, /replace\(\/\^\\\/overseas-api\//)
})
test('overseas workbench contains only confirmed inbound and in-warehouse domains', async () => {
@@ -34,6 +38,9 @@ test('login has no committed demo credentials and inbound operation implements t
assert.doesNotMatch(login, /ChangeMe|platform_admin/)
assert.match(login, /DEV_LOGIN\.username|DEV_LOGIN\.password/)
assert.match(runtime, /runtimeEnvironment\.DEV|VITE_PDA_DEV_USERNAME|VITE_PDA_DEV_PASSWORD/)
assert.match(runtime, /'\/overseas-api'/)
assert.match(runtime, /#ifdef H5[\s\S]*?VITE_H5_AUTH_API_BASE_URL/)
assert.match(runtime, /#ifdef H5[\s\S]*?VITE_H5_OVERSEAS_API_BASE_URL/)
assert.match(operation, /startScanBroadcast/)
for (const action of ['prepareReceivingContainer', 'getCurrentReceiving', 'resolveCurrentSku', 'resolveCurrentCarton', 'resolveCurrentPallet', 'receiveCurrentBulk', 'receiveCurrentCarton', 'completeCurrentReceiving']) assert.match(operation, new RegExp(action))
for (const path of ['/candidates', '/prepare', '/unload-photos/files', '/sign', '/skus/resolve', '/cartons/resolve', '/pallets/resolve', '/receipts/bulk', '/receipts/carton', '/complete']) assert.match(inbound, new RegExp(path.replaceAll('/', '\\/')))
+3 -1
View File
@@ -3,12 +3,14 @@ import assert from 'node:assert/strict'
import { isNoCurrentReceiving, isReceivingConflict, normalizeReceivingCandidate, normalizeReceivingSession, RECEIVING_PHOTO_ROLES } from '../src/api/receiving-adapter.js'
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: 1 }, { role: 'CONTAINER_SEAL', fileId: 2 }],
photos: [{ role: 'DOOR_OPEN', fileId: '3' }, { role: 'LICENSE_PLATE', fileId: snowflakeFileId }, { role: 'CONTAINER_SEAL', fileId: '2' }],
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.inbounds[0].plannedPalletCount, 2)
})
+17 -2
View File
@@ -1,8 +1,22 @@
import { defineConfig } from 'vite'
import { defineConfig, loadEnv } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
export default defineConfig({
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
const domesticTarget = env.API_PROXY_TARGET || 'http://127.0.0.1:9000'
const overseasTarget = env.OVERSEAS_API_PROXY_TARGET || 'http://127.0.0.1:9300'
return {
plugins: [uni()],
server: {
proxy: {
'/api': { target: domesticTarget, changeOrigin: true },
'/overseas-api': {
target: overseasTarget,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/overseas-api/, '/api')
}
}
},
css: {
preprocessorOptions: {
scss: {
@@ -18,4 +32,5 @@ export default defineConfig({
}
}
}
}
})