feat: implement boxing module with three modes (one2one, one2many, many2one)

- Add BoxInfoResult/BoxSaveResult data classes and API methods to ApiService
- Create BoxingPage with mode switching, auto-fill rules, duplicate detection
- Create BoxingDetailPage for read-only box detail view
- Activate boxing module in HomePage navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-05-11 17:38:16 +08:00
parent cdc64bf82f
commit 5bae60cc82
5 changed files with 1649 additions and 7 deletions

View File

@@ -25,6 +25,120 @@ class RegistrationResult {
RegistrationResult(success: false, errorMessage: message);
}
// === 装箱模块数据类 ===
/// 箱号内单个总排号明细
class BoxItemData {
final String zongpaiNo;
final int quantity;
BoxItemData({required this.zongpaiNo, required this.quantity});
factory BoxItemData.fromJson(Map<String, dynamic> json) {
return BoxItemData(
zongpaiNo: json['zongpai_no'] as String,
quantity: json['quantity'] as int,
);
}
}
/// 箱号明细
class BoxDetailData {
final int boxNo;
final List<BoxItemData> items;
BoxDetailData({required this.boxNo, required this.items});
factory BoxDetailData.fromJson(Map<String, dynamic> json) {
final items = (json['items'] as List<dynamic>?)
?.map((i) => BoxItemData.fromJson(i as Map<String, dynamic>))
.toList() ??
[];
return BoxDetailData(boxNo: json['box_no'] as int, items: items);
}
}
/// 装箱信息查询结果
class BoxInfoResult {
final bool success;
final String? errorMessage;
final String? zongpaiNo;
final String? paichanNo;
final int? quantity;
final List<BoxDetailData> existingBoxes;
final int maxBoxNo;
final int suggestedBoxNo;
BoxInfoResult({
required this.success,
this.errorMessage,
this.zongpaiNo,
this.paichanNo,
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?,
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);
}
}
/// 装箱保存结果
class BoxSaveResult {
final bool success;
final bool isDuplicate;
final String? errorMessage;
final String? paichanNo;
final int? boxNo;
BoxSaveResult({
required this.success,
this.isDuplicate = false,
this.errorMessage,
this.paichanNo,
this.boxNo,
});
factory BoxSaveResult.ok(Map<String, dynamic> json) {
return BoxSaveResult(
success: true,
paichanNo: json['paichan_no'] as String?,
boxNo: json['box_no'] as int?,
);
}
factory BoxSaveResult.duplicate(Map<String, dynamic> json) {
return BoxSaveResult(
success: false,
isDuplicate: true,
paichanNo: json['paichan_no'] as String?,
boxNo: json['box_no'] as int?,
);
}
factory BoxSaveResult.error(String message) {
return BoxSaveResult(success: false, errorMessage: message);
}
}
class ApiService {
final http.Client _client;
final Duration timeout;
@@ -71,6 +185,77 @@ class ApiService {
}
}
/// 查询装箱信息 — GET /CargoTrace/box/info
Future<BoxInfoResult> fetchBoxInfo({
required String baseUrl,
required String zongpaiNo,
}) async {
final uri = Uri.parse('$baseUrl/CargoTrace/box/info').replace(
queryParameters: {'zongpai_no': zongpaiNo},
);
try {
final response = await _client
.get(uri, headers: {'Content-Type': 'application/json'})
.timeout(timeout);
switch (response.statusCode) {
case 200:
final body = jsonDecode(response.body) as Map<String, dynamic>;
return BoxInfoResult.ok(body);
case 400:
return BoxInfoResult.error('无效的总排号格式');
case 404:
return BoxInfoResult.error('未找到该总排号对应的排产号信息');
default:
return BoxInfoResult.error('查询失败 (${response.statusCode})');
}
} catch (e) {
return BoxInfoResult.error('网络异常,请检查网络连接');
}
}
/// 保存装箱记录 — POST /CargoTrace/box
Future<BoxSaveResult> saveBoxRecord({
required String baseUrl,
required String zongpaiNo,
required int boxNo,
required int quantity,
}) async {
final uri = Uri.parse('$baseUrl/CargoTrace/box');
try {
final response = await _client
.post(
uri,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'zongpai_no': zongpaiNo,
'box_no': boxNo,
'quantity': quantity,
}),
)
.timeout(timeout);
switch (response.statusCode) {
case 200:
final body = jsonDecode(response.body) as Map<String, dynamic>;
return BoxSaveResult.ok(body);
case 400:
final body = jsonDecode(response.body) as Map<String, dynamic>;
final msg = body['message']?.toString() ?? '请求参数错误';
return BoxSaveResult.error(msg);
case 409:
final body = jsonDecode(response.body) as Map<String, dynamic>;
return BoxSaveResult.duplicate(body);
case 404:
return BoxSaveResult.error('未找到该总排号对应的排产号信息');
default:
return BoxSaveResult.error('提交失败 (${response.statusCode})');
}
} catch (e) {
return BoxSaveResult.error('网络异常,请检查网络连接');
}
}
/// Test connectivity by making a HEAD request to the base URL.
Future<bool> testConnection(String baseUrl) async {
try {