diff --git a/apps/pad_scanner b/apps/pad_scanner index 6258351..94db282 160000 --- a/apps/pad_scanner +++ b/apps/pad_scanner @@ -1 +1 @@ -Subproject commit 62583510b26f06526045150081d53224dbfa2b01 +Subproject commit 94db2825c86e2abd96db7afe94c2974d64b56d4c diff --git a/docs/plans/2026-05-12-work-order-no-display.md b/docs/plans/2026-05-12-work-order-no-display.md new file mode 100644 index 0000000..c6d86f7 --- /dev/null +++ b/docs/plans/2026-05-12-work-order-no-display.md @@ -0,0 +1,449 @@ +# 工令号显示功能 Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** 在装箱编号模块的信息区增加工令号(work_order_no)显示,采用"字段名称一行、实际值一行"的两行布局,排产号和工令号并排展示。 + +**Architecture:** 后端从 ERP 视图 `vw_productionContractData` 额外查询 `工令号` 列,通过已有的 `/box/info` 接口返回给前端。Flutter 端解析后渲染为两行布局(标签行 + 值行)。 + +**Tech Stack:** FastAPI + SQLAlchemy (后端), Flutter (前端), PostgreSQL (连接 ERP 视图) + +--- + +## 步骤一:FastAPI 后端 + +### Task 1: 扩展 ERP 查询 SQL,增加工令号字段 + +**Files:** +- Modify: `services/fastapi/app/services/box_service.py:46-59` + +**Step 1: 修改 `_erp_info_sql` 函数** + +将 SQL 查询从 3 列扩展为 4 列,增加 `工令号`。 + +```python +def _erp_info_sql(db: Session) -> str: + dialect_name = db.bind.dialect.name if db.bind is not None else "" + if dialect_name == "postgresql": + return ( + 'SELECT "总排号", "排产号", "数量", "工令号" ' + 'FROM "ERPAuto"."vw_productionContractData" ' + 'WHERE "总排号" = :zongpai_no ' + "LIMIT 1" + ) + return ( + "SELECT TOP 1 [总排号], [排产号], [数量], [工令号] " + "FROM [ERPAuto].[vw_productionContractData] " + "WHERE [总排号] = :zongpai_no" + ) +``` + +**Step 2: 修改 `query_erp_info` 函数** + +在返回字典中增加 `work_order_no` 字段。 + +```python +def query_erp_info(db: Session, zongpai_no: str) -> dict: + """从 ERP 视图查询总排号对应的排产号、工令号和数量。""" + sql = text(_erp_info_sql(db)) + row = db.execute(sql, {"zongpai_no": zongpai_no}).fetchone() + if not row: + raise ZongpaiNotFoundError() + return { + "zongpai_no": row[0], + "paichan_no": row[1], + "quantity": int(row[2]), + "work_order_no": row[3], + } +``` + +**Step 3: 修改 `get_box_info` 函数的返回值** + +在返回字典中增加 `work_order_no`。 + +```python +return { + "zongpai_no": zongpai_no, + "paichan_no": paichan_no, + "work_order_no": erp["work_order_no"], + "quantity": quantity, + "existing_boxes": existing_boxes, + "max_box_no": max_box_no, + "suggested_box_no": max_box_no + 1, +} +``` + +**Step 4: 验证** + +启动服务,用 curl 或浏览器测试: +```bash +curl "http://localhost:8000/CargoTrace/box/info?zongpai_no=26BW0011" +``` + +预期响应中包含 `"work_order_no": "6-1(7)"`。 + +--- + +### Task 2: 更新 Pydantic Schema + +**Files:** +- Modify: `services/fastapi/app/schemas/box.py:20-27` + +**Step 1: 在 `BoxInfoResponse` 中增加 `work_order_no` 字段** + +```python +class BoxInfoResponse(BaseModel): + """GET /box/info 响应""" + zongpai_no: str + paichan_no: str + work_order_no: str | None = None + quantity: int + existing_boxes: list[BoxDetail] + max_box_no: int + suggested_box_no: int +``` + +**Step 2: 验证 API 文档** + +启动服务后访问 `http://localhost:8000/docs`,确认 `BoxInfoResponse` 包含 `work_order_no` 字段。 + +--- + +### Task 3: 更新测试 + +**Files:** +- Modify: `services/fastapi/tests/test_box_api.py:7-19` + +**Step 1: 在 `test_box_info_success` 中增加 `work_order_no` 断言** + +```python +def test_box_info_success(client: TestClient): + """查询真实存在的总排号 → 200""" + resp = client.get( + "/CargoTrace/box/info", params={"zongpai_no": "26BW0011"} + ) + assert resp.status_code == 200 + data = resp.json() + assert data["zongpai_no"] == "26BW0011" + assert data["paichan_no"] == "W00009" + assert data["work_order_no"] is not None + assert isinstance(data["work_order_no"], str) + assert data["quantity"] == 80 + assert "existing_boxes" in data + assert "max_box_no" in data + assert data["suggested_box_no"] == data["max_box_no"] + 1 +``` + +**Step 2: 运行测试验证** + +```bash +cd services/fastapi +source ../../.venv/Scripts/activate # 或项目虚拟环境 +pytest tests/test_box_api.py -v +``` + +预期:`test_box_info_success` 通过,`work_order_no` 有值。 + +**Step 3: 格式化代码** + +```bash +black app/services/box_service.py app/schemas/box.py tests/test_box_api.py +``` + +**Step 4: 提交** + +```bash +git add services/fastapi/app/services/box_service.py services/fastapi/app/schemas/box.py services/fastapi/tests/test_box_api.py +git commit -m "feat(box): add work_order_no to box info API response" +``` + +--- + +## 步骤二:Flutter 应用 + +### Task 4: 扩展 API 数据模型 + +**Files:** +- Modify: `apps/pad_scanner/lib/services/api_service.dart:66-107` + +**Step 1: 在 `BoxInfoResult` 中增加 `workOrderNo` 字段** + +```dart +class BoxInfoResult { + final bool success; + final String? errorMessage; + final String? zongpaiNo; + final String? paichanNo; + final String? workOrderNo; + final int? quantity; + final List existingBoxes; + final int maxBoxNo; + final int suggestedBoxNo; + + BoxInfoResult({ + required this.success, + this.errorMessage, + this.zongpaiNo, + this.paichanNo, + this.workOrderNo, + this.quantity, + this.existingBoxes = const [], + this.maxBoxNo = 0, + this.suggestedBoxNo = 1, + }); + + factory BoxInfoResult.ok(Map json) { + final boxes = + (json['existing_boxes'] as List?) + ?.map((b) => BoxDetailData.fromJson(b as Map)) + .toList() ?? + []; + return BoxInfoResult( + success: true, + zongpaiNo: json['zongpai_no'] as String?, + paichanNo: json['paichan_no'] as String?, + workOrderNo: json['work_order_no'] as String?, + quantity: json['quantity'] as int?, + existingBoxes: boxes, + maxBoxNo: json['max_box_no'] as int? ?? 0, + suggestedBoxNo: json['suggested_box_no'] as int? ?? 1, + ); + } + + factory BoxInfoResult.error(String message) { + return BoxInfoResult(success: false, errorMessage: message); + } +} +``` + +**Step 2: 验证编译通过** + +```bash +cd apps/pad_scanner +flutter analyze lib/services/api_service.dart +``` + +--- + +### Task 5: 更新 BoxingPage 状态管理 + +**Files:** +- Modify: `apps/pad_scanner/lib/pages/boxing_page.dart` + +**Step 1: 增加状态变量** + +在 `_BoxingPageState` 的状态变量区域(约第 51 行之后),增加: + +```dart +// 工令号(来自后端查询) +String? _workOrderNo; +``` + +**Step 2: 更新 `_resetState` 方法** + +在 `_resetState` 中增加清除 `work_order_no`: + +```dart +void _resetState() { + _phase = _Phase.waiting; + _zongpaiNo = null; + _paichanNo = null; + _workOrderNo = null; // 新增 + _erpQuantity = null; + // ... 其余不变 +} +``` + +**Step 3: 更新 `_queryBoxInfo` 中的 setState** + +在 `_queryBoxInfo` 方法的 `setState` 块中增加赋值: + +```dart +setState(() { + _zongpaiNo = zongpai; + _paichanNo = result.paichanNo; + _workOrderNo = result.workOrderNo; // 新增 + _erpQuantity = result.quantity; + // ... 其余不变 +}); +``` + +--- + +### Task 6: 重写信息区 UI — 两行布局 + +**Files:** +- Modify: `apps/pad_scanner/lib/pages/boxing_page.dart` (`_buildInfoArea` 方法,约第 659-744 行) + +**Step 1: 用新布局替换整个 `_buildInfoArea` 方法** + +将原来的 `排产号标签 → 排产号值 → 已有箱数/最大箱号` 布局,改为: +- 第一行:`排产号:` + `工令号:`(标签行) +- 第二行:`W00009` + `6-1(7)`(值行) +- 第三行:`已有箱数:3箱 最大箱号:3` + `详情 →`(不变) + +```dart +Widget _buildInfoArea(bool isWaiting, ColorScheme colorScheme) { + final grey = isWaiting; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 字段名称行:排产号 + 工令号 + Row( + children: [ + Text( + '排产号:', + style: TextStyle( + fontSize: 13, + color: grey ? Colors.grey : Colors.black54, + ), + ), + const SizedBox(width: 24), + Text( + '工令号:', + style: TextStyle( + fontSize: 13, + color: grey ? Colors.grey : Colors.black54, + ), + ), + ], + ), + const SizedBox(height: 2), + // 实际值行 + Row( + children: [ + Expanded( + child: Text( + _paichanNo ?? '--', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: grey ? Colors.grey : Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + ), + Expanded( + child: Text( + _workOrderNo ?? '--', + style: TextStyle( + fontSize: 14, + color: grey ? Colors.grey : Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 8), + + // 已有箱数 + 最大箱号 + 详情按钮(不变) + Row( + children: [ + Expanded( + child: Row( + children: [ + Text( + '已有箱数:', + style: TextStyle( + fontSize: 13, + color: grey ? Colors.grey : Colors.black54, + ), + ), + Text( + grey ? '--' : '${_existingBoxes.length}箱', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: grey ? Colors.grey : Colors.black87, + ), + ), + const SizedBox(width: 16), + Text( + '最大箱号:', + style: TextStyle( + fontSize: 13, + color: grey ? Colors.grey : Colors.black54, + ), + ), + Text( + grey ? '--' : '$_maxBoxNo', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: _maxBoxNo > 0 + ? Colors.amber.shade800 + : Colors.grey, + ), + ), + ], + ), + ), + TextButton( + onPressed: grey || _existingBoxes.isEmpty ? null : _openDetail, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 8), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: Text( + '详情 →', + style: TextStyle( + fontSize: 13, + color: grey || _existingBoxes.isEmpty + ? Colors.grey.shade400 + : colorScheme.primary, + ), + ), + ), + ], + ), + ], + ); + } +``` + +**Step 2: 验证编译和静态分析** + +```bash +cd apps/pad_scanner +flutter analyze lib/pages/boxing_page.dart +``` + +预期:无错误。 + +--- + +### Task 7: 构建、安装并手动验证 + +**Step 1: 构建 APK** + +```bash +cd apps/pad_scanner +flutter build apk --release +``` + +**Step 2: 安装到设备** + +```bash +adb install -r build/app/outputs/flutter-apk/app-release.apk +``` + +**Step 3: 手动验证** + +在设备上操作: +1. 进入装箱编号页面 +2. 确认等待状态下信息区显示 `排产号:` 和 `工令号:` 标签并排,值均为 `--` +3. 扫描一个真实的执行卡(如 `26BW0011`) +4. 确认信息区显示: + - 标签行:`排产号:` | `工令号:` + - 值行:`W00009`(加粗)| `6-1(7)`(常规) +5. 在三种模式下分别测试确认布局正确 + +**Step 4: 提交** + +```bash +git add apps/pad_scanner/lib/services/api_service.dart apps/pad_scanner/lib/pages/boxing_page.dart +git commit -m "feat(boxing): display work order number in info area with two-row layout" +``` diff --git a/services/fastapi b/services/fastapi index 25982b1..cc34966 160000 --- a/services/fastapi +++ b/services/fastapi @@ -1 +1 @@ -Subproject commit 25982b12bf29622391606bc296f22afbcf6e8ead +Subproject commit cc34966be6ba3a9e0e73104153d2069c3de0285f