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

14 KiB
Raw Permalink Blame History

工令号显示功能 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 "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: 运行测试验证

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']?.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: 验证编译通过

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 方法

将原来的 排产号标签 → 排产号值 → 已有箱数/最大箱号 布局,改为:

  • 第一行:只显示具体值,W000096-1(7) 两列并排,不显示 排产号:工令号: 字段名称
  • 排产号和工令号都使用更大字号、加粗,等待状态显示 --
  • 第二行:已有箱数3箱 最大箱号3 + 详情 →(不变)
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: 验证编译和静态分析

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 字段,并在构造函数中要求传入:

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 文本改成更紧凑的两行展示:

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

void _openDetail() {
  Navigator.push(
    context,
    MaterialPageRoute(
      builder: (_) => BoxingDetailPage(
        paichanNo: _paichanNo ?? '',
        workOrderNo: _workOrderNo ?? '--',
        existingBoxes: _existingBoxes,
      ),
    ),
  );
}

Step 4: 验证编译和静态分析

cd apps/pad_scanner
flutter analyze lib/pages/boxing_page.dart lib/pages/boxing_detail_page.dart

预期:无错误。


Task 8: 构建、安装并手动验证

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: 手动验证

在设备上操作:

  1. 进入装箱编号页面
  2. 确认等待状态下信息区不显示字段名称,只显示两个并排的 --
  3. 扫描一个真实的执行卡(如 26BW0011
  4. 确认信息区显示:
    • W000096-1(7) 两个值并排
    • 两个值都比原信息区更醒目,并且加粗
    • 不显示 排产号:工令号: 标签
  5. 点击 详情 →,确认装箱详情页显示排产号和工令号
  6. 在三种模式下分别测试确认布局正确

Step 4: 提交

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"