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 \}\}/)
+})