This commit is contained in:
2026-08-27 17:16:41 +08:00
parent b1f46835b2
commit 431f3f2f92
5 changed files with 99 additions and 42 deletions
+3 -2
View File
@@ -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` 与页面禁用规则 |
+15 -14
View File
@@ -95,7 +95,7 @@
</view>
<view class="pick-item-row2">
<text class="pick-item-desc">{{ itemDescription(item) }}</text>
<text class="pick-item-location">{{ item.location }}</text>
<text v-if="item.location" class="pick-item-location">{{ item.location }}</text>
</view>
<view v-if="itemState(item) === ITEM_STATUS.PENDING" class="pick-item-actions">
@@ -225,31 +225,32 @@
<view class="modal-sheet partial-modal" role="dialog" aria-modal="true" aria-label="部分拣货" @tap.stop>
<PopupHeader title="部分拣货" @close="closeOverlay" />
<view v-if="partialTargetItem" class="partial-info">
<text class="partial-info-sku">{{ partialTargetItem.sku }}</text>
<text class="partial-info-desc">{{ partialTargetItem.desc }}</text>
<view class="partial-info-qty">需拣总量:<text>{{ partialTargetItem.qty }}</text> {{ partialTargetItem.unit }}</view>
<view class="partial-info-copy"><text v-if="partialTargetItem.sku" class="partial-info-sku">{{ partialTargetItem.sku }}</text><text class="partial-info-desc">{{ partialTargetItem.desc }}</text></view>
<view class="partial-info-qty"><text class="partial-info-qty-label">需拣</text><text class="partial-info-qty-number">{{ partialTargetItem.qty }}</text><text>{{ partialTargetItem.unit }}</text></view>
</view>
<view class="partial-input-row">
<text class="partial-input-label">实际拣出</text>
<input
v-model="partialInput"
class="partial-input"
aria-label="实际拣出数量"
<view class="partial-field">
<text class="partial-input-label">实际拣出数量</text>
<view class="partial-input-control"><input
v-model="partialInput"
class="partial-input"
aria-label="实际拣出数量"
type="number"
placeholder="请输入数量"
placeholder-class="partial-input-placeholder"
:focus="partialFocused"
confirm-type="done"
@confirm="confirmPartial"
/>
<text class="partial-input-unit">{{ partialTargetItem ? partialTargetItem.unit : '' }}</text>
/><text class="partial-input-unit">{{ partialTargetItem ? partialTargetItem.unit : '' }}</text></view>
</view>
<view class="partial-quick-row">
<button
v-for="option in partialQuickOptions"
:key="option.label"
class="partial-quick-btn"
:class="{ selected: String(partialInput) === String(option.value) }"
@tap="setPartialInput(option.value)"
>
{{ option.label }} ({{ option.value }})
<text class="partial-quick-ratio">{{ option.label }}</text><text class="partial-quick-value">{{ option.value }}{{ partialTargetItem ? partialTargetItem.unit : '' }}</text>
</button>
</view>
<text v-if="partialValidationMessage" class="partial-validation" role="alert">{{ partialValidationMessage }}</text>
@@ -280,7 +281,7 @@
<PopupHeader :title="`仍有 ${summary.pending} 件未完成`" tone="danger" @close="closeOverlay" />
<text class="decision-desc submit-decision-desc">确认提交后,以下待拣商品会被明确标记为“未拣出”,本次演示提交不可撤销。</text>
<view class="pending-sku-list">
<text v-for="item in pendingItems.slice(0, 3)" :key="item.id">{{ item.sku }} · {{ item.location }}</text>
<text v-for="item in pendingItems.slice(0, 3)" :key="item.id">{{ item.location ? `${item.sku} · ${item.location}` : item.sku }}</text>
<text v-if="pendingItems.length > 3">另有 {{ pendingItems.length - 3 }} 件</text>
</view>
<text class="decision-label">选择未完成原因</text>
+45 -26
View File
@@ -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; }
+25
View File
@@ -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;/)
})
+11
View File
@@ -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, /<text v-if="item\.location" class="pick-item-location">\{\{ item\.location \}\}<\/text>/)
assert.match(page, /item\.location \? `\$\{item\.sku\} · \$\{item\.location\}` : item\.sku/)
assert.doesNotMatch(page, /<text class="pick-item-location">\{\{ item\.location \}\}/)
})