12 KiB
工令号显示功能 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 列,增加 工令号。
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 字段。
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。
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 或浏览器测试:
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 字段
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 断言
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: 运行测试验证
cd services/fastapi
source ../../.venv/Scripts/activate # 或项目虚拟环境
pytest tests/test_box_api.py -v
预期:test_box_info_success 通过,work_order_no 有值。
Step 3: 格式化代码
black app/services/box_service.py app/schemas/box.py tests/test_box_api.py
Step 4: 提交
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 字段
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'] 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: 验证编译通过
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 行之后),增加:
// 工令号(来自后端查询)
String? _workOrderNo;
Step 2: 更新 _resetState 方法
在 _resetState 中增加清除 work_order_no:
void _resetState() {
_phase = _Phase.waiting;
_zongpaiNo = null;
_paichanNo = null;
_workOrderNo = null; // 新增
_erpQuantity = null;
// ... 其余不变
}
Step 3: 更新 _queryBoxInfo 中的 setState
在 _queryBoxInfo 方法的 setState 块中增加赋值:
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+详情 →(不变)
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: 验证编译和静态分析
cd apps/pad_scanner
flutter analyze lib/pages/boxing_page.dart
预期:无错误。
Task 7: 构建、安装并手动验证
Step 1: 构建 APK
cd apps/pad_scanner
flutter build apk --release
Step 2: 安装到设备
adb install -r build/app/outputs/flutter-apk/app-release.apk
Step 3: 手动验证
在设备上操作:
- 进入装箱编号页面
- 确认等待状态下信息区显示
排产号:和工令号:标签并排,值均为-- - 扫描一个真实的执行卡(如
26BW0011) - 确认信息区显示:
- 标签行:
排产号:|工令号: - 值行:
W00009(加粗)|6-1(7)(常规)
- 标签行:
- 在三种模式下分别测试确认布局正确
Step 4: 提交
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"