From 431f3f2f926c590655d744620789c3b2ab60294d Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 27 Aug 2026 17:16:41 +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 --- AGENTS.md | 5 +- pages/picking/picking.vue | 29 +++++----- styles/pda.scss | 71 ++++++++++++++++--------- tests/partial-layout.test.mjs | 25 +++++++++ tests/picking-location-display.test.mjs | 11 ++++ 5 files changed, 99 insertions(+), 42 deletions(-) create mode 100644 tests/partial-layout.test.mjs create mode 100644 tests/picking-location-display.test.mjs diff --git a/AGENTS.md b/AGENTS.md index b2eb641..3d9d186 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,7 +49,8 @@ | 目标 | 入口 | 继续查询 | |---|---|---| | 页面注册与启动 | `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` 与页面禁用规则 | diff --git a/pages/picking/picking.vue b/pages/picking/picking.vue index e7155c1..ae1d63a 100644 --- a/pages/picking/picking.vue +++ b/pages/picking/picking.vue @@ -95,7 +95,7 @@ {{ itemDescription(item) }} - {{ item.location }} + {{ item.location }} @@ -225,31 +225,32 @@ - {{ partialTargetItem.sku }} - {{ partialTargetItem.desc }} - 需拣总量:{{ partialTargetItem.qty }} {{ partialTargetItem.unit }} + {{ partialTargetItem.sku }}{{ partialTargetItem.desc }} + 需拣{{ partialTargetItem.qty }}{{ partialTargetItem.unit }} - - 实际拣出 - + 实际拣出数量 + - {{ partialTargetItem ? partialTargetItem.unit : '' }} + />{{ partialTargetItem ? partialTargetItem.unit : '' }} {{ partialValidationMessage }} @@ -280,7 +281,7 @@ 确认提交后,以下待拣商品会被明确标记为“未拣出”,本次演示提交不可撤销。 - {{ item.sku }} · {{ item.location }} + {{ item.location ? `${item.sku} · ${item.location}` : item.sku }} 另有 {{ pendingItems.length - 3 }} 件 选择未完成原因 diff --git a/styles/pda.scss b/styles/pda.scss index a2bd6e6..9dbbea0 100644 --- a/styles/pda.scss +++ b/styles/pda.scss @@ -226,8 +226,7 @@ .pick-item-row1, .pick-item-row2, .submit-info, - .box-list-item, - .partial-input-row { + .box-list-item { display: flex; align-items: center; } @@ -1041,61 +1040,81 @@ .partial-info { display: flex; - flex-direction: column; - padding: 12px; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; border-radius: 9px; background: $primary-light; } - .partial-info-sku { color: $primary; font-size: 15px; font-weight: 700; } - .partial-info-desc { margin-top: 4px; color: $text-secondary; font-size: 12px; } - .partial-info-qty { margin-top: 7px; font-size: 13px; } - .partial-info-qty text { font-size: 16px; font-weight: 700; } + .partial-info-copy { display: flex; min-width: 0; flex: 1; flex-direction: column; } + .partial-info-sku { overflow: hidden; color: $primary; font-size: 13px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; } + .partial-info-desc { overflow: hidden; margin-top: 2px; color: $text-secondary; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } + .partial-info-qty { display: flex; flex-shrink: 0; align-items: baseline; gap: 3px; color: $text-secondary; font-size: 11px; } + .partial-info-qty-label { margin-right: 1px; } + .partial-info-qty-number { color: $text; font-size: 18px; font-weight: 700; } - .partial-input-row { - gap: 9px; - margin-top: 14px; - } + .partial-field { margin-top: 12px; } .partial-input-label { - flex-shrink: 0; - font-size: 13px; + display: block; + margin-bottom: 6px; + color: $text-secondary; + font-size: 12px; + font-weight: 600; } + .partial-input-control { display: flex; height: 50px; align-items: center; overflow: hidden; border: 2px solid $primary; border-radius: 9px; background: #ffffff; } .partial-input { box-sizing: border-box; flex: 1; min-width: 0; - height: 48px; - padding: 0 12px; - border: 2px solid $primary; - border-radius: 8px; + height: 46px; + padding: 0 8px 0 14px; + border: 0; font-size: 18px; font-weight: 700; - text-align: center; + text-align: left; } + .partial-input-placeholder { color: #98a2b3; font-size: 14px; font-weight: 400; } - .partial-input-unit { flex-shrink: 0; color: $text-secondary; font-size: 13px; } + .partial-input-unit { flex-shrink: 0; padding: 0 14px 0 8px; color: $text-secondary; font-size: 13px; } .partial-quick-row { display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 8px; - margin-top: 12px; + grid-template-columns: repeat(4, minmax(0, 1fr)); + align-items: stretch; + gap: 6px; + margin-top: 10px; } .partial-quick-btn { + box-sizing: border-box; + display: flex; + width: 100%; height: 48px; - padding: 0 6px; + margin: 0; + padding: 4px 2px; + align-items: center; + justify-content: center; + flex-direction: column; border: 1px solid #b9c9df; border-radius: 7px; color: #436080; - font-size: 12px; - line-height: 48px; + font-size: 11px; + line-height: 16px; background: #f4f8fc; } + .partial-quick-btn.selected { border-color: $primary; color: $primary; background: $primary-light; } + .partial-quick-ratio, .partial-quick-value { display: block; } + .partial-quick-ratio { font-weight: 700; } + .partial-quick-value { color: $text-secondary; font-size: 10px; } + .partial-quick-btn.selected .partial-quick-value { color: #356cae; } .partial-validation { display: block; margin-top: 10px; color: $danger; font-size: 12px; line-height: 18px; } + .partial-modal .modal-actions { margin-top: 12px; } + .partial-modal .modal-cancel-btn, .partial-modal .modal-confirm-btn { box-sizing: border-box; width: 100%; margin: 0; } .modal-confirm-btn[disabled] { color: #8a9099; background: #e4e6eb; opacity: 1; } .decision-desc { display: block; color: $text-secondary; font-size: 13px; line-height: 20px; text-align: center; } .submit-decision-modal { padding: 18px 16px 16px; } diff --git a/tests/partial-layout.test.mjs b/tests/partial-layout.test.mjs new file mode 100644 index 0000000..87672ff --- /dev/null +++ b/tests/partial-layout.test.mjs @@ -0,0 +1,25 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readFile } from 'node:fs/promises' + +const read = (path) => readFile(new URL(path, import.meta.url), 'utf8') + +test('partial picking uses a compact quantity panel with four equal quick choices', async () => { + const [page, styles] = await Promise.all([ + read('../pages/picking/picking.vue'), + read('../styles/pda.scss') + ]) + + assert.match(page, /partial-info-copy[\s\S]*?partial-info-qty-number/) + assert.match(page, /partial-input-label">实际拣出数量[\s\S]*?partial-input-control/) + assert.match(page, /placeholder="请输入数量"/) + assert.match(page, /placeholder-class="partial-input-placeholder"/) + assert.match(page, /partial-quick-btn"[\s\S]*?:class="\{ selected: String\(partialInput\) === String\(option\.value\) \}"/) + assert.match(page, /partial-quick-ratio[\s\S]*?partial-quick-value/) + assert.match(styles, /\.partial-quick-row \{[^}]*grid-template-columns: repeat\(4, minmax\(0, 1fr\)\)[^}]*align-items: stretch/) + assert.match(styles, /\.partial-quick-btn \{[^}]*width: 100%;[^}]*height: 48px;[^}]*margin: 0;/) + assert.match(styles, /\.partial-input-control \{[^}]*height: 50px;[^}]*border: 2px solid \$primary/) + assert.match(styles, /\.partial-input-placeholder \{[^}]*font-size: 14px;[^}]*font-weight: 400;/) + assert.match(styles, /\.partial-modal \.modal-actions \{[^}]*margin-top: 12px/) + assert.match(styles, /\.partial-modal \.modal-cancel-btn, \.partial-modal \.modal-confirm-btn \{[^}]*width: 100%;[^}]*margin: 0;/) +}) diff --git a/tests/picking-location-display.test.mjs b/tests/picking-location-display.test.mjs new file mode 100644 index 0000000..d1cbade --- /dev/null +++ b/tests/picking-location-display.test.mjs @@ -0,0 +1,11 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readFile } from 'node:fs/promises' + +const page = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8') + +test('missing picking location does not render an empty badge or dangling separator', () => { + assert.match(page, /\{\{ item\.location \}\}<\/text>/) + assert.match(page, /item\.location \? `\$\{item\.sku\} · \$\{item\.location\}` : item\.sku/) + assert.doesNotMatch(page, /\{\{ item\.location \}\}/) +})