Files
CargoTrace/docs/plans/2026-05-12-work-order-no-display.md
2026-05-13 09:09:50 +08:00

531 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 工令号显示功能 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<BoxDetailData> 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<String, dynamic> json) {
final boxes =
(json['existing_boxes'] as List<dynamic>?)
?.map((b) => BoxDetailData.fromJson(b as Map<String, dynamic>))
.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<BoxDetailData> 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"
```