# 工令号显示功能 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 "work_order_no" in data assert data["work_order_no"] is None or 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']?.toString(), 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; final paichanText = grey ? '--' : (_paichanNo ?? '--'); final workOrderText = grey ? '--' : (_workOrderNo ?? '--'); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 排产号 + 工令号:只显示值,突出关键信息 Row( children: [ Expanded( child: Text( paichanText, style: TextStyle( fontSize: 20, fontWeight: FontWeight.w700, color: grey ? Colors.grey : Colors.black87, ), overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 12), Expanded( child: Text( workOrderText, textAlign: TextAlign.right, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: grey ? Colors.grey : Colors.black87, ), overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 10), // 已有箱数 + 最大箱号 + 详情按钮(不变) 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: 在装箱详情页显示工令号 **Files:** - Modify: `apps/pad_scanner/lib/pages/boxing_page.dart` (`_openDetail` 方法) - Modify: `apps/pad_scanner/lib/pages/boxing_detail_page.dart` **Step 1: 给 `BoxingDetailPage` 增加工令号参数** 在 `BoxingDetailPage` 中新增 `workOrderNo` 字段,并在构造函数中要求传入: ```dart class BoxingDetailPage extends StatelessWidget { final String paichanNo; final String workOrderNo; final List existingBoxes; const BoxingDetailPage({ super.key, required this.paichanNo, required this.workOrderNo, required this.existingBoxes, }); ``` **Step 2: 在详情页标题区域显示排产号和工令号** 将 AppBar 标题中的单个 `paichanNo` 文本改成更紧凑的两行展示: ```dart appBar: AppBar( title: Row( children: [ const Text('装箱详情'), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( paichanNo, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ), overflow: TextOverflow.ellipsis, ), Text( workOrderNo, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, ), overflow: TextOverflow.ellipsis, ), ], ), ), ], ), ), ``` 如果工令号为空,传入方应使用 `--`,避免详情页出现空白。 **Step 3: 从装箱主页面传入工令号** 修改 `_openDetail`: ```dart void _openDetail() { Navigator.push( context, MaterialPageRoute( builder: (_) => BoxingDetailPage( paichanNo: _paichanNo ?? '', workOrderNo: _workOrderNo ?? '--', existingBoxes: _existingBoxes, ), ), ); } ``` **Step 4: 验证编译和静态分析** ```bash cd apps/pad_scanner flutter analyze lib/pages/boxing_page.dart lib/pages/boxing_detail_page.dart ``` 预期:无错误。 --- ### Task 8: 构建、安装并手动验证 **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. 点击 `详情 →`,确认装箱详情页显示排产号和工令号 6. 在三种模式下分别测试确认布局正确 **Step 4: 提交** ```bash git add apps/pad_scanner/lib/services/api_service.dart apps/pad_scanner/lib/pages/boxing_page.dart apps/pad_scanner/lib/pages/boxing_detail_page.dart git commit -m "feat(boxing): display work order number in boxing views" ```