Files
pad_scanner/lib/services/api_service.dart
Misaka 9dc714dbeb feat: extend boxing page for accessory boxing support
Add pending accessories section to both single-code and multi-code boxing
modes. Accessories display with orange styling and a "加入本箱" button that
calls saveBoxRecord with item_type=accessory. Extend API service with
PendingAccessory model, itemType fields on data classes, and optional
accessory_id parameter on saveBoxRecord. Show accessories with visual
indicator (wrench icon + orange background) in boxing detail page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-24 18:01:01 +08:00

1137 lines
34 KiB
Dart

import 'dart:convert';
import 'package:http/http.dart' as http;
/// Result of a registration API call.
class RegistrationResult {
final bool success;
final bool isDuplicate;
final bool isAlreadyOffShelf;
final bool isOffShelfSuccess;
final String? errorMessage;
final Map<String, dynamic>? duplicateInfo;
final Map<String, dynamic>? conflictInfo;
RegistrationResult({
required this.success,
this.isDuplicate = false,
this.isAlreadyOffShelf = false,
this.isOffShelfSuccess = false,
this.errorMessage,
this.duplicateInfo,
this.conflictInfo,
});
factory RegistrationResult.ok() => RegistrationResult(success: true);
factory RegistrationResult.offShelfSuccess() =>
RegistrationResult(success: true, isOffShelfSuccess: true);
factory RegistrationResult.duplicate(Map<String, dynamic> info) =>
RegistrationResult(
success: false,
isDuplicate: true,
duplicateInfo: info,
conflictInfo: info,
);
factory RegistrationResult.alreadyOffShelf(Map<String, dynamic> info) =>
RegistrationResult(
success: false,
isAlreadyOffShelf: true,
errorMessage: info['message']?.toString() ?? '该总排号已下架至转运区域,不可重新上架',
conflictInfo: info,
);
factory RegistrationResult.error(String message) =>
RegistrationResult(success: false, errorMessage: message);
}
class PaichaOverviewItem {
final String zongpaiNo;
final String? workOrderNo;
final int quantity;
final String? locationCode;
final String status;
PaichaOverviewItem({
required this.zongpaiNo,
this.workOrderNo,
required this.quantity,
this.locationCode,
required this.status,
});
factory PaichaOverviewItem.fromJson(Map<String, dynamic> json) {
return PaichaOverviewItem(
zongpaiNo: json['zongpai_no'] as String,
workOrderNo: json['work_order_no']?.toString(),
quantity: json['quantity'] as int? ?? 0,
locationCode: json['location_code']?.toString(),
status: json['status']?.toString() ?? 'not_shelved',
);
}
}
class PaichaOverviewResult {
final bool success;
final bool notFound;
final String? errorMessage;
final String? paichaNo;
final int totalCount;
final List<PaichaOverviewItem> items;
PaichaOverviewResult({
required this.success,
this.notFound = false,
this.errorMessage,
this.paichaNo,
this.totalCount = 0,
this.items = const [],
});
factory PaichaOverviewResult.ok(Map<String, dynamic> json) {
final items =
(json['items'] as List<dynamic>?)
?.map(
(item) =>
PaichaOverviewItem.fromJson(item as Map<String, dynamic>),
)
.toList() ??
[];
return PaichaOverviewResult(
success: true,
paichaNo: json['paicha_no'] as String?,
totalCount: json['total_count'] as int? ?? items.length,
items: items,
);
}
factory PaichaOverviewResult.notFoundResult() => PaichaOverviewResult(
success: false,
notFound: true,
errorMessage: '暂无排产信息',
);
factory PaichaOverviewResult.error(String message) =>
PaichaOverviewResult(success: false, errorMessage: message);
}
// === 装箱模块数据类 ===
/// 待装箱附件
class PendingAccessory {
final int accessoryId;
final String accessoryType;
final int quantity;
final String? locationCode;
PendingAccessory({
required this.accessoryId,
required this.accessoryType,
required this.quantity,
this.locationCode,
});
factory PendingAccessory.fromJson(Map<String, dynamic> json) {
return PendingAccessory(
accessoryId: json['accessory_id'] as int,
accessoryType: json['accessory_type'] as String,
quantity: json['quantity'] as int,
locationCode: json['location_code']?.toString(),
);
}
}
/// 箱号内单个总排号明细
class BoxItemData {
final int? boxItemId;
final String zongpaiNo;
final String? workOrderNo;
final int quantity;
final int? totalQuantity;
final String itemType;
BoxItemData({
this.boxItemId,
required this.zongpaiNo,
this.workOrderNo,
required this.quantity,
this.totalQuantity,
this.itemType = 'product',
});
factory BoxItemData.fromJson(Map<String, dynamic> json) {
return BoxItemData(
boxItemId: json['box_item_id'] as int?,
zongpaiNo: json['zongpai_no'] as String,
workOrderNo: json['work_order_no']?.toString(),
quantity: json['quantity'] as int,
totalQuantity: json['total_quantity'] as int?,
itemType: json['item_type'] as String? ?? 'product',
);
}
}
/// 箱号明细
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 CurrentZongpaiBoxData {
final int boxItemId;
final int boxNo;
final int quantity;
final String itemType;
CurrentZongpaiBoxData({
required this.boxItemId,
required this.boxNo,
required this.quantity,
this.itemType = 'product',
});
factory CurrentZongpaiBoxData.fromJson(Map<String, dynamic> json) {
return CurrentZongpaiBoxData(
boxItemId: json['box_item_id'] as int,
boxNo: json['box_no'] as int,
quantity: json['quantity'] as int,
itemType: json['item_type'] as String? ?? 'product',
);
}
}
/// 装箱信息查询结果
class BoxInfoResult {
final bool success;
final String? errorMessage;
final String? zongpaiNo;
final String? paichanNo;
final String? workOrderNo;
final int? quantity;
final List<CurrentZongpaiBoxData> currentZongpaiBoxes;
final List<BoxDetailData> existingBoxes;
final int maxBoxNo;
final int suggestedBoxNo;
final List<PendingAccessory> pendingAccessories;
BoxInfoResult({
required this.success,
this.errorMessage,
this.zongpaiNo,
this.paichanNo,
this.workOrderNo,
this.quantity,
this.currentZongpaiBoxes = const [],
this.existingBoxes = const [],
this.maxBoxNo = 0,
this.suggestedBoxNo = 1,
this.pendingAccessories = const [],
});
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() ??
[];
final currentBoxes =
(json['current_zongpai_boxes'] as List<dynamic>?)
?.map(
(b) => CurrentZongpaiBoxData.fromJson(b as Map<String, dynamic>),
)
.toList() ??
[];
final pendingAccessories =
(json['pending_accessories'] as List<dynamic>?)
?.map((a) => PendingAccessory.fromJson(a 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?,
currentZongpaiBoxes: currentBoxes,
existingBoxes: boxes,
maxBoxNo: json['max_box_no'] as int? ?? 0,
suggestedBoxNo: json['suggested_box_no'] as int? ?? 1,
pendingAccessories: pendingAccessories,
);
}
factory BoxInfoResult.error(String message) {
return BoxInfoResult(success: false, errorMessage: message);
}
}
/// 装箱保存结果
class BoxSaveResult {
final bool success;
final bool isDuplicate;
final String? errorMessage;
final String? errorCode;
final int? boxItemId;
final String? paichanNo;
final String? zongpaiNo;
final int? boxNo;
final int? quantity;
BoxSaveResult({
required this.success,
this.isDuplicate = false,
this.errorMessage,
this.errorCode,
this.boxItemId,
this.paichanNo,
this.zongpaiNo,
this.boxNo,
this.quantity,
});
factory BoxSaveResult.ok(Map<String, dynamic> json) {
return BoxSaveResult(
success: true,
boxItemId: json['box_item_id'] as int?,
paichanNo: json['paichan_no'] as String?,
zongpaiNo: json['zongpai_no'] as String?,
boxNo: json['box_no'] as int?,
quantity: json['quantity'] as int?,
);
}
factory BoxSaveResult.duplicate(Map<String, dynamic> json) {
return BoxSaveResult(
success: false,
isDuplicate: true,
errorCode: json['error_code']?.toString(),
errorMessage: json['message']?.toString(),
paichanNo: json['paichan_no'] as String?,
zongpaiNo: json['zongpai_no'] as String?,
boxNo: json['box_no'] as int?,
);
}
factory BoxSaveResult.error(String message) {
return BoxSaveResult(success: false, errorMessage: message);
}
}
class BoxDeleteResult {
final bool success;
final String? errorMessage;
BoxDeleteResult({required this.success, this.errorMessage});
}
// === 附件模块数据类 ===
/// 工令号分组(工令号 → 总排号列表)
class WorkOrderGroup {
final String workOrderNo;
final List<String> zongpaiNos;
WorkOrderGroup({required this.workOrderNo, required this.zongpaiNos});
factory WorkOrderGroup.fromJson(Map<String, dynamic> json) => WorkOrderGroup(
workOrderNo: json['work_order_no'] as String,
zongpaiNos: (json['zongpai_nos'] as List).map((e) => e as String).toList(),
);
}
/// 附件类型预设
class AccessoryTypeInfo {
final int id;
final String name;
final int sortOrder;
AccessoryTypeInfo({
required this.id,
required this.name,
required this.sortOrder,
});
factory AccessoryTypeInfo.fromJson(Map<String, dynamic> json) =>
AccessoryTypeInfo(
id: json['id'] as int,
name: json['name'] as String,
sortOrder: json['sort_order'] as int,
);
}
/// 附件记录
class AccessoryRecord {
final int id;
final String paichanNo;
final String zongpaiNo;
final String accessoryType;
final int quantity;
final String? locationCode;
final bool isBoxed;
final String createdAt;
AccessoryRecord({
required this.id,
required this.paichanNo,
required this.zongpaiNo,
required this.accessoryType,
required this.quantity,
this.locationCode,
required this.isBoxed,
required this.createdAt,
});
factory AccessoryRecord.fromJson(Map<String, dynamic> json) =>
AccessoryRecord(
id: json['id'] as int,
paichanNo: json['paichan_no'] as String,
zongpaiNo: json['zongpai_no'] as String,
accessoryType: json['accessory_type'] as String,
quantity: json['quantity'] as int,
locationCode: json['location_code'] as String?,
isBoxed: json['is_boxed'] as bool,
createdAt: json['created_at'] as String,
);
}
// === 附件模块结果类 ===
class WorkOrderQueryResult {
final bool success;
final String? paichanNo;
final List<WorkOrderGroup> workOrders;
final String? errorMessage;
final String? errorCode;
WorkOrderQueryResult({
required this.success,
this.paichanNo,
this.workOrders = const [],
this.errorMessage,
this.errorCode,
});
}
class AccessoryListResult {
final bool success;
final List<AccessoryRecord> items;
final String? errorMessage;
final String? errorCode;
AccessoryListResult({
required this.success,
this.items = const [],
this.errorMessage,
this.errorCode,
});
}
class AccessoryCreateResult {
final bool success;
final AccessoryRecord? record;
final String? errorMessage;
final String? errorCode;
AccessoryCreateResult({
required this.success,
this.record,
this.errorMessage,
this.errorCode,
});
}
class AccessoryUpdateResult {
final bool success;
final AccessoryRecord? record;
final String? errorMessage;
final String? errorCode;
AccessoryUpdateResult({
required this.success,
this.record,
this.errorMessage,
this.errorCode,
});
}
class AccessoryDeleteResult {
final bool success;
final String? errorMessage;
final String? errorCode;
AccessoryDeleteResult({
required this.success,
this.errorMessage,
this.errorCode,
});
}
class AccessoryTypeListResult {
final bool success;
final List<AccessoryTypeInfo> types;
final String? errorMessage;
final String? errorCode;
AccessoryTypeListResult({
required this.success,
this.types = const [],
this.errorMessage,
this.errorCode,
});
}
class AccessoryTypeCreateResult {
final bool success;
final AccessoryTypeInfo? type;
final String? errorMessage;
final String? errorCode;
AccessoryTypeCreateResult({
required this.success,
this.type,
this.errorMessage,
this.errorCode,
});
}
class ApiService {
final http.Client _client;
final Duration timeout;
ApiService({http.Client? client, this.timeout = const Duration(seconds: 5)})
: _client = client ?? http.Client();
/// Submit a shelf registration (上架登记).
Future<RegistrationResult> registerLocation({
required String baseUrl,
required String zongpaiNo,
required String locationCode,
}) async {
final uri = Uri.parse('$baseUrl/CargoTrace/location');
try {
final response = await _client
.post(
uri,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'zongpai_no': zongpaiNo,
'location_code': locationCode,
}),
)
.timeout(timeout);
switch (response.statusCode) {
case 200:
final body = jsonDecode(response.body) as Map<String, dynamic>;
if (body['previous_location'] != null) {
return RegistrationResult.offShelfSuccess();
}
return RegistrationResult.ok();
case 400:
final body = jsonDecode(response.body) as Map<String, dynamic>;
final msg = body['message']?.toString() ?? '请求参数错误';
return RegistrationResult.error(msg);
case 409:
final body = jsonDecode(response.body) as Map<String, dynamic>;
final errorCode = body['error_code']?.toString();
if (errorCode == 'DUPLICATE_LOCATION') {
return RegistrationResult.duplicate(body);
}
if (errorCode == 'ALREADY_OFF_SHELF') {
return RegistrationResult.alreadyOffShelf(body);
}
final msg = body['message']?.toString() ?? '提交失败';
return RegistrationResult.error(msg);
default:
final body = jsonDecode(response.body) as Map<String, dynamic>;
final msg =
body['message'] ??
body['error'] ??
'Unknown error (${response.statusCode})';
return RegistrationResult.error(msg.toString());
}
} catch (e) {
return RegistrationResult.error('网络异常,请检查网络连接');
}
}
Future<PaichaOverviewResult> fetchPaichaOverview({
required String baseUrl,
required String zongpaiNo,
}) async {
final uri = Uri.parse(
'$baseUrl/CargoTrace/location/paicha-overview',
).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 PaichaOverviewResult.ok(body);
case 400:
return PaichaOverviewResult.error('无效的总排号格式');
case 404:
return PaichaOverviewResult.notFoundResult();
default:
return PaichaOverviewResult.error('加载失败,点击重试');
}
} catch (e) {
return PaichaOverviewResult.error('加载失败,点击重试');
}
}
/// 查询装箱信息 — 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,
String itemType = 'product',
int? accessoryId,
}) async {
final uri = Uri.parse('$baseUrl/CargoTrace/box');
try {
final bodyMap = <String, dynamic>{
'zongpai_no': zongpaiNo,
'box_no': boxNo,
'quantity': quantity,
'item_type': itemType,
};
if (accessoryId != null) {
bodyMap['accessory_id'] = accessoryId;
}
final response = await _client
.post(
uri,
headers: {'Content-Type': 'application/json'},
body: jsonEncode(bodyMap),
)
.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('网络异常,请检查网络连接');
}
}
/// 更新装箱明细 — PATCH /CargoTrace/box/{boxItemId}
Future<BoxSaveResult> updateBoxRecord({
required String baseUrl,
required int boxItemId,
required int boxNo,
required int quantity,
}) async {
final uri = Uri.parse('$baseUrl/CargoTrace/box/$boxItemId');
try {
final response = await _client
.patch(
uri,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'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('网络异常,请检查网络连接');
}
}
/// 删除装箱明细 — DELETE /CargoTrace/box/{boxItemId}
Future<BoxDeleteResult> deleteBoxRecord({
required String baseUrl,
required int boxItemId,
}) async {
final uri = Uri.parse('$baseUrl/CargoTrace/box/$boxItemId');
try {
final response = await _client
.delete(uri, headers: {'Content-Type': 'application/json'})
.timeout(timeout);
switch (response.statusCode) {
case 200:
return BoxDeleteResult(success: true);
case 404:
return BoxDeleteResult(success: false, errorMessage: '指定装箱明细不存在');
default:
return BoxDeleteResult(
success: false,
errorMessage: '删除失败 (${response.statusCode})',
);
}
} catch (e) {
return BoxDeleteResult(success: false, errorMessage: '网络异常,请检查网络连接');
}
}
// === 附件模块 API 方法 ===
/// 工令号查询 — GET /CargoTrace/accessory/work-orders
Future<WorkOrderQueryResult> queryWorkOrders({
required String baseUrl,
required String paichanNo,
}) async {
final uri = Uri.parse(
'$baseUrl/CargoTrace/accessory/work-orders',
).replace(queryParameters: {'paichan_no': paichanNo});
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>;
final workOrders =
(body['work_orders'] as List<dynamic>?)
?.map(
(w) => WorkOrderGroup.fromJson(w as Map<String, dynamic>),
)
.toList() ??
[];
return WorkOrderQueryResult(
success: true,
paichanNo: body['paichan_no'] as String?,
workOrders: workOrders,
);
case 400:
final body = jsonDecode(response.body) as Map<String, dynamic>;
return WorkOrderQueryResult(
success: false,
errorMessage: body['message']?.toString() ?? '请求参数错误',
errorCode: body['error_code']?.toString(),
);
case 404:
return WorkOrderQueryResult(
success: false,
errorMessage: '未找到该排产号对应的工令号信息',
errorCode: 'NOT_FOUND',
);
default:
return WorkOrderQueryResult(
success: false,
errorMessage: '查询失败 (${response.statusCode})',
);
}
} catch (e) {
return WorkOrderQueryResult(success: false, errorMessage: '网络异常,请检查网络连接');
}
}
/// 附件列表查询 — GET /CargoTrace/accessory
Future<AccessoryListResult> listAccessories({
required String baseUrl,
required String paichanNo,
}) async {
final uri = Uri.parse(
'$baseUrl/CargoTrace/accessory',
).replace(queryParameters: {'paichan_no': paichanNo});
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>;
final items =
(body['items'] as List<dynamic>?)
?.map(
(i) => AccessoryRecord.fromJson(i as Map<String, dynamic>),
)
.toList() ??
[];
return AccessoryListResult(success: true, items: items);
case 400:
final body = jsonDecode(response.body) as Map<String, dynamic>;
return AccessoryListResult(
success: false,
errorMessage: body['message']?.toString() ?? '请求参数错误',
errorCode: body['error_code']?.toString(),
);
case 404:
return AccessoryListResult(
success: false,
errorMessage: '未找到该排产号信息',
errorCode: 'NOT_FOUND',
);
default:
return AccessoryListResult(
success: false,
errorMessage: '查询失败 (${response.statusCode})',
);
}
} catch (e) {
return AccessoryListResult(success: false, errorMessage: '网络异常,请检查网络连接');
}
}
/// 附件创建(上架) — POST /CargoTrace/accessory
Future<AccessoryCreateResult> createAccessory({
required String baseUrl,
required String paichanNo,
required String zongpaiNo,
required String accessoryType,
required int quantity,
String? locationCode,
}) async {
final uri = Uri.parse('$baseUrl/CargoTrace/accessory');
try {
final bodyMap = <String, dynamic>{
'paichan_no': paichanNo,
'zongpai_no': zongpaiNo,
'accessory_type': accessoryType,
'quantity': quantity,
};
if (locationCode != null) {
bodyMap['location_code'] = locationCode;
}
final response = await _client
.post(
uri,
headers: {'Content-Type': 'application/json'},
body: jsonEncode(bodyMap),
)
.timeout(timeout);
switch (response.statusCode) {
case 200:
final body = jsonDecode(response.body) as Map<String, dynamic>;
return AccessoryCreateResult(
success: true,
record: AccessoryRecord.fromJson(body),
);
case 400:
final body = jsonDecode(response.body) as Map<String, dynamic>;
return AccessoryCreateResult(
success: false,
errorMessage: body['message']?.toString() ?? '请求参数错误',
errorCode: body['error_code']?.toString(),
);
case 404:
return AccessoryCreateResult(
success: false,
errorMessage: '未找到该排产号对应的工令号信息',
errorCode: 'NOT_FOUND',
);
case 409:
final body = jsonDecode(response.body) as Map<String, dynamic>;
return AccessoryCreateResult(
success: false,
errorMessage: body['message']?.toString() ?? '冲突',
errorCode: body['error_code']?.toString(),
);
default:
return AccessoryCreateResult(
success: false,
errorMessage: '创建失败 (${response.statusCode})',
);
}
} catch (e) {
return AccessoryCreateResult(
success: false,
errorMessage: '网络异常,请检查网络连接',
);
}
}
/// 附件更新 — PATCH /CargoTrace/accessory/{id}
Future<AccessoryUpdateResult> updateAccessory({
required String baseUrl,
required int id,
String? accessoryType,
int? quantity,
String? locationCode,
}) async {
final uri = Uri.parse('$baseUrl/CargoTrace/accessory/$id');
try {
final bodyMap = <String, dynamic>{};
if (accessoryType != null) bodyMap['accessory_type'] = accessoryType;
if (quantity != null) bodyMap['quantity'] = quantity;
if (locationCode != null) bodyMap['location_code'] = locationCode;
final response = await _client
.patch(
uri,
headers: {'Content-Type': 'application/json'},
body: jsonEncode(bodyMap),
)
.timeout(timeout);
switch (response.statusCode) {
case 200:
final body = jsonDecode(response.body) as Map<String, dynamic>;
return AccessoryUpdateResult(
success: true,
record: AccessoryRecord.fromJson(body),
);
case 400:
final body = jsonDecode(response.body) as Map<String, dynamic>;
return AccessoryUpdateResult(
success: false,
errorMessage: body['message']?.toString() ?? '请求参数错误',
errorCode: body['error_code']?.toString(),
);
case 404:
return AccessoryUpdateResult(
success: false,
errorMessage: '指定附件记录不存在',
errorCode: 'NOT_FOUND',
);
default:
return AccessoryUpdateResult(
success: false,
errorMessage: '更新失败 (${response.statusCode})',
);
}
} catch (e) {
return AccessoryUpdateResult(
success: false,
errorMessage: '网络异常,请检查网络连接',
);
}
}
/// 附件删除 — DELETE /CargoTrace/accessory/{id}
Future<AccessoryDeleteResult> deleteAccessory({
required String baseUrl,
required int id,
}) async {
final uri = Uri.parse('$baseUrl/CargoTrace/accessory/$id');
try {
final response = await _client
.delete(uri, headers: {'Content-Type': 'application/json'})
.timeout(timeout);
switch (response.statusCode) {
case 200:
return AccessoryDeleteResult(success: true);
case 404:
return AccessoryDeleteResult(
success: false,
errorMessage: '指定附件记录不存在',
errorCode: 'NOT_FOUND',
);
default:
return AccessoryDeleteResult(
success: false,
errorMessage: '删除失败 (${response.statusCode})',
);
}
} catch (e) {
return AccessoryDeleteResult(
success: false,
errorMessage: '网络异常,请检查网络连接',
);
}
}
/// 附件类型列表 — GET /CargoTrace/accessory-type
Future<AccessoryTypeListResult> listAccessoryTypes({
required String baseUrl,
}) async {
final uri = Uri.parse('$baseUrl/CargoTrace/accessory-type');
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>;
final types =
(body['types'] as List<dynamic>?)
?.map(
(t) =>
AccessoryTypeInfo.fromJson(t as Map<String, dynamic>),
)
.toList() ??
[];
return AccessoryTypeListResult(success: true, types: types);
case 400:
final body = jsonDecode(response.body) as Map<String, dynamic>;
return AccessoryTypeListResult(
success: false,
errorMessage: body['message']?.toString() ?? '请求参数错误',
errorCode: body['error_code']?.toString(),
);
default:
return AccessoryTypeListResult(
success: false,
errorMessage: '查询失败 (${response.statusCode})',
);
}
} catch (e) {
return AccessoryTypeListResult(
success: false,
errorMessage: '网络异常,请检查网络连接',
);
}
}
/// 附件类型创建 — POST /CargoTrace/accessory-type
Future<AccessoryTypeCreateResult> createAccessoryType({
required String baseUrl,
required String name,
int sortOrder = 0,
}) async {
final uri = Uri.parse('$baseUrl/CargoTrace/accessory-type');
try {
final response = await _client
.post(
uri,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'name': name, 'sort_order': sortOrder}),
)
.timeout(timeout);
switch (response.statusCode) {
case 200:
final body = jsonDecode(response.body) as Map<String, dynamic>;
return AccessoryTypeCreateResult(
success: true,
type: AccessoryTypeInfo.fromJson(body),
);
case 400:
final body = jsonDecode(response.body) as Map<String, dynamic>;
return AccessoryTypeCreateResult(
success: false,
errorMessage: body['message']?.toString() ?? '请求参数错误',
errorCode: body['error_code']?.toString(),
);
case 409:
final body = jsonDecode(response.body) as Map<String, dynamic>;
return AccessoryTypeCreateResult(
success: false,
errorMessage: body['message']?.toString() ?? '附件类型已存在',
errorCode: body['error_code']?.toString(),
);
default:
return AccessoryTypeCreateResult(
success: false,
errorMessage: '创建失败 (${response.statusCode})',
);
}
} catch (e) {
return AccessoryTypeCreateResult(
success: false,
errorMessage: '网络异常,请检查网络连接',
);
}
}
/// 附件类型删除 — DELETE /CargoTrace/accessory-type/{id}
Future<AccessoryDeleteResult> deleteAccessoryType({
required String baseUrl,
required int id,
}) async {
final uri = Uri.parse('$baseUrl/CargoTrace/accessory-type/$id');
try {
final response = await _client
.delete(uri, headers: {'Content-Type': 'application/json'})
.timeout(timeout);
switch (response.statusCode) {
case 200:
return AccessoryDeleteResult(success: true);
case 404:
return AccessoryDeleteResult(
success: false,
errorMessage: '指定附件类型不存在',
errorCode: 'NOT_FOUND',
);
case 409:
final body = jsonDecode(response.body) as Map<String, dynamic>;
return AccessoryDeleteResult(
success: false,
errorMessage: body['message']?.toString() ?? '该类型下存在附件记录,无法删除',
errorCode: body['error_code']?.toString(),
);
default:
return AccessoryDeleteResult(
success: false,
errorMessage: '删除失败 (${response.statusCode})',
);
}
} catch (e) {
return AccessoryDeleteResult(
success: false,
errorMessage: '网络异常,请检查网络连接',
);
}
}
/// Test connectivity by making a HEAD request to the base URL.
Future<bool> testConnection(String baseUrl) async {
try {
final uri = Uri.parse(baseUrl);
final response = await _client.head(uri).timeout(timeout);
return response.statusCode < 500;
} catch (_) {
return false;
}
}
}