From 083299602dcab3c2393d1a6fbf6c2db44ee4833e Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 26 Aug 2026 16:50:25 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 2 + AGENTS.md | 7 +- README.md | 25 ++- src/App.vue | 10 +- src/api/inbound.js | 2 +- src/components/PopupHeader.vue | 2 +- src/components/PortalHeader.vue | 14 +- src/components/ReceivingSessionSummary.vue | 46 +++++ src/components/ScanTargetInput.vue | 8 +- src/data/overseas-workbench.js | 28 +-- src/i18n/index.js | 47 +++++ src/i18n/locales/en.js | 18 ++ src/i18n/locales/es.js | 18 ++ src/i18n/locales/zh-CN.js | 23 +++ src/main.js | 2 + src/pages/history/history.vue | 4 +- src/pages/home/home.vue | 48 ++++- src/pages/login/login.vue | 16 +- src/pages/module/module.vue | 17 +- src/pages/operation/operation.vue | 219 ++++++++++----------- src/pages/profile/profile.vue | 45 ++++- src/pages/scanner-test/scanner-test.vue | 28 +-- src/stores/receiving.js | 14 +- src/stores/session.js | 31 ++- src/styles/pda.scss | 65 ++++-- src/utils/app-update.js | 191 ++++++++++++++++++ tests/app-update.test.mjs | 32 +++ tests/project-contract.test.mjs | 51 ++++- tests/receiving-photo-ui-contract.test.mjs | 23 ++- tests/session-auth.test.mjs | 14 +- vite.config.js | 5 + 31 files changed, 816 insertions(+), 239 deletions(-) create mode 100644 src/components/ReceivingSessionSummary.vue create mode 100644 src/i18n/index.js create mode 100644 src/i18n/locales/en.js create mode 100644 src/i18n/locales/es.js create mode 100644 src/i18n/locales/zh-CN.js create mode 100644 src/utils/app-update.js create mode 100644 tests/app-update.test.mjs diff --git a/.env.example b/.env.example index 973c142..df31f09 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,8 @@ 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 +# 海外 PDA 独立更新清单地址;不得与国内 PDA 发布记录混用。 +VITE_PDA_UPDATE_URL=https://updates.example.com/pda/overseas/latest # 仅开发服务器自动预填;请在不提交的 .env.local 中填写真实开发账号。 VITE_PDA_DEV_USERNAME= VITE_PDA_DEV_PASSWORD= diff --git a/AGENTS.md b/AGENTS.md index 56bd78b..7e37c76 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 逐箱收货并逐入仓码核平;库内、换标与查询入口在海外 PDA 专用 Controller、权限和 Token 冻结仓库范围契约齐备前必须禁用,不得调用平台/客户/内部接口或模拟成功。 +- 当前通用作业页中的入仓作业已接柜号级真实海外 API:账号锁柜、三类照片签收、统一箱码定位入仓码、FBA 按箱数收货、OVERSEAS 逐箱收货并逐入仓码核平;收货页不预列柜内全部入仓码,也不要求作业员扫描 SKU;库内、换标与查询入口在海外 PDA 专用 Controller、权限和 Token 冻结仓库范围契约齐备前必须禁用,不得调用平台/客户/内部接口或模拟成功。 ## 页面与能力快速索引 @@ -36,7 +36,8 @@ | 页面注册与启动 | `src/pages.json`、`src/main.js`、`src/App.vue` | `src/manifest.json`、全局样式与应用生命周期 | | 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/home/home.vue`、`src/pages/module/module.vue` | `src/data/overseas-workbench.js` 是模块与作业可用性入口;首页通过 `/api/pda/v1/last-leg/receiving-containers/warehouse-profile` 读取 Token 绑定仓的中文主档名称 | +| 中英西多语言 | `src/i18n/index.js`、`src/i18n/locales/`、`src/pages/profile/profile.vue` | 语言仅允许 `zh-CN`、`en`、`es`,首次跟随设备且本地持久化;业务配置使用稳定 key,不以翻译后的名称驱动流程 | | 柜号签收、照片与入仓真实作业 | `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 见本文件服务边界 | @@ -45,7 +46,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` 统一处理。 +- `src/api/inbound.js` 已接入待卸柜候选、柜详情/prepare、current 恢复、三类照片签收、统一箱码定位入仓码、实物托盘解析、FBA bulk/OVERSEAS carton 收货及整柜完成;FBA bulk 不提交 `skuCode`,库存分配由服务端按计划明细完成;响应与 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。 diff --git a/README.md b/README.md index 374a660..26da7ad 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,31 @@ npm run build:app - 已复用并内置:登录壳、PDA 样式、扫码广播、声音反馈、导航、网络检测、图标及 `uni_modules`。 - 已隔离:国内集装箱拣货、装柜证据、国内 Firstleg API、国内任务状态和国内本地草稿。 -- 入仓页面已接待卸柜候选、柜详情与账号锁柜、三类角色照片签收、current 恢复、SKU/箱唛与既有物理托盘解析、FBA 按 SKU 和箱数收货、OVERSEAS 逐箱收货、逐入仓码核平及整柜完成的真实 API;409 会引导返回当前柜,错误会保留业务码、requestId 与 traceId。 +- 入仓页面已接待卸柜候选、柜详情与账号锁柜、三类角色照片签收、current 恢复、统一箱码与既有物理托盘解析、FBA 按箱数和 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 真机验收。 + +## App 在线更新 + +Android App 启动后会读取 `VITE_PDA_UPDATE_URL` 并检查海外 PDA 的独立更新通道;H5 不执行该逻辑。更新服务接收 `appid`、`platform`、`appVersion`、`appVersionCode`、`wgtVersion` 查询参数,无更新返回 `{ "update": false }`,有更新返回: + +```json +{ + "update": true, + "packageType": "wgt", + "versionName": "0.0.2", + "versionCode": 2, + "downloadUrl": "https://updates.example.com/pda/overseas/0.0.2.wgt", + "mandatory": true, + "releaseNotes": "本次更新内容" +} +``` + +`packageType` 只允许 `apk` 或 `wgt`。`mandatory=true` 时员工不能跳过,并在下载或安装失败后提供重试;wgt 安装完成会立即重启,APK 交由 Android 安装器完成覆盖安装。正式发布要求: + +1. 海外 PDA 每次整包发布同时递增 `src/manifest.json` 的 `versionName` 与整数 `versionCode`,使用海外独立 AppID、包名和签名打包 APK。 +2. wgt 发布至少递增 `versionName`,并使用与已安装原生包兼容的 uni-app 编译器生成;新增原生模块、权限或不兼容运行时能力时必须发布 APK,不能只发 wgt。 +3. 更新清单与下载地址使用 HTTPS,先在测试设备验证可选更新、强制更新、断网重试、APK 覆盖安装、wgt 重启及扫码作业回归,再切换正式清单。 +4. 海外和国内更新记录、文件目录及发布权限保持隔离,服务端必须按请求中的 `appid` 拒绝跨应用更新包。 diff --git a/src/App.vue b/src/App.vue index bee3444..f5ee245 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,13 @@ diff --git a/src/components/ScanTargetInput.vue b/src/components/ScanTargetInput.vue index b526121..72058fd 100644 --- a/src/components/ScanTargetInput.vue +++ b/src/components/ScanTargetInput.vue @@ -15,13 +15,13 @@ class="scan-target-value" :value="modelValue" :placeholder="placeholder" - :aria-label="`${label},支持扫码或人工输入`" + :aria-label="label" confirm-type="done" @focus="$emit('activate', name)" @input="handleInput" @confirm="handleConfirm" /> - 当前扫码 + {{ $t('scanInput.active') }} @@ -31,8 +31,8 @@ props: { name: { type: String, required: true }, modelValue: { type: String, default: '' }, - label: { type: String, default: '扫码输入框' }, - placeholder: { type: String, default: '扫描或人工输入' }, + label: { type: String, default: '' }, + placeholder: { type: String, default: '' }, active: Boolean }, methods: { diff --git a/src/data/overseas-workbench.js b/src/data/overseas-workbench.js index 37ebbb7..7ced9d5 100644 --- a/src/data/overseas-workbench.js +++ b/src/data/overseas-workbench.js @@ -1,21 +1,21 @@ export const MODULES = [ - { key: 'inbound', name: '入仓作业', icon: '入', description: '单账号锁定一柜,完成柜号级入库闭环', operations: [ - { name: '拆柜收货', icon: '卸', iconType: 'download', description: '三类照片签收,FBA 按 SKU 箱数、海外仓逐箱收货', available: true } + { key: 'inbound', name: '入仓作业', nameKey: 'modules.inbound.name', icon: '入', description: '单账号锁定一柜,完成柜号级入库闭环', descriptionKey: 'modules.inbound.description', operations: [ + { key: 'receiving', name: '拆柜收货', nameKey: 'modules.receiving.name', icon: '卸', iconType: 'download', description: '三类照片签收,箱码定位入仓码后按目的类型收货', descriptionKey: 'modules.receiving.description', available: true } ] }, - { key: 'inventory', name: '库内作业', icon: '库', description: '盘点、移库与托盘管理', operations: [ - { name: '库存盘点', icon: '盘', iconType: 'list', description: '按任务扫描库位和库存', available: false }, - { name: '商品移库', icon: '移', iconType: 'loop', description: '扫描来源与目标库位完成移库', available: false }, - { name: '托盘管理', icon: '托', iconType: 'grid', description: '组托、拆托及托盘状态核对', available: false } + { key: 'inventory', name: '库内作业', nameKey: 'modules.inventory.name', icon: '库', description: '盘点、移库与托盘管理', descriptionKey: 'modules.inventory.description', operations: [ + { key: 'stocktake', name: '库存盘点', nameKey: 'modules.stocktake.name', icon: '盘', iconType: 'list', description: '按任务扫描库位和库存', descriptionKey: 'modules.stocktake.description', available: false }, + { key: 'move', name: '商品移库', nameKey: 'modules.move.name', icon: '移', iconType: 'loop', description: '扫描来源与目标库位完成移库', descriptionKey: 'modules.move.description', available: false }, + { key: 'pallet', name: '托盘管理', nameKey: 'modules.pallet.name', icon: '托', iconType: 'map', description: '组托、拆托及托盘状态核对', descriptionKey: 'modules.pallet.description', available: false } ] }, - { key: 'relabel', name: '换标作业', icon: '换', description: '换标收货、执行与复核', operations: [ - { name: '换标收货', icon: '收', iconType: 'download', description: '扫描换标单并确认待处理库存', available: false }, - { name: '换标执行', icon: '换', iconType: 'compose', description: '扫描旧标和新标记录换标结果', available: false }, - { name: '换标复核', icon: '核', iconType: 'checkbox', description: '复核标签和商品关联关系', available: false } + { key: 'relabel', name: '换标作业', nameKey: 'modules.relabel.name', icon: '换', description: '换标收货、执行与复核', descriptionKey: 'modules.relabel.description', operations: [ + { key: 'relabelReceive', name: '换标收货', nameKey: 'modules.relabelReceive.name', icon: '收', iconType: 'download', description: '扫描换标单并确认待处理库存', descriptionKey: 'modules.relabelReceive.description', available: false }, + { key: 'relabelExecute', name: '换标执行', nameKey: 'modules.relabelExecute.name', icon: '换', iconType: 'compose', description: '扫描旧标和新标记录换标结果', descriptionKey: 'modules.relabelExecute.description', available: false }, + { key: 'relabelReview', name: '换标复核', nameKey: 'modules.relabelReview.name', icon: '核', iconType: 'checkbox', description: '复核标签和商品关联关系', descriptionKey: 'modules.relabelReview.description', available: false } ] }, - { key: 'query', name: '现场查询', icon: '查', description: '库存、库位与托盘查询', operations: [ - { name: '库存查询', icon: '存', iconType: 'search', description: '扫描商品或箱码查询可用库存', available: false }, - { name: '库位查询', icon: '位', iconType: 'location', description: '扫描库位查看容量和在库明细', available: false }, - { name: '托盘查询', icon: '托', iconType: 'flag', description: '扫描托盘码查看绑定库存', available: false } + { key: 'query', name: '现场查询', nameKey: 'modules.query.name', icon: '查', description: '库存、库位与托盘查询', descriptionKey: 'modules.query.description', operations: [ + { key: 'inventoryQuery', name: '库存查询', nameKey: 'modules.inventoryQuery.name', icon: '存', iconType: 'search', description: '扫描商品或箱码查询可用库存', descriptionKey: 'modules.inventoryQuery.description', available: false }, + { key: 'locationQuery', name: '库位查询', nameKey: 'modules.locationQuery.name', icon: '位', iconType: 'location', description: '扫描库位查看容量和在库明细', descriptionKey: 'modules.locationQuery.description', available: false }, + { key: 'palletQuery', name: '托盘查询', nameKey: 'modules.palletQuery.name', icon: '托', iconType: 'flag', description: '扫描托盘码查看绑定库存', descriptionKey: 'modules.palletQuery.description', available: false } ] } ] diff --git a/src/i18n/index.js b/src/i18n/index.js new file mode 100644 index 0000000..8668cf3 --- /dev/null +++ b/src/i18n/index.js @@ -0,0 +1,47 @@ +import { createI18n } from 'vue-i18n' +import zhCN from './locales/zh-CN.js' +import en from './locales/en.js' +import es from './locales/es.js' + +const LOCALE_KEY = 'overseas-pda-locale' +export const SUPPORTED_LOCALES = ['zh-CN', 'en', 'es'] +export const LOCALE_OPTIONS = [ + { value: 'zh-CN', labelKey: 'language.zh' }, + { value: 'en', labelKey: 'language.en' }, + { value: 'es', labelKey: 'language.es' } +] + +function normalizeLocale(value) { + const locale = String(value || '').toLowerCase() + if (locale.startsWith('zh')) return 'zh-CN' + if (locale.startsWith('es')) return 'es' + if (locale.startsWith('en')) return 'en' + return 'en' +} + +function initialLocale() { + if (typeof uni === 'undefined') return 'zh-CN' + const saved = typeof uni.getStorageSync === 'function' ? uni.getStorageSync(LOCALE_KEY) : '' + if (SUPPORTED_LOCALES.includes(saved)) return saved + return normalizeLocale(typeof uni.getLocale === 'function' ? uni.getLocale() : '') +} + +export const i18n = createI18n({ + legacy: false, + globalInjection: true, + locale: initialLocale(), + fallbackLocale: 'zh-CN', + messages: { 'zh-CN': zhCN, en, es } +}) + +export function currentLocale() { return i18n.global.locale.value } +export function t(key, params) { return i18n.global.t(key, params) } +export function setLocale(locale) { + const value = SUPPORTED_LOCALES.includes(locale) ? locale : normalizeLocale(locale) + i18n.global.locale.value = value + if (typeof uni !== 'undefined') { + if (typeof uni.setStorageSync === 'function') uni.setStorageSync(LOCALE_KEY, value) + if (typeof uni.setLocale === 'function') uni.setLocale(value) + } + return value +} diff --git a/src/i18n/locales/en.js b/src/i18n/locales/en.js new file mode 100644 index 0000000..5d82d09 --- /dev/null +++ b/src/i18n/locales/en.js @@ -0,0 +1,18 @@ +export default { + receivingSummary: { container: 'Current container', warehouse: 'Warehouse', destinations: 'Destinations', inboundCodes: 'Inbound codes', plannedPallets: 'Planned pallets', plannedBoxes: 'Planned cartons', pendingBoxes: 'Pending cartons' }, + scanInput: { active: 'Scan target', label: 'Scan input', placeholder: 'Scan or enter manually' }, + operationPhase: { candidates: { title: 'Containers awaiting unload', subtitle: 'Select a container and take 3 photos to sign it in' }, detail: { title: 'Container details', subtitle: 'Verify the container and total planned cartons' }, sign: { title: 'Photo sign-off', subtitle: 'Capture on-site proof before receiving' }, receive: { title: 'Current container', subtitle: 'Scan a carton to identify its inbound code' }, completed: { title: 'Inbound completed', subtitle: 'Inventory and container totals are reconciled by the server' } }, + common: { close: 'Close', cancel: 'Cancel', confirm: 'Confirm', refresh: 'Refresh', back: 'Back', loading: 'Loading…', boxes: 'ctns', pallets: 'pallets', plans: 'plans', inboundCodes: 'inbound codes', available: 'Ready', unavailable: 'Unavailable', required: 'Required', uploaded: 'Uploaded', retry: 'Try again', currentWarehouse: 'Current warehouse', warehouseFallback: 'Assigned automatically to this account', warehouseOperator: 'Warehouse operator' }, + app: { title: 'Baiyue Logistics Overseas', workbench: 'Overseas Warehouse', subtitle: 'Warehouse Operations System' }, + login: { username: 'Employee ID / Username', usernamePlaceholder: 'Enter your employee ID', password: 'Password', passwordPlaceholder: 'Enter your password', submit: 'Sign in', submitting: 'Signing in...', usernameRequired: 'Enter your employee ID', passwordRequired: 'Enter your password', offline: 'No network connection. Connect and try again.', failed: 'Sign-in failed' }, + nav: { menu: 'Menu', history: 'Work history', profile: 'Profile', openMenu: 'Open menu', back: 'Go back' }, + home: { eyebrow: 'OVERSEAS WAREHOUSE', actionable: 'ready', night: 'Stay safe on the night shift', morning: 'Good morning', afternoon: 'Good afternoon', evening: 'Good evening' }, + module: { title: 'Operations', functions: 'Functions', hint: 'Tap an available function to begin', availability: '{available}/{total} ready', onlyReceiving: 'Only container receiving is available. Other operations are not yet open.' }, + modules: { inbound: { name: 'Inbound', description: 'Lock one container per account and complete receiving' }, inventory: { name: 'Warehouse', description: 'Stocktake, moves, and pallet operations' }, relabel: { name: 'Relabeling', description: 'Receive, execute, and review relabel work' }, query: { name: 'Lookup', description: 'Inventory, location, and pallet lookup' }, receiving: { name: 'Container receiving', description: 'Capture three photos; receive FBA by count and overseas cartons individually' }, stocktake: { name: 'Stocktake', description: 'Scan locations and stock by task' }, move: { name: 'Stock move', description: 'Scan source and destination locations' }, pallet: { name: 'Pallet management', description: 'Build, split, and verify pallets' }, relabelReceive: { name: 'Relabel receiving', description: 'Scan a relabel order and confirm stock' }, relabelExecute: { name: 'Relabel execution', description: 'Scan old and new labels' }, relabelReview: { name: 'Relabel review', description: 'Verify labels and product links' }, inventoryQuery: { name: 'Inventory lookup', description: 'Scan an item or carton to view stock' }, locationQuery: { name: 'Location lookup', description: 'Scan a location to view capacity and stock' }, palletQuery: { name: 'Pallet lookup', description: 'Scan a pallet to view linked stock' } }, + profile: { title: 'Profile', warehouse: 'Work warehouse', warehousePending: 'Loading warehouse profile', shift: 'Current shift', shiftValue: 'Based on the warehouse roster', version: 'App version', language: 'Language', settings: 'Settings', settingsLater: 'Settings will be available in a future version', logout: 'Sign out', logoutTitle: 'Sign out?', logoutDescription: 'Signing out clears the local overseas PDA session. Unsubmitted work cannot be retained across accounts.' }, + language: { zh: '中文', en: 'English', es: 'Español' }, + receivingSummary: { container: 'Current container', inboundCodes: 'Inbound codes', plannedBoxes: 'Planned cartons', pendingBoxes: 'Pending cartons' }, + history: { title: 'Work history', completedToday: 'Completed today', inProgress: 'In progress', recent: 'Recent work', emptyTitle: 'No overseas warehouse records', emptyDescription: 'Completed work will appear here when live task history is connected.' }, + scanner: { title: 'Scanner broadcast test', waitingTitle: 'Receiver started', waitingDetail: 'Waiting for a hardware scan', receivedTitle: 'Scan broadcast received', receivedDetail: 'Scan again to verify continuous reception', errorTitle: 'Receiver failed to start', errorDetail: 'Check the PDA scanner settings and reopen this page', unsupportedTitle: 'Broadcast not supported here', unsupportedDetail: 'Test on an Android PDA app', latest: 'Latest value', waiting: 'Waiting for scan', hardwareHint: 'Press the PDA hardware scan key', count: 'Scan {count} on this page', extra: 'Extra key', records: 'Received scans', clear: 'Clear', empty: 'No broadcast data' }, + operation: { title: 'Container receiving', restoring: 'Restoring the current container…', scanContainer: 'Scan container number', container: 'Container number', containerPlaceholder: 'Scan or enter a container number', queryContainer: 'Find container', pendingContainers: '{count} containers awaiting unload', emptyContainers: 'No containers awaiting unload', destination: 'Destination: {value}', receivingProgress: 'Receiving progress', continueCurrent: 'Continue current container', viewDetail: 'View details', lockedNotice: 'After selection, this account is locked to the container until completion.', backList: 'Back to list', selectContainer: 'Select container and take photos', unloadPhotos: 'Unload proof photos', photoHint: 'Take one photo for each of the three required slots', notAdded: 'Not added', uploading: 'Uploading…', retakeFailedKept: 'Retake failed; original kept', uploadFailed: 'Upload failed. Try again.', previewPhoto: 'Tap the photo to preview', takePhotoHint: 'Take an on-site photo', retake: 'Retake', takePhoto: 'Take photo', signAndReceive: 'Confirm arrival and start receiving', scanCartonStep: '1. Scan a carton to identify the inbound code', carton: 'Carton mark', parseCarton: 'Resolve carton', fbaMode: 'FBA receiving by carton count', overseasMode: 'Overseas receiving by carton', totalBoxes: '{count} cartons total', receivedBoxes: '{count} received', pendingBoxes: '{count} pending', scanPalletStep: '2. Scan the physical pallet', pallet: 'Pallet code', palletPlaceholder: 'Scan or enter a pallet code', verifyPallet: 'Verify pallet', palletMatched: 'Destination matches; ready to receive', palletMismatch: 'Pallet does not match', inputCountStep: '3. Enter carton count', maxCount: 'Up to {count} cartons', confirmBulk: 'Confirm carton-count receipt', confirmCarton: 'Confirm this carton', receiptSuccess: '{accepted} received; {inbound} has {pending} remaining', completeReady: 'All inbound codes reconciled — complete container', incomplete: '{count} cartons still pending', completedTitle: 'Container receiving completed', actualReceipt: '{received} / {total} cartons received', returnPending: 'Return to pending containers', unavailableSubtitle: 'This function is not available yet', unavailableTitle: 'Function unavailable', unavailableDescription: 'Warehouse operators cannot call platform, customer, or internal APIs. Scans are not submitted and no simulated success is produced.', lockedContainer: 'Container locked to this account', inbounds: 'Inbound codes in container', plannedPallets: '{count} planned pallets', photoLicense: 'License plate', photoSeal: 'Container seal', photoDoor: 'Open-door view', statusWait: 'Awaiting sign-off', statusSigned: 'Signed', statusReceiving: 'Receiving', statusCompleted: 'Completed', actionSuccess: '{action} succeeded', actionFailed: '{action} failed', photoLoadFailed: 'Photo failed to load', scanUnavailable: 'This function has no dedicated overseas PDA API; scan not submitted', currentConflict: 'Account already has unfinished container {container}; returned to current work', actions: { refresh: 'Refresh pending containers', query: 'Find container', select: 'Select container', sign: 'Sign container', carton: 'Resolve carton', sku: 'Resolve SKU', pallet: 'Verify pallet', receipt: 'Submit receipt', complete: 'Complete receiving' } } +} diff --git a/src/i18n/locales/es.js b/src/i18n/locales/es.js new file mode 100644 index 0000000..34e0e08 --- /dev/null +++ b/src/i18n/locales/es.js @@ -0,0 +1,18 @@ +export default { + receivingSummary: { container: 'Contenedor actual', inboundCodes: 'Códigos de entrada', plannedBoxes: 'Cajas planificadas', pendingBoxes: 'Cajas pendientes' }, + scanInput: { active: 'Destino del escáner', label: 'Entrada de escaneo', placeholder: 'Escanee o ingrese manualmente' }, + operationPhase: { candidates: { title: 'Contenedores por descargar', subtitle: 'Seleccione un contenedor y tome 3 fotos para firmar' }, detail: { title: 'Detalles del contenedor', subtitle: 'Verifique el contenedor y el total de cajas planificadas' }, sign: { title: 'Firma con fotos', subtitle: 'Capture la evidencia antes de recibir' }, receive: { title: 'Contenedor actual', subtitle: 'Escanee una caja para identificar su entrada' }, completed: { title: 'Entrada completada', subtitle: 'El servidor ha conciliado el inventario y el contenedor' } }, + common: { close: 'Cerrar', cancel: 'Cancelar', confirm: 'Confirmar', refresh: 'Actualizar', back: 'Volver', loading: 'Cargando…', boxes: 'cajas', pallets: 'palés', plans: 'planes', inboundCodes: 'códigos de entrada', available: 'Disponible', unavailable: 'No disponible', required: 'Obligatoria', uploaded: 'Cargada', retry: 'Reintentar', currentWarehouse: 'Almacén actual', warehouseFallback: 'Asignado automáticamente a esta cuenta', warehouseOperator: 'Operario de almacén' }, + app: { title: 'Baiyue Logistics Internacional', workbench: 'Almacén internacional', subtitle: 'Sistema de operaciones de almacén' }, + login: { username: 'ID de empleado / Usuario', usernamePlaceholder: 'Ingrese su ID de empleado', password: 'Contraseña', passwordPlaceholder: 'Ingrese su contraseña', submit: 'Iniciar sesión', submitting: 'Iniciando sesión...', usernameRequired: 'Ingrese su ID de empleado', passwordRequired: 'Ingrese su contraseña', offline: 'Sin conexión. Conéctese e inténtelo de nuevo.', failed: 'Error al iniciar sesión' }, + nav: { menu: 'Menú', history: 'Historial de trabajo', profile: 'Perfil', openMenu: 'Abrir menú', back: 'Volver' }, + home: { eyebrow: 'ALMACÉN INTERNACIONAL', actionable: 'disponible', night: 'Trabaje con seguridad en el turno nocturno', morning: 'Buenos días', afternoon: 'Buenas tardes', evening: 'Buenas noches' }, + module: { title: 'Operaciones', functions: 'Funciones', hint: 'Toque una función disponible para comenzar', availability: '{available}/{total} disponibles', onlyReceiving: 'Solo está disponible la recepción de contenedores. Las demás operaciones aún no están abiertas.' }, + modules: { inbound: { name: 'Entrada', description: 'Bloquee un contenedor por cuenta y complete la recepción' }, inventory: { name: 'Operaciones internas', description: 'Inventario, movimientos y operaciones de palés' }, relabel: { name: 'Reetiquetado', description: 'Recepción, ejecución y revisión de reetiquetado' }, query: { name: 'Consultas', description: 'Consulta de inventario, ubicaciones y palés' }, receiving: { name: 'Recepción de contenedor', description: 'Tres fotos; FBA por cantidad y cajas internacionales una a una' }, stocktake: { name: 'Conteo de inventario', description: 'Escanee ubicaciones e inventario por tarea' }, move: { name: 'Mover mercancía', description: 'Escanee ubicaciones de origen y destino' }, pallet: { name: 'Gestión de palés', description: 'Agrupe, divida y verifique palés' }, relabelReceive: { name: 'Recepción para reetiquetar', description: 'Escanee la orden y confirme el inventario' }, relabelExecute: { name: 'Ejecutar reetiquetado', description: 'Escanee etiquetas antiguas y nuevas' }, relabelReview: { name: 'Revisar reetiquetado', description: 'Verifique etiquetas y productos' }, inventoryQuery: { name: 'Consultar inventario', description: 'Escanee un artículo o una caja' }, locationQuery: { name: 'Consultar ubicación', description: 'Escanee una ubicación para ver capacidad e inventario' }, palletQuery: { name: 'Consultar palé', description: 'Escanee un palé para ver el inventario vinculado' } }, + profile: { title: 'Perfil', warehouse: 'Almacén de trabajo', warehousePending: 'Cargando datos del almacén', shift: 'Turno actual', shiftValue: 'Según el horario del almacén', version: 'Versión de la aplicación', language: 'Idioma', settings: 'Configuración', settingsLater: 'La configuración estará disponible en una versión futura', logout: 'Cerrar sesión', logoutTitle: '¿Cerrar sesión?', logoutDescription: 'Al salir se borra la sesión local del PDA internacional. El trabajo sin enviar no puede conservarse entre cuentas.' }, + language: { zh: '中文', en: 'English', es: 'Español' }, + receivingSummary: { container: 'Contenedor actual', warehouse: 'Almacén', destinations: 'Destinos', inboundCodes: 'Códigos de entrada', plannedPallets: 'Palés previstos', plannedBoxes: 'Cajas previstas', pendingBoxes: 'Cajas pendientes' }, + history: { title: 'Historial de trabajo', completedToday: 'Completado hoy', inProgress: 'En curso', recent: 'Trabajo reciente', emptyTitle: 'Sin registros de almacén', emptyDescription: 'El trabajo completado aparecerá aquí cuando se conecte el historial real.' }, + scanner: { title: 'Prueba de difusión del escáner', waitingTitle: 'Receptor iniciado', waitingDetail: 'Esperando un escaneo físico', receivedTitle: 'Escaneo recibido', receivedDetail: 'Escanee de nuevo para verificar la recepción continua', errorTitle: 'No se pudo iniciar el receptor', errorDetail: 'Revise la configuración del escáner PDA y vuelva a abrir la página', unsupportedTitle: 'Difusión no compatible aquí', unsupportedDetail: 'Pruebe en la aplicación Android del PDA', latest: 'Último valor', waiting: 'Esperando escaneo', hardwareHint: 'Pulse la tecla física de escaneo', count: 'Escaneo {count} en esta página', extra: 'Clave Extra', records: 'Escaneos recibidos', clear: 'Borrar', empty: 'Sin datos de difusión' }, + operation: { title: 'Recepción de contenedor', restoring: 'Restaurando el contenedor actual…', scanContainer: 'Escanear número de contenedor', container: 'Número de contenedor', containerPlaceholder: 'Escanee o ingrese el número', queryContainer: 'Buscar contenedor', pendingContainers: '{count} contenedores por descargar', emptyContainers: 'No hay contenedores por descargar', destination: 'Destino: {value}', receivingProgress: 'Progreso de recepción', continueCurrent: 'Continuar contenedor actual', viewDetail: 'Ver detalles', lockedNotice: 'Al seleccionarlo, la cuenta queda bloqueada al contenedor hasta finalizar.', backList: 'Volver a la lista', selectContainer: 'Seleccionar y tomar fotos', unloadPhotos: 'Fotos de recepción', photoHint: 'Tome una foto en cada una de las tres posiciones obligatorias', notAdded: 'Sin foto', uploading: 'Cargando…', retakeFailedKept: 'Falló la nueva foto; se conserva la anterior', uploadFailed: 'Error de carga. Reintente.', previewPhoto: 'Toque la foto para ampliarla', takePhotoHint: 'Tome una foto en el lugar', retake: 'Repetir', takePhoto: 'Tomar foto', signAndReceive: 'Confirmar llegada e iniciar recepción', scanCartonStep: '1. Escanee una caja para identificar la entrada', carton: 'Marca de caja', parseCarton: 'Resolver caja', fbaMode: 'Recepción FBA por cantidad', overseasMode: 'Recepción internacional por caja', totalBoxes: '{count} cajas en total', receivedBoxes: '{count} recibidas', pendingBoxes: '{count} pendientes', scanPalletStep: '2. Escanee el palé físico', pallet: 'Código de palé', palletPlaceholder: 'Escanee o ingrese el código', verifyPallet: 'Verificar palé', palletMatched: 'El destino coincide; listo para recibir', palletMismatch: 'El palé no coincide', inputCountStep: '3. Ingrese la cantidad de cajas', maxCount: 'Máximo {count} cajas', confirmBulk: 'Confirmar recepción por cantidad', confirmCarton: 'Confirmar esta caja', receiptSuccess: '{accepted} recibidas; quedan {pending} en {inbound}', completeReady: 'Todas las entradas conciliadas — completar contenedor', incomplete: 'Quedan {count} cajas', completedTitle: 'Recepción del contenedor completada', actualReceipt: '{received} / {total} cajas recibidas', returnPending: 'Volver a contenedores pendientes', unavailableSubtitle: 'Esta función aún no está disponible', unavailableTitle: 'Función no disponible', unavailableDescription: 'Los operarios no pueden llamar API de plataforma, cliente o internas. Los escaneos no se envían ni generan resultados simulados.', lockedContainer: 'Contenedor bloqueado para esta cuenta', inbounds: 'Códigos de entrada del contenedor', plannedPallets: '{count} palés planificados', photoLicense: 'Matrícula del vehículo', photoSeal: 'Sello del contenedor', photoDoor: 'Puertas abiertas', statusWait: 'Pendiente de firma', statusSigned: 'Firmado', statusReceiving: 'Recibiendo', statusCompleted: 'Completado', actionSuccess: '{action} completado', actionFailed: 'Error: {action}', photoLoadFailed: 'No se pudo cargar la foto', scanUnavailable: 'Esta función no tiene API PDA internacional; escaneo no enviado', currentConflict: 'La cuenta ya tiene el contenedor {container} sin terminar; se volvió al trabajo actual', actions: { refresh: 'Actualizar pendientes', query: 'Buscar contenedor', select: 'Seleccionar contenedor', sign: 'Firmar contenedor', carton: 'Resolver caja', sku: 'Resolver SKU', pallet: 'Verificar palé', receipt: 'Enviar recepción', complete: 'Completar recepción' } } +} diff --git a/src/i18n/locales/zh-CN.js b/src/i18n/locales/zh-CN.js new file mode 100644 index 0000000..69269c7 --- /dev/null +++ b/src/i18n/locales/zh-CN.js @@ -0,0 +1,23 @@ +export default { + receivingSummary: { container: '当前柜号', warehouse: '作业仓', destinations: '目的仓', inboundCodes: '入仓码', plannedPallets: '计划托盘', plannedBoxes: '计划箱数', pendingBoxes: '待收箱数' }, + scanInput: { active: '当前扫码', label: '扫码输入框', placeholder: '扫描或人工输入' }, + operationPhase: { candidates: { title: '待卸货集装箱', subtitle: '选择集装箱后拍摄 3 张照片完成签收' }, detail: { title: '柜号详情', subtitle: '核对柜号与整柜计划箱数' }, sign: { title: '拆柜拍照签收', subtitle: '完成现场取证后进入收货' }, receive: { title: '当前柜收货', subtitle: '扫描箱码定位入仓码后开始收货' }, completed: { title: '入库完成', subtitle: '库存与柜级进度已由服务端核平' } }, + common: { close: '关闭', cancel: '取消', confirm: '确认', refresh: '刷新', back: '返回', loading: '加载中…', boxes: '箱', pallets: '托', plans: '个计划', inboundCodes: '个入仓码', available: '可处理', unavailable: '暂未开放', required: '必传', uploaded: '已上传', retry: '请重试', currentWarehouse: '当前作业仓', warehouseFallback: '仓库由当前账号自动绑定', warehouseOperator: '仓库作业员' }, + app: { title: '柏悦物流海外版', workbench: '海外仓工作台', subtitle: '海外仓现场作业系统' }, + login: { username: '工号 / 用户名', usernamePlaceholder: '请输入工号', password: '密码', passwordPlaceholder: '请输入密码', submit: '登录', submitting: '登录中...', usernameRequired: '请输入工号', passwordRequired: '请输入密码', offline: '当前网络不可用,请联网后重试', failed: '登录失败' }, + nav: { menu: '功能菜单', history: '作业记录', profile: '个人中心', openMenu: '打开功能菜单', back: '返回上一页' }, + home: { eyebrow: 'OVERSEAS WAREHOUSE', actionable: '项可处理', night: '夜间作业请注意安全', morning: '早上好', afternoon: '下午好', evening: '晚上好' }, + module: { title: '业务模块', functions: '业务功能', hint: '点击可用功能开始作业', availability: '{available}/{total} 可用', onlyReceiving: '当前仅开放拆柜收货,其他作业暂未开放。' }, + modules: { + inbound: { name: '入仓作业', description: '单账号锁定一柜,完成柜号级入库闭环' }, inventory: { name: '库内作业', description: '盘点、移库与托盘管理' }, relabel: { name: '换标作业', description: '换标收货、执行与复核' }, query: { name: '现场查询', description: '库存、库位与托盘查询' }, + receiving: { name: '拆柜收货', description: '三类照片签收,FBA 按 SKU 箱数、海外仓逐箱收货' }, stocktake: { name: '库存盘点', description: '按任务扫描库位和库存' }, move: { name: '商品移库', description: '扫描来源与目标库位完成移库' }, pallet: { name: '托盘管理', description: '组托、拆托及托盘状态核对' }, relabelReceive: { name: '换标收货', description: '扫描换标单并确认待处理库存' }, relabelExecute: { name: '换标执行', description: '扫描旧标和新标记录换标结果' }, relabelReview: { name: '换标复核', description: '复核标签和商品关联关系' }, inventoryQuery: { name: '库存查询', description: '扫描商品或箱码查询可用库存' }, locationQuery: { name: '库位查询', description: '扫描库位查看容量和在库明细' }, palletQuery: { name: '托盘查询', description: '扫描托盘码查看绑定库存' } + }, + profile: { title: '个人中心', warehouse: '作业仓库', warehousePending: '正在读取仓库主档', shift: '当前班次', shiftValue: '以海外仓排班为准', version: '应用版本', language: '语言', settings: '系统设置', settingsLater: '设置功能将在后续版本开放', logout: '退出登录', logoutTitle: '确认退出登录?', logoutDescription: '退出后会清空海外 PDA 的本地登录会话;未提交作业数据不得跨账号保留。' }, + language: { zh: '中文', en: 'English', es: 'Español' }, + receivingSummary: { container: '当前柜号', inboundCodes: '入仓码', plannedBoxes: '计划箱数', pendingBoxes: '待收箱数' }, + history: { title: '作业记录', completedToday: '今日完成', inProgress: '进行中', recent: '最近作业', emptyTitle: '暂无海外仓作业记录', emptyDescription: '接入海外仓真实任务后,完成记录会显示在这里' }, + scanner: { title: '广播扫码测试', waitingTitle: '接收器已启动', waitingDetail: '正在等待硬件扫描结果', receivedTitle: '已收到扫描广播', receivedDetail: '继续扫描可验证连续接收', errorTitle: '接收器启动失败', errorDetail: '请检查 PDA 扫描服务设置后重新进入页面', unsupportedTitle: '当前环境不支持广播', unsupportedDetail: '请在 Android App 真机中测试', latest: '最近收到', waiting: '等待扫描', hardwareHint: '按下 PDA 硬件扫描键', count: '本页第 {count} 次', extra: 'Extra 标签', records: '接收记录', clear: '清空', empty: '暂无广播数据' }, + operation: { + title: '拆柜收货', restoring: '正在恢复当前柜作业…', scanContainer: '扫描柜号', container: '柜号', containerPlaceholder: '扫描或人工输入柜号', queryContainer: '查询柜号', pendingContainers: '{count} 个待卸货集装箱', emptyContainers: '暂无待卸货集装箱', destination: '目的仓:{value}', receivingProgress: '收货进度', continueCurrent: '继续当前柜', viewDetail: '查看详情', lockedNotice: '选择后账号将锁定当前柜,完成前不能切换其他柜号。', backList: '返回列表', selectContainer: '选择此柜并开始拍照', unloadPhotos: '卸货签收照片', photoHint: '三个固定槽位各拍摄 1 张现场照片', notAdded: '未添加', uploading: '正在上传…', retakeFailedKept: '重拍失败,已保留原图', uploadFailed: '上传失败,请重试', previewPhoto: '点击左侧照片可查看大图', takePhotoHint: '请拍摄现场照片', retake: '重拍', takePhoto: '拍照', signAndReceive: '确认签收并开始收货', scanCartonStep: '1. 扫描箱码定位入仓码', carton: '箱码', parseCarton: '解析箱码', fbaMode: 'FBA 按箱数收货', overseasMode: '海外仓逐箱收货', totalBoxes: '总计 {count} 箱', receivedBoxes: '已收 {count} 箱', pendingBoxes: '待收 {count} 箱', scanPalletStep: '2. 扫描实物托盘码', pallet: '托盘码', palletPlaceholder: '扫描或人工输入托盘码', verifyPallet: '校验托盘', palletMatched: '目的仓匹配,可收货', palletMismatch: '托盘不匹配', inputCountStep: '3. 输入本次箱数', maxCount: '最多可收 {count} 箱', confirmBulk: '确认按箱数收货', confirmCarton: '确认本箱收货', receiptSuccess: '本次已收 {accepted} 箱,{inbound} 剩余 {pending} 箱', completeReady: '每个入仓码已核平,完成整柜入库', incomplete: '尚有 {count} 箱未收', completedTitle: '当前柜已完成入库', actualReceipt: '实收 {received} / {total} 箱', returnPending: '返回待卸货列表', unavailableSubtitle: '当前功能暂未开放', unavailableTitle: '功能暂未开放', unavailableDescription: '现有平台、客户或内部接口不能由仓库作业员直接调用。扫码不会提交,也不会产生模拟成功结果。', lockedContainer: '当前账号锁定柜号', inbounds: '柜内入仓码', plannedPallets: '计划 {count} 托', photoLicense: '车牌照片', photoSeal: '集装箱封签', photoDoor: '开柜现场', statusWait: '待签收', statusSigned: '已签收', statusReceiving: '收货中', statusCompleted: '已完成', actionSuccess: '{action}成功', actionFailed: '{action}失败', photoLoadFailed: '照片加载失败', scanUnavailable: '该功能缺少海外 PDA 专用接口,扫码未提交', currentConflict: '账号已有未完成柜 {container},已返回当前柜', actions: { refresh: '刷新待卸柜', query: '查询柜号', select: '选择当前柜', sign: '签收当前柜', carton: '解析箱唛', sku: '解析 SKU', pallet: '校验托盘', receipt: '提交实收', complete: '完成入库' } + } +} diff --git a/src/main.js b/src/main.js index 18e1d8f..94f778f 100644 --- a/src/main.js +++ b/src/main.js @@ -1,8 +1,10 @@ import { createSSRApp } from 'vue' import App from './App.vue' +import { i18n } from './i18n/index.js' export function createApp() { const app = createSSRApp(App) + app.use(i18n) return { app diff --git a/src/pages/history/history.vue b/src/pages/history/history.vue index 668484e..3a0df14 100644 --- a/src/pages/history/history.vue +++ b/src/pages/history/history.vue @@ -1,6 +1,4 @@ - + diff --git a/src/pages/operation/operation.vue b/src/pages/operation/operation.vue index 6018261..316d378 100644 --- a/src/pages/operation/operation.vue +++ b/src/pages/operation/operation.vue @@ -1,102 +1,86 @@