diff --git a/shierfeng-fastapi-app/README.md b/shierfeng-fastapi-app/README.md
index c113185..ee0578a 100644
--- a/shierfeng-fastapi-app/README.md
+++ b/shierfeng-fastapi-app/README.md
@@ -23,6 +23,18 @@
>
> 另外谨慎升级 `package.json` 中锁定的 `pinia`/`vue`/`@vue/*` 相关包的版本,新版本可能 `uni-app` 没有兼容,造成一些奇怪的 bug
+### 配置 PDA 后端地址
+
+PDA 默认请求本机 `http://localhost:9099`,仅适用于后端运行在同一设备的本地开发。部署到独立 PDA 或局域网服务器时,在构建环境注入 `VITE_APP_BASE_API`:
+
+```bash
+VITE_APP_BASE_API=http://<服务器地址>:9099 pnpm run build:h5
+```
+
+不要把包含真实凭据的环境文件或地址配置提交到仓库。
+
+PDA 测试模式、批次授权和失效回退规则请阅读[测试数据批次操作说明](../docs/test-data-batch-operations.md)。PDA 只允许选择后端返回的 ACTIVE 授权批次,退出登录会清除本地批次上下文。
+
### vscode
使用 `vscode` 的开发者,请先安装 [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) 智能提示与感应插件
diff --git a/shierfeng-fastapi-app/package.json b/shierfeng-fastapi-app/package.json
index 75c2e4a..c30cf7b 100644
--- a/shierfeng-fastapi-app/package.json
+++ b/shierfeng-fastapi-app/package.json
@@ -3,6 +3,7 @@
"version": "1.9.0",
"packageManager": "pnpm@10.28.1",
"scripts": {
+ "test:business": "node --test tests/business-contract.test.mjs",
"dev": "uni -p mp-weixin",
"build": "npm run build:mp-weixin",
"dev:app": "uni -p app",
diff --git a/shierfeng-fastapi-app/src/api/business/filling.js b/shierfeng-fastapi-app/src/api/business/filling.js
new file mode 100644
index 0000000..22782b0
--- /dev/null
+++ b/shierfeng-fastapi-app/src/api/business/filling.js
@@ -0,0 +1,38 @@
+import request from "@/utils/request";
+
+export function listFilling(query) {
+ return request({
+ url: "/business/filling/list",
+ method: "get",
+ params: query,
+ });
+}
+
+export function getFillingStrainOptions() {
+ return request({
+ url: "/business/filling/strain-options",
+ method: "get",
+ });
+}
+
+export function scanFillingPallet(qrToken) {
+ return request({
+ url: "/business/filling/scan/" + qrToken,
+ method: "get",
+ });
+}
+
+export function getFilling(fillingId) {
+ return request({
+ url: "/business/filling/" + fillingId,
+ method: "get",
+ });
+}
+
+export function addFilling(data) {
+ return request({
+ url: "/business/filling",
+ method: "post",
+ data,
+ });
+}
diff --git a/shierfeng-fastapi-app/src/api/business/inbound.js b/shierfeng-fastapi-app/src/api/business/inbound.js
new file mode 100644
index 0000000..264d13b
--- /dev/null
+++ b/shierfeng-fastapi-app/src/api/business/inbound.js
@@ -0,0 +1,56 @@
+import request from "@/utils/request";
+
+export function listInbound(query) {
+ return request({
+ url: "/business/inbound/list",
+ method: "get",
+ params: query,
+ });
+}
+
+export function getInboundWarehouseOptions() {
+ return request({
+ url: "/business/inbound/warehouse-options",
+ method: "get",
+ });
+}
+
+export function scanInboundPallet(qrToken, query) {
+ return request({
+ url: "/business/inbound/scan/" + qrToken,
+ method: "get",
+ params: query,
+ });
+}
+
+export function getInbound(inboundId) {
+ return request({
+ url: "/business/inbound/" + inboundId,
+ method: "get",
+ });
+}
+
+export function addInbound(data) {
+ return request({
+ url: "/business/inbound",
+ method: "post",
+ data,
+ });
+}
+
+export function updateInbound(data) {
+ return request({
+ url: "/business/inbound",
+ method: "put",
+ data,
+ });
+}
+
+export function confirmInbound(inboundId, data) {
+ return request({
+ url: "/business/inbound/" + inboundId + "/confirm",
+ method: "put",
+ data,
+ headers: { repeatSubmit: false },
+ });
+}
diff --git a/shierfeng-fastapi-app/src/api/business/return.js b/shierfeng-fastapi-app/src/api/business/return.js
new file mode 100644
index 0000000..e1f29a3
--- /dev/null
+++ b/shierfeng-fastapi-app/src/api/business/return.js
@@ -0,0 +1,47 @@
+import request from "@/utils/request";
+
+export function listReturn(query) {
+ return request({
+ url: "/business/return/list",
+ method: "get",
+ params: query,
+ });
+}
+
+export function getReturnWarehouseOptions() {
+ return request({
+ url: "/business/return/warehouse-options",
+ method: "get",
+ });
+}
+
+export function scanReturnPallet(qrToken, query) {
+ return request({
+ url: "/business/return/scan/" + qrToken,
+ method: "get",
+ params: query,
+ });
+}
+
+export function requestReturn(data) {
+ return request({
+ url: "/business/return/request",
+ method: "post",
+ data,
+ });
+}
+
+export function getReturn(returnId) {
+ return request({
+ url: "/business/return/" + returnId,
+ method: "get",
+ });
+}
+
+export function confirmReturn(returnId, data) {
+ return request({
+ url: "/business/return/" + returnId + "/confirm",
+ method: "put",
+ data,
+ });
+}
diff --git a/shierfeng-fastapi-app/src/api/business/shipment.js b/shierfeng-fastapi-app/src/api/business/shipment.js
new file mode 100644
index 0000000..c3c9753
--- /dev/null
+++ b/shierfeng-fastapi-app/src/api/business/shipment.js
@@ -0,0 +1,62 @@
+import request from "@/utils/request";
+
+export function listShipment(query) {
+ return request({
+ url: "/business/shipment/list",
+ method: "get",
+ params: query,
+ });
+}
+
+export function getShipmentWarehouseOptions() {
+ return request({
+ url: "/business/shipment/warehouse-options",
+ method: "get",
+ });
+}
+
+export function getShipmentSiteOptions() {
+ return request({
+ url: "/business/shipment/site-options",
+ method: "get",
+ });
+}
+
+export function scanShipmentPallet(qrToken, query) {
+ return request({
+ url: "/business/shipment/scan/" + qrToken,
+ method: "get",
+ params: query,
+ });
+}
+
+export function getShipment(shipmentId) {
+ return request({
+ url: "/business/shipment/" + shipmentId,
+ method: "get",
+ });
+}
+
+export function addShipment(data) {
+ return request({
+ url: "/business/shipment",
+ method: "post",
+ data,
+ });
+}
+
+export function updateShipment(data) {
+ return request({
+ url: "/business/shipment",
+ method: "put",
+ data,
+ });
+}
+
+export function confirmShipment(shipmentId, data) {
+ return request({
+ url: "/business/shipment/" + shipmentId + "/confirm",
+ method: "put",
+ data,
+ });
+}
diff --git a/shierfeng-fastapi-app/src/api/business/usage.js b/shierfeng-fastapi-app/src/api/business/usage.js
new file mode 100644
index 0000000..07a25e6
--- /dev/null
+++ b/shierfeng-fastapi-app/src/api/business/usage.js
@@ -0,0 +1,39 @@
+import request from "@/utils/request";
+
+export function listUsage(query) {
+ return request({
+ url: "/business/usage/list",
+ method: "get",
+ params: query,
+ });
+}
+
+export function getUsageLineOptions(siteId) {
+ return request({
+ url: "/business/usage/line-options",
+ method: "get",
+ params: { siteId },
+ });
+}
+
+export function scanUsagePallet(qrToken) {
+ return request({
+ url: "/business/usage/scan/" + qrToken,
+ method: "get",
+ });
+}
+
+export function getUsage(usageId) {
+ return request({
+ url: "/business/usage/" + usageId,
+ method: "get",
+ });
+}
+
+export function addUsage(data) {
+ return request({
+ url: "/business/usage",
+ method: "post",
+ data,
+ });
+}
diff --git a/shierfeng-fastapi-app/src/api/system/testBatch.js b/shierfeng-fastapi-app/src/api/system/testBatch.js
new file mode 100644
index 0000000..6b800f3
--- /dev/null
+++ b/shierfeng-fastapi-app/src/api/system/testBatch.js
@@ -0,0 +1,9 @@
+import request from "@/utils/request";
+
+// PDA 只读取当前登录用户被授权且仍处于 ACTIVE 状态的批次。
+export function listAvailableTestBatches() {
+ return request({
+ url: "/system/test-batch/options",
+ method: "get",
+ });
+}
diff --git a/shierfeng-fastapi-app/src/components/business/PalletScanInput.vue b/shierfeng-fastapi-app/src/components/business/PalletScanInput.vue
new file mode 100644
index 0000000..031957a
--- /dev/null
+++ b/shierfeng-fastapi-app/src/components/business/PalletScanInput.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+ {{ errorMessage }}
+
+
+
+
+
diff --git a/shierfeng-fastapi-app/src/components/business/TestBatchBanner.vue b/shierfeng-fastapi-app/src/components/business/TestBatchBanner.vue
new file mode 100644
index 0000000..09daf4c
--- /dev/null
+++ b/shierfeng-fastapi-app/src/components/business/TestBatchBanner.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ 测试数据
+
+ 当前批次:{{ testBatchStore.currentBatchName || "已选测试批次" }}
+
+
+
+
+
+
diff --git a/shierfeng-fastapi-app/src/components/business/TestBatchModeSwitch.vue b/shierfeng-fastapi-app/src/components/business/TestBatchModeSwitch.vue
new file mode 100644
index 0000000..3e48260
--- /dev/null
+++ b/shierfeng-fastapi-app/src/components/business/TestBatchModeSwitch.vue
@@ -0,0 +1,153 @@
+
+
+
+
+ 数据模式
+
+ {{ testBatchStore.isTestMode ? "当前操作隔离在测试批次" : "当前使用正式数据" }}
+
+
+
+ {{ testBatchStore.isTestMode ? "测试数据" : "正式数据" }}
+
+
+
+
+
+
+
+
+
+ 测试批次
+
+
+ {{ selectedBatch?.batchName || (loading ? "加载中..." : "暂无可用测试批次") }}
+
+
+
+
+
+ {{ loadError }}
+
+
+
+
+
+
diff --git a/shierfeng-fastapi-app/src/config.js b/shierfeng-fastapi-app/src/config.js
index 658b12d..67a148f 100644
--- a/shierfeng-fastapi-app/src/config.js
+++ b/shierfeng-fastapi-app/src/config.js
@@ -1,6 +1,7 @@
// 应用全局配置
export default {
- baseUrl: "http://localhost:9099",
+ // PDA 部署到独立设备时由构建环境注入服务器地址;本地开发保留回环地址。
+ baseUrl: import.meta.env.VITE_APP_BASE_API || "http://localhost:9099",
// 应用信息
appInfo: {
// 应用名称
diff --git a/shierfeng-fastapi-app/src/main.ts b/shierfeng-fastapi-app/src/main.ts
index 255aa09..2aef405 100644
--- a/shierfeng-fastapi-app/src/main.ts
+++ b/shierfeng-fastapi-app/src/main.ts
@@ -5,10 +5,24 @@ import store from "./store"; // store
import { install } from "./plugins"; // plugins
import "./permission"; // permission
import { useDict } from "@/utils/dict";
+import { useTestBatchStore } from "./store";
+
+let testBatchUnavailableListenerRegistered = false;
+
+function registerTestBatchUnavailableListener() {
+ if (testBatchUnavailableListenerRegistered || typeof uni.$on !== "function") {
+ return;
+ }
+ testBatchUnavailableListenerRegistered = true;
+ uni.$on("test-batch-unavailable", () => {
+ useTestBatchStore().leaveTestMode();
+ });
+}
export function createApp() {
const app = createSSRApp(App);
app.use(store);
+ registerTestBatchUnavailableListener();
app.config.globalProperties.useDict = useDict;
install(app);
return {
diff --git a/shierfeng-fastapi-app/src/pages.json b/shierfeng-fastapi-app/src/pages.json
index c5e305f..eeb1e3c 100644
--- a/shierfeng-fastapi-app/src/pages.json
+++ b/shierfeng-fastapi-app/src/pages.json
@@ -25,6 +25,36 @@
"navigationBarTitleText": "工作台"
}
},
+ {
+ "path": "pages/business/inbound/index",
+ "style": {
+ "navigationBarTitleText": "仓库入库"
+ }
+ },
+ {
+ "path": "pages/business/filling/index",
+ "style": {
+ "navigationBarTitleText": "托盘装填"
+ }
+ },
+ {
+ "path": "pages/business/shipment/index",
+ "style": {
+ "navigationBarTitleText": "发货出库"
+ }
+ },
+ {
+ "path": "pages/business/usage/index",
+ "style": {
+ "navigationBarTitleText": "现场使用"
+ }
+ },
+ {
+ "path": "pages/business/return/index",
+ "style": {
+ "navigationBarTitleText": "退回仓库"
+ }
+ },
{
"path": "pages/mine/index",
"style": {
diff --git a/shierfeng-fastapi-app/src/pages/business/filling/index.vue b/shierfeng-fastapi-app/src/pages/business/filling/index.vue
new file mode 100644
index 0000000..2b9cf5f
--- /dev/null
+++ b/shierfeng-fastapi-app/src/pages/business/filling/index.vue
@@ -0,0 +1,364 @@
+
+
+
+
+
+ 托盘装填
+
+ 扫描空托盘,录入菌种、批次、规格和初始包数
+
+
+
+
+
+ 当前账号没有装填操作权限
+
+
+
+
+
+ 扫描待装填托盘
+
+
+
+
+ 仅接受 EMPTY_IN_WAREHOUSE 状态托盘。
+
+ {{ scanError }}
+
+
+
+
+ {{ scannedPallet.palletCode || `#${scannedPallet.palletId}` }}
+
+ {{ scannedPallet.flowState }}
+
+
+ 仓库:{{ scannedPallet.warehouseName || `#${scannedPallet.warehouseId}` }} · 容量:{{ scannedPallet.capacityPackages }} 包
+
+
+
+
+
+ 装填信息
+
+
+ 菌种
+
+
+ {{ selectedStrainName || "请选择启用菌种" }}
+
+
+
+
+ 生产批次
+
+
+
+ 包装规格
+
+
+
+ 初始包数
+
+
+ 托盘容量 {{ scannedPallet.capacityPackages }} 包,最终容量由服务端校验
+
+
+
+ {{ formError }}
+
+
+
+
+
+ 最近装填记录
+
+
+ {{ listError }}
+ 加载中...
+
+ 暂无装填记录
+
+
+ {{ row.fillingNo || `#${row.fillingId}` }}
+
+ {{ row.strainNameSnapshot || row.strainCodeSnapshot }} · {{ row.batchNo }} · {{ row.initialPackages }} 包
+
+ 托盘 #{{ row.palletId }} · {{ row.operatedAt || row.createTime || "—" }}
+
+
+
+
+
+
+
+
+
diff --git a/shierfeng-fastapi-app/src/pages/business/inbound/index.vue b/shierfeng-fastapi-app/src/pages/business/inbound/index.vue
new file mode 100644
index 0000000..00e5f8e
--- /dev/null
+++ b/shierfeng-fastapi-app/src/pages/business/inbound/index.vue
@@ -0,0 +1,486 @@
+
+
+
+
+
+
+ 仓库入库
+
+ 扫描尚未首次入库的空托盘,保存草稿后确认
+
+
+
+
+
+
+
+ 当前账号没有入库操作权限
+
+
+
+
+
+
+ {{ editing ? "编辑入库草稿" : "新建入库草稿" }}
+
+ 托盘数量 {{ scannedPallets.length }}
+
+
+
+ 入库仓库
+
+
+ {{ selectedWarehouseName || "请选择授权仓库" }}
+
+
+
+
+
+ 连续扫码
+
+
+ 仅接受 UNSTOCKED 托盘;同一托盘不会重复加入。
+
+
+
+ {{ scanError }}
+ {{ formError }}
+
+
+
+
+
+ {{ pallet.palletCode || `#${pallet.palletId}` }}
+
+ {{ pallet.flowState }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 入库单
+
+
+ {{ listError }}
+ {{ confirmError }}
+ 加载中...
+
+ 暂无入库单
+
+
+
+ {{ row.inboundNo || `#${row.inboundId}` }}
+
+ {{ row.status === "DRAFT" ? "草稿" : "已确认" }}
+
+
+
+ 仓库:{{ warehouseName(row.warehouseId) }} · 托盘:{{ row.items?.length || 0 }} 个
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/shierfeng-fastapi-app/src/pages/business/return/index.vue b/shierfeng-fastapi-app/src/pages/business/return/index.vue
new file mode 100644
index 0000000..6b078d1
--- /dev/null
+++ b/shierfeng-fastapi-app/src/pages/business/return/index.vue
@@ -0,0 +1,560 @@
+
+
+
+
+
+ 退回仓库
+
+ 客户现场发起退回申请,仓库逐托扫描后确认收货
+
+
+
+
+
+ 当前账号没有退回操作权限
+
+
+
+
+
+ 发起退回申请
+
+
+
+ 目标仓库
+
+
+ {{ selectedWarehouseName || "请选择目标仓库" }}
+
+
+
+
+
+ 同一申请只能包含同一客户现场的托盘;扫码成功后托盘由服务端冻结。
+
+ {{ requestError }}
+
+
+
+ {{ pallet.palletCode || `#${pallet.palletId}` }}
+ 现场 #{{ pallet.siteId }} · 剩余 {{ pallet.remainingPackages }} 包
+
+
+
+
+
+ 来源现场:#{{ requestForm.sourceSiteId }} · {{ requestScannedPallets.length }} 个托盘
+
+
+
+
+
+
+ 仓库确认退回
+ 必须完整扫描单据托盘
+
+
+
+ {{ selectedReturnLabel || "请选择待确认退回单" }}
+
+
+ {{ confirmError }}
+
+
+ 预期托盘 {{ confirmExpectedRows.length }} 个,已扫描 {{ confirmScannedPallets.length }} 个
+
+
+
+ {{ row.palletCode || `#${row.palletId}` }}
+ · 已扫描
+
+
+
+
+
+
+
+
+
+ 退回单
+
+
+ {{ listError }}
+ 加载中...
+
+ 暂无退回单
+
+
+
+ {{ row.returnNo || `#${row.returnId}` }}
+
+ {{ row.status === "REQUESTED" ? "待确认" : "已确认" }}
+
+
+
+ 现场 #{{ row.sourceSiteId }} → {{ warehouseName(row.targetWarehouseId) }} · 托盘 {{ row.items?.length || 0 }} 个
+
+
+
+
+
+
+
+
+
+
+
diff --git a/shierfeng-fastapi-app/src/pages/business/shipment/index.vue b/shierfeng-fastapi-app/src/pages/business/shipment/index.vue
new file mode 100644
index 0000000..b1d21b6
--- /dev/null
+++ b/shierfeng-fastapi-app/src/pages/business/shipment/index.vue
@@ -0,0 +1,549 @@
+
+
+
+
+
+
+ 发货出库
+
+ 选择来源仓库和目标现场,扫码创建整托发货单
+
+
+
+
+
+
+
+ 当前账号没有发货操作权限
+
+
+
+
+
+
+ {{ editing ? "编辑发货草稿" : "新建发货草稿" }}
+
+ 托盘数量 {{ scannedPallets.length }}
+
+
+
+ 来源仓库
+
+
+ {{ selectedWarehouseName || "请选择来源仓库" }}
+
+
+
+
+
+ 目标客户现场
+
+
+ {{ selectedSiteName || "请选择目标现场" }}
+
+
+
+
+
+ 连续扫码
+
+
+ 仅接受来源仓库内有剩余包数的 FILLED_IN_WAREHOUSE 托盘。
+
+
+
+ {{ scanError }}
+ {{ formError }}
+
+
+
+
+
+
+ {{ pallet.palletCode || `#${pallet.palletId}` }}
+
+
+ {{ pallet.strainName || pallet.strainCode || "未提供菌种" }} · 剩余 {{ pallet.remainingPackages }} 包
+
+
+
+
+
+ 批次:{{ pallet.batchNo || "—" }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 发货单
+
+
+ {{ listError }}
+ {{ confirmError }}
+ 加载中...
+
+ 暂无发货单
+
+
+
+ {{ row.shipmentNo || `#${row.shipmentId}` }}
+
+ {{ row.status === "DRAFT" ? "草稿" : "已确认" }}
+
+
+
+ {{ warehouseName(row.sourceWarehouseId) }} → {{ siteName(row.targetSiteId) }} · 托盘 {{ row.items?.length || 0 }} 个
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/shierfeng-fastapi-app/src/pages/business/usage/index.vue b/shierfeng-fastapi-app/src/pages/business/usage/index.vue
new file mode 100644
index 0000000..3455777
--- /dev/null
+++ b/shierfeng-fastapi-app/src/pages/business/usage/index.vue
@@ -0,0 +1,380 @@
+
+
+
+
+
+ 客户现场使用
+
+ 扫描现场托盘,首次使用绑定产线并记录本次消耗包数
+
+
+
+
+
+ 当前账号没有现场使用权限
+
+
+
+
+
+ 扫描待使用托盘
+
+
+
+
+ 仅接受 AT_SITE_UNUSED 或 AT_SITE_PARTIAL 且仍有余量的托盘。
+
+ {{ operationError }}
+
+
+
+
+ {{ scannedPallet.palletCode || `#${scannedPallet.palletId}` }}
+
+ {{ scannedPallet.flowState }}
+
+
+ {{ scannedPallet.strainName || scannedPallet.strainCode || "未提供菌种" }} · 批次 {{ scannedPallet.batchNo || "—" }}
+
+
+ 当前剩余:{{ scannedPallet.remainingPackages }} 包 · 现场 #{{ scannedPallet.siteId }}
+
+
+
+
+
+ 使用信息
+
+ 使用产线
+
+ {{ selectedLineName }}(已由托盘绑定)
+
+
+
+ {{ selectedLineName || "请选择现场产线" }}
+
+
+
+
+ 本次使用包数
+
+
+ 不能超过当前剩余 {{ scannedPallet.remainingPackages }} 包,最终以服务端校验为准
+
+
+ {{ submitError }}
+
+ 使用记录已提交
+
+ 使用后剩余 {{ usageResult.remainingPackages }} 包 · 状态 {{ usageResult.flowState }}
+
+
+
+
+
+
+
+ 最近使用记录
+
+
+ {{ listError }}
+ 加载中...
+
+ 暂无使用记录
+
+
+ {{ row.usageNo || `#${row.usageId}` }}
+
+ 托盘 #{{ row.palletId }} · 产线 #{{ row.lineId }} · 使用 {{ row.usedPackages }} 包
+
+
+ 剩余 {{ row.remainingPackagesAfter }} 包 · {{ row.occurredAt || "—" }}
+
+
+
+
+
+
+
+
+
+
diff --git a/shierfeng-fastapi-app/src/pages/login.vue b/shierfeng-fastapi-app/src/pages/login.vue
index 413b8b9..73fd973 100644
--- a/shierfeng-fastapi-app/src/pages/login.vue
+++ b/shierfeng-fastapi-app/src/pages/login.vue
@@ -135,7 +135,7 @@ const captchaEnabled = ref(true);
const register = ref(false);
const loginForm = ref({
username: "admin",
- password: "admin123",
+ password: "",
code: "",
uuid: "",
});
diff --git a/shierfeng-fastapi-app/src/pages/work/index.vue b/shierfeng-fastapi-app/src/pages/work/index.vue
index 0d8072a..a01d346 100644
--- a/shierfeng-fastapi-app/src/pages/work/index.vue
+++ b/shierfeng-fastapi-app/src/pages/work/index.vue
@@ -2,6 +2,8 @@
+
+
+
+
+
+
+ 现场业务
+
+
+
+
+
+
+
+
+ {{ entry.title }}
+
+
+ {{ entry.description }}
+
+
+
+
+
+
@@ -202,7 +234,8 @@