优化
This commit is contained in:
@@ -13,7 +13,7 @@
|
|||||||
- 正式作业已确认为在线模式:断网不能开始正式拣货、提交、完成任务或推进 `LOADED`,不实现离线业务提交队列;恢复网络后提示员工刷新重试。
|
- 正式作业已确认为在线模式:断网不能开始正式拣货、提交、完成任务或推进 `LOADED`,不实现离线业务提交队列;恢复网络后提示员工刷新重试。
|
||||||
- 明确使用的工具链和发布流程后,补充准确的依赖版本、安装、调试、测试和打包命令,并同步更新 `README.md` 与本文件。
|
- 明确使用的工具链和发布流程后,补充准确的依赖版本、安装、调试、测试和打包命令,并同步更新 `README.md` 与本文件。
|
||||||
- 在 `package.json` 与锁文件正式补齐前,`vite.config.js` 只证明目标配置,不证明依赖可安装;维护者只能运行仓库当前可执行的 `node --test tests/*.test.mjs`,不得从海外 PDA 或全局缓存猜测国内 PDA 的 uni-app/Vite 版本。
|
- 在 `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。
|
- 国内 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` | 全局样式和应用生命周期 |
|
| 页面注册与启动 | `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 不执行更新逻辑 |
|
| 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/login/`、`stores/session.js` | 远程鉴权适配和会话清理 |
|
||||||
| 工作台与模块入口 | `pages/home/`、`pages/module/`、`pages/operation/` | `data/demo-data.js` 与页面禁用规则 |
|
| 工作台与模块入口 | `pages/home/`、`pages/module/`、`pages/operation/` | `data/demo-data.js` 与页面禁用规则 |
|
||||||
|
|||||||
+15
-14
@@ -95,7 +95,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="pick-item-row2">
|
<view class="pick-item-row2">
|
||||||
<text class="pick-item-desc">{{ itemDescription(item) }}</text>
|
<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>
|
||||||
|
|
||||||
<view v-if="itemState(item) === ITEM_STATUS.PENDING" class="pick-item-actions">
|
<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>
|
<view class="modal-sheet partial-modal" role="dialog" aria-modal="true" aria-label="部分拣货" @tap.stop>
|
||||||
<PopupHeader title="部分拣货" @close="closeOverlay" />
|
<PopupHeader title="部分拣货" @close="closeOverlay" />
|
||||||
<view v-if="partialTargetItem" class="partial-info">
|
<view v-if="partialTargetItem" class="partial-info">
|
||||||
<text class="partial-info-sku">{{ partialTargetItem.sku }}</text>
|
<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>
|
||||||
<text class="partial-info-desc">{{ partialTargetItem.desc }}</text>
|
<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 class="partial-info-qty">需拣总量:<text>{{ partialTargetItem.qty }}</text> {{ partialTargetItem.unit }}</view>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="partial-input-row">
|
<view class="partial-field">
|
||||||
<text class="partial-input-label">实际拣出</text>
|
<text class="partial-input-label">实际拣出数量</text>
|
||||||
<input
|
<view class="partial-input-control"><input
|
||||||
v-model="partialInput"
|
v-model="partialInput"
|
||||||
class="partial-input"
|
class="partial-input"
|
||||||
aria-label="实际拣出数量"
|
aria-label="实际拣出数量"
|
||||||
type="number"
|
type="number"
|
||||||
|
placeholder="请输入数量"
|
||||||
|
placeholder-class="partial-input-placeholder"
|
||||||
:focus="partialFocused"
|
:focus="partialFocused"
|
||||||
confirm-type="done"
|
confirm-type="done"
|
||||||
@confirm="confirmPartial"
|
@confirm="confirmPartial"
|
||||||
/>
|
/><text class="partial-input-unit">{{ partialTargetItem ? partialTargetItem.unit : '' }}</text></view>
|
||||||
<text class="partial-input-unit">{{ partialTargetItem ? partialTargetItem.unit : '' }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="partial-quick-row">
|
<view class="partial-quick-row">
|
||||||
<button
|
<button
|
||||||
v-for="option in partialQuickOptions"
|
v-for="option in partialQuickOptions"
|
||||||
:key="option.label"
|
:key="option.label"
|
||||||
class="partial-quick-btn"
|
class="partial-quick-btn"
|
||||||
|
:class="{ selected: String(partialInput) === String(option.value) }"
|
||||||
@tap="setPartialInput(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>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
<text v-if="partialValidationMessage" class="partial-validation" role="alert">{{ partialValidationMessage }}</text>
|
<text v-if="partialValidationMessage" class="partial-validation" role="alert">{{ partialValidationMessage }}</text>
|
||||||
@@ -280,7 +281,7 @@
|
|||||||
<PopupHeader :title="`仍有 ${summary.pending} 件未完成`" tone="danger" @close="closeOverlay" />
|
<PopupHeader :title="`仍有 ${summary.pending} 件未完成`" tone="danger" @close="closeOverlay" />
|
||||||
<text class="decision-desc submit-decision-desc">确认提交后,以下待拣商品会被明确标记为“未拣出”,本次演示提交不可撤销。</text>
|
<text class="decision-desc submit-decision-desc">确认提交后,以下待拣商品会被明确标记为“未拣出”,本次演示提交不可撤销。</text>
|
||||||
<view class="pending-sku-list">
|
<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>
|
<text v-if="pendingItems.length > 3">另有 {{ pendingItems.length - 3 }} 件</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="decision-label">选择未完成原因</text>
|
<text class="decision-label">选择未完成原因</text>
|
||||||
|
|||||||
+45
-26
@@ -226,8 +226,7 @@
|
|||||||
.pick-item-row1,
|
.pick-item-row1,
|
||||||
.pick-item-row2,
|
.pick-item-row2,
|
||||||
.submit-info,
|
.submit-info,
|
||||||
.box-list-item,
|
.box-list-item {
|
||||||
.partial-input-row {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -1041,61 +1040,81 @@
|
|||||||
|
|
||||||
.partial-info {
|
.partial-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
padding: 12px;
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
background: $primary-light;
|
background: $primary-light;
|
||||||
}
|
}
|
||||||
|
|
||||||
.partial-info-sku { color: $primary; font-size: 15px; font-weight: 700; }
|
.partial-info-copy { display: flex; min-width: 0; flex: 1; flex-direction: column; }
|
||||||
.partial-info-desc { margin-top: 4px; color: $text-secondary; font-size: 12px; }
|
.partial-info-sku { overflow: hidden; color: $primary; font-size: 13px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.partial-info-qty { margin-top: 7px; font-size: 13px; }
|
.partial-info-desc { overflow: hidden; margin-top: 2px; color: $text-secondary; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.partial-info-qty text { font-size: 16px; font-weight: 700; }
|
.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 {
|
.partial-field { margin-top: 12px; }
|
||||||
gap: 9px;
|
|
||||||
margin-top: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.partial-input-label {
|
.partial-input-label {
|
||||||
flex-shrink: 0;
|
display: block;
|
||||||
font-size: 13px;
|
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 {
|
.partial-input {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 48px;
|
height: 46px;
|
||||||
padding: 0 12px;
|
padding: 0 8px 0 14px;
|
||||||
border: 2px solid $primary;
|
border: 0;
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
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 {
|
.partial-quick-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
gap: 8px;
|
align-items: stretch;
|
||||||
margin-top: 12px;
|
gap: 6px;
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.partial-quick-btn {
|
.partial-quick-btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
padding: 0 6px;
|
margin: 0;
|
||||||
|
padding: 4px 2px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
border: 1px solid #b9c9df;
|
border: 1px solid #b9c9df;
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
color: #436080;
|
color: #436080;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
line-height: 48px;
|
line-height: 16px;
|
||||||
background: #f4f8fc;
|
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-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; }
|
.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; }
|
.decision-desc { display: block; color: $text-secondary; font-size: 13px; line-height: 20px; text-align: center; }
|
||||||
.submit-decision-modal { padding: 18px 16px 16px; }
|
.submit-decision-modal { padding: 18px 16px 16px; }
|
||||||
|
|||||||
@@ -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;/)
|
||||||
|
})
|
||||||
@@ -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 \}\}/)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user