Compare commits
8 Commits
e4a8816b98
...
ce80e1a1f6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce80e1a1f6 | ||
|
|
a165bfeabe | ||
|
|
28871eb21f | ||
|
|
077a1ab1b9 | ||
|
|
42a9cc7204 | ||
|
|
396a19ff43 | ||
|
|
b32c9c5aad | ||
|
|
b975b15ba7 |
@@ -22,9 +22,10 @@ class BoxingDetailPage extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
paichanNo,
|
paichanNo,
|
||||||
|
textAlign: TextAlign.end,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.normal,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@@ -60,14 +61,37 @@ class BoxingDetailPage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBoxGroup(BuildContext context, BoxDetailData box) {
|
Widget _buildBoxGroup(BuildContext context, BoxDetailData box) {
|
||||||
|
final boxTotalQuantity = box.items.fold<int>(
|
||||||
|
0,
|
||||||
|
(sum, item) => sum + item.quantity,
|
||||||
|
);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
'箱号 ${box.boxNo}',
|
'箱号 ${box.boxNo}',
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'共 $boxTotalQuantity 件',
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Container(
|
Container(
|
||||||
@@ -102,7 +126,7 @@ class BoxingDetailPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'数量:${item.quantity}',
|
'${item.quantity}/${item.totalQuantity ?? '--'}',
|
||||||
style: const TextStyle(fontSize: 14),
|
style: const TextStyle(fontSize: 14),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -49,8 +49,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onKeyEvent(KeyEvent event) {
|
void _onKeyEvent(KeyEvent event) {
|
||||||
if (event is KeyDownEvent &&
|
if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.enter) {
|
||||||
event.logicalKey == LogicalKeyboardKey.enter) {
|
|
||||||
_submit();
|
_submit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,11 +194,24 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
_locationType = null;
|
_locationType = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
final msg = _isLocked ? '货位已锁定,请扫描下一张执行卡' : '上架成功';
|
final msg = result.isOffShelfSuccess
|
||||||
_showStatusOverride(msg, StatusDotColor.green, const Duration(milliseconds: 1500));
|
? '下架成功'
|
||||||
|
: (_isLocked ? '货位已锁定,请扫描下一张执行卡' : '上架成功');
|
||||||
|
_showStatusOverride(
|
||||||
|
msg,
|
||||||
|
StatusDotColor.green,
|
||||||
|
const Duration(milliseconds: 1500),
|
||||||
|
);
|
||||||
} else if (result.isDuplicate) {
|
} else if (result.isDuplicate) {
|
||||||
_feedbackService.trigger(FeedbackEvent.duplicateError);
|
_feedbackService.trigger(FeedbackEvent.duplicateError);
|
||||||
_showDuplicateDialog(zongpaiNo, result.duplicateInfo);
|
_showDuplicateDialog(zongpaiNo, result.duplicateInfo);
|
||||||
|
} else if (result.isAlreadyOffShelf) {
|
||||||
|
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||||||
|
_showStatusOverride(
|
||||||
|
result.errorMessage ?? '该总排号已下架至转运区域,不可重新上架',
|
||||||
|
StatusDotColor.red,
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
final isNetwork = result.errorMessage == '网络异常,请检查网络连接';
|
final isNetwork = result.errorMessage == '网络异常,请检查网络连接';
|
||||||
_feedbackService.trigger(
|
_feedbackService.trigger(
|
||||||
@@ -215,6 +227,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
|
|
||||||
Future<void> _submitBatch(String baseUrl) async {
|
Future<void> _submitBatch(String baseUrl) async {
|
||||||
int successCount = 0;
|
int successCount = 0;
|
||||||
|
int offShelfCount = 0;
|
||||||
final failed = <String>[];
|
final failed = <String>[];
|
||||||
final toRemove = <String>[];
|
final toRemove = <String>[];
|
||||||
|
|
||||||
@@ -226,6 +239,9 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
);
|
);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
successCount++;
|
successCount++;
|
||||||
|
if (result.isOffShelfSuccess) {
|
||||||
|
offShelfCount++;
|
||||||
|
}
|
||||||
toRemove.add(zp);
|
toRemove.add(zp);
|
||||||
} else {
|
} else {
|
||||||
failed.add(zp);
|
failed.add(zp);
|
||||||
@@ -238,6 +254,19 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
_showDuplicateDialog(zp, result.duplicateInfo);
|
_showDuplicateDialog(zp, result.duplicateInfo);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (result.isAlreadyOffShelf) {
|
||||||
|
setState(() {
|
||||||
|
_zongpaiNos.removeWhere((e) => toRemove.contains(e));
|
||||||
|
_isSubmitting = false;
|
||||||
|
});
|
||||||
|
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||||||
|
_showStatusOverride(
|
||||||
|
result.errorMessage ?? '该总排号已下架至转运区域,不可重新上架',
|
||||||
|
StatusDotColor.red,
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,8 +279,11 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
|
|
||||||
if (failed.isEmpty) {
|
if (failed.isEmpty) {
|
||||||
_feedbackService.trigger(FeedbackEvent.submitSuccess);
|
_feedbackService.trigger(FeedbackEvent.submitSuccess);
|
||||||
|
final msg = offShelfCount == successCount
|
||||||
|
? '批量下架成功($successCount 条)'
|
||||||
|
: '批量上架成功($successCount 条)';
|
||||||
_showStatusOverride(
|
_showStatusOverride(
|
||||||
'批量上架成功($successCount 条)',
|
msg,
|
||||||
StatusDotColor.green,
|
StatusDotColor.green,
|
||||||
const Duration(milliseconds: 1500),
|
const Duration(milliseconds: 1500),
|
||||||
);
|
);
|
||||||
@@ -492,7 +524,9 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(14),
|
padding: const EdgeInsets.all(14),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: Colors.grey.shade400),
|
border: Border.all(
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
@@ -509,7 +543,9 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return Dismissible(
|
return Dismissible(
|
||||||
key: ValueKey('${_zongpaiNos[index]}-$index'),
|
key: ValueKey(
|
||||||
|
'${_zongpaiNos[index]}-$index',
|
||||||
|
),
|
||||||
direction: DismissDirection.endToStart,
|
direction: DismissDirection.endToStart,
|
||||||
onDismissed: (_) => _removeZongpai(index),
|
onDismissed: (_) => _removeZongpai(index),
|
||||||
background: Container(
|
background: Container(
|
||||||
|
|||||||
@@ -57,14 +57,27 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _saveUrl() async {
|
Future<void> _saveUrl() async {
|
||||||
final url = _controller.text.trim();
|
final url = _controller.text.trim().replaceAll(RegExp(r'/+$'), '');
|
||||||
if (url.isEmpty) {
|
if (url.isEmpty) {
|
||||||
_showSnackBar('请输入 API 地址', isError: true);
|
_showSnackBar('请输入 API 地址', isError: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() => _saving = true);
|
setState(() => _saving = true);
|
||||||
|
// Load config once, apply all changes, then save once to avoid race conditions
|
||||||
final configService = AppConfigService();
|
final configService = AppConfigService();
|
||||||
await configService.setString('api_url', url);
|
final config = await configService.loadConfig();
|
||||||
|
config['api_url'] = url;
|
||||||
|
if (_successPath != null) config['sound_success'] = _successPath;
|
||||||
|
else config.remove('sound_success');
|
||||||
|
if (_failurePath != null) config['sound_failure'] = _failurePath;
|
||||||
|
else config.remove('sound_failure');
|
||||||
|
if (_beepPath != null) config['sound_beep'] = _beepPath;
|
||||||
|
else config.remove('sound_beep');
|
||||||
|
if (_errorPath != null) config['sound_error'] = _errorPath;
|
||||||
|
else config.remove('sound_error');
|
||||||
|
if (_alertPath != null) config['sound_alert'] = _alertPath;
|
||||||
|
else config.remove('sound_alert');
|
||||||
|
await configService.saveConfig(config);
|
||||||
setState(() => _saving = false);
|
setState(() => _saving = false);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_showSnackBar('设置已保存');
|
_showSnackBar('设置已保存');
|
||||||
@@ -91,18 +104,6 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
);
|
);
|
||||||
if (result != null && result.files.single.path != null) {
|
if (result != null && result.files.single.path != null) {
|
||||||
final path = result.files.single.path!;
|
final path = result.files.single.path!;
|
||||||
switch (key) {
|
|
||||||
case 'success':
|
|
||||||
await _soundService.setSuccessPath(path);
|
|
||||||
case 'failure':
|
|
||||||
await _soundService.setFailurePath(path);
|
|
||||||
case 'beep':
|
|
||||||
await _soundService.setBeepPath(path);
|
|
||||||
case 'error':
|
|
||||||
await _soundService.setErrorPath(path);
|
|
||||||
case 'alert':
|
|
||||||
await _soundService.setAlertPath(path);
|
|
||||||
}
|
|
||||||
setState(() {
|
setState(() {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'success':
|
case 'success':
|
||||||
@@ -121,18 +122,6 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _clearSound(String key) async {
|
Future<void> _clearSound(String key) async {
|
||||||
switch (key) {
|
|
||||||
case 'success':
|
|
||||||
await _soundService.setSuccessPath(null);
|
|
||||||
case 'failure':
|
|
||||||
await _soundService.setFailurePath(null);
|
|
||||||
case 'beep':
|
|
||||||
await _soundService.setBeepPath(null);
|
|
||||||
case 'error':
|
|
||||||
await _soundService.setErrorPath(null);
|
|
||||||
case 'alert':
|
|
||||||
await _soundService.setAlertPath(null);
|
|
||||||
}
|
|
||||||
setState(() {
|
setState(() {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'success':
|
case 'success':
|
||||||
|
|||||||
@@ -5,23 +5,41 @@ import 'package:http/http.dart' as http;
|
|||||||
class RegistrationResult {
|
class RegistrationResult {
|
||||||
final bool success;
|
final bool success;
|
||||||
final bool isDuplicate;
|
final bool isDuplicate;
|
||||||
|
final bool isAlreadyOffShelf;
|
||||||
|
final bool isOffShelfSuccess;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
final Map<String, dynamic>? duplicateInfo;
|
final Map<String, dynamic>? duplicateInfo;
|
||||||
|
final Map<String, dynamic>? conflictInfo;
|
||||||
|
|
||||||
RegistrationResult({
|
RegistrationResult({
|
||||||
required this.success,
|
required this.success,
|
||||||
this.isDuplicate = false,
|
this.isDuplicate = false,
|
||||||
|
this.isAlreadyOffShelf = false,
|
||||||
|
this.isOffShelfSuccess = false,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
this.duplicateInfo,
|
this.duplicateInfo,
|
||||||
|
this.conflictInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory RegistrationResult.ok() => RegistrationResult(success: true);
|
factory RegistrationResult.ok() => RegistrationResult(success: true);
|
||||||
|
|
||||||
|
factory RegistrationResult.offShelfSuccess() =>
|
||||||
|
RegistrationResult(success: true, isOffShelfSuccess: true);
|
||||||
|
|
||||||
factory RegistrationResult.duplicate(Map<String, dynamic> info) =>
|
factory RegistrationResult.duplicate(Map<String, dynamic> info) =>
|
||||||
RegistrationResult(
|
RegistrationResult(
|
||||||
success: false,
|
success: false,
|
||||||
isDuplicate: true,
|
isDuplicate: true,
|
||||||
duplicateInfo: info,
|
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) =>
|
factory RegistrationResult.error(String message) =>
|
||||||
@@ -32,21 +50,27 @@ class RegistrationResult {
|
|||||||
|
|
||||||
/// 箱号内单个总排号明细
|
/// 箱号内单个总排号明细
|
||||||
class BoxItemData {
|
class BoxItemData {
|
||||||
|
final int? boxItemId;
|
||||||
final String zongpaiNo;
|
final String zongpaiNo;
|
||||||
final String? workOrderNo;
|
final String? workOrderNo;
|
||||||
final int quantity;
|
final int quantity;
|
||||||
|
final int? totalQuantity;
|
||||||
|
|
||||||
BoxItemData({
|
BoxItemData({
|
||||||
|
this.boxItemId,
|
||||||
required this.zongpaiNo,
|
required this.zongpaiNo,
|
||||||
this.workOrderNo,
|
this.workOrderNo,
|
||||||
required this.quantity,
|
required this.quantity,
|
||||||
|
this.totalQuantity,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory BoxItemData.fromJson(Map<String, dynamic> json) {
|
factory BoxItemData.fromJson(Map<String, dynamic> json) {
|
||||||
return BoxItemData(
|
return BoxItemData(
|
||||||
|
boxItemId: json['box_item_id'] as int?,
|
||||||
zongpaiNo: json['zongpai_no'] as String,
|
zongpaiNo: json['zongpai_no'] as String,
|
||||||
workOrderNo: json['work_order_no']?.toString(),
|
workOrderNo: json['work_order_no']?.toString(),
|
||||||
quantity: json['quantity'] as int,
|
quantity: json['quantity'] as int,
|
||||||
|
totalQuantity: json['total_quantity'] as int?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,6 +92,27 @@ class BoxDetailData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 当前总排号已分配的装箱明细
|
||||||
|
class CurrentZongpaiBoxData {
|
||||||
|
final int boxItemId;
|
||||||
|
final int boxNo;
|
||||||
|
final int quantity;
|
||||||
|
|
||||||
|
CurrentZongpaiBoxData({
|
||||||
|
required this.boxItemId,
|
||||||
|
required this.boxNo,
|
||||||
|
required this.quantity,
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 装箱信息查询结果
|
/// 装箱信息查询结果
|
||||||
class BoxInfoResult {
|
class BoxInfoResult {
|
||||||
final bool success;
|
final bool success;
|
||||||
@@ -76,6 +121,7 @@ class BoxInfoResult {
|
|||||||
final String? paichanNo;
|
final String? paichanNo;
|
||||||
final String? workOrderNo;
|
final String? workOrderNo;
|
||||||
final int? quantity;
|
final int? quantity;
|
||||||
|
final List<CurrentZongpaiBoxData> currentZongpaiBoxes;
|
||||||
final List<BoxDetailData> existingBoxes;
|
final List<BoxDetailData> existingBoxes;
|
||||||
final int maxBoxNo;
|
final int maxBoxNo;
|
||||||
final int suggestedBoxNo;
|
final int suggestedBoxNo;
|
||||||
@@ -87,6 +133,7 @@ class BoxInfoResult {
|
|||||||
this.paichanNo,
|
this.paichanNo,
|
||||||
this.workOrderNo,
|
this.workOrderNo,
|
||||||
this.quantity,
|
this.quantity,
|
||||||
|
this.currentZongpaiBoxes = const [],
|
||||||
this.existingBoxes = const [],
|
this.existingBoxes = const [],
|
||||||
this.maxBoxNo = 0,
|
this.maxBoxNo = 0,
|
||||||
this.suggestedBoxNo = 1,
|
this.suggestedBoxNo = 1,
|
||||||
@@ -98,12 +145,20 @@ class BoxInfoResult {
|
|||||||
?.map((b) => BoxDetailData.fromJson(b as Map<String, dynamic>))
|
?.map((b) => BoxDetailData.fromJson(b as Map<String, dynamic>))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[];
|
[];
|
||||||
|
final currentBoxes =
|
||||||
|
(json['current_zongpai_boxes'] as List<dynamic>?)
|
||||||
|
?.map(
|
||||||
|
(b) => CurrentZongpaiBoxData.fromJson(b as Map<String, dynamic>),
|
||||||
|
)
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
return BoxInfoResult(
|
return BoxInfoResult(
|
||||||
success: true,
|
success: true,
|
||||||
zongpaiNo: json['zongpai_no'] as String?,
|
zongpaiNo: json['zongpai_no'] as String?,
|
||||||
paichanNo: json['paichan_no'] as String?,
|
paichanNo: json['paichan_no'] as String?,
|
||||||
workOrderNo: json['work_order_no']?.toString(),
|
workOrderNo: json['work_order_no']?.toString(),
|
||||||
quantity: json['quantity'] as int?,
|
quantity: json['quantity'] as int?,
|
||||||
|
currentZongpaiBoxes: currentBoxes,
|
||||||
existingBoxes: boxes,
|
existingBoxes: boxes,
|
||||||
maxBoxNo: json['max_box_no'] as int? ?? 0,
|
maxBoxNo: json['max_box_no'] as int? ?? 0,
|
||||||
suggestedBoxNo: json['suggested_box_no'] as int? ?? 1,
|
suggestedBoxNo: json['suggested_box_no'] as int? ?? 1,
|
||||||
@@ -120,22 +175,33 @@ class BoxSaveResult {
|
|||||||
final bool success;
|
final bool success;
|
||||||
final bool isDuplicate;
|
final bool isDuplicate;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
final String? errorCode;
|
||||||
|
final int? boxItemId;
|
||||||
final String? paichanNo;
|
final String? paichanNo;
|
||||||
|
final String? zongpaiNo;
|
||||||
final int? boxNo;
|
final int? boxNo;
|
||||||
|
final int? quantity;
|
||||||
|
|
||||||
BoxSaveResult({
|
BoxSaveResult({
|
||||||
required this.success,
|
required this.success,
|
||||||
this.isDuplicate = false,
|
this.isDuplicate = false,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
|
this.errorCode,
|
||||||
|
this.boxItemId,
|
||||||
this.paichanNo,
|
this.paichanNo,
|
||||||
|
this.zongpaiNo,
|
||||||
this.boxNo,
|
this.boxNo,
|
||||||
|
this.quantity,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory BoxSaveResult.ok(Map<String, dynamic> json) {
|
factory BoxSaveResult.ok(Map<String, dynamic> json) {
|
||||||
return BoxSaveResult(
|
return BoxSaveResult(
|
||||||
success: true,
|
success: true,
|
||||||
|
boxItemId: json['box_item_id'] as int?,
|
||||||
paichanNo: json['paichan_no'] as String?,
|
paichanNo: json['paichan_no'] as String?,
|
||||||
|
zongpaiNo: json['zongpai_no'] as String?,
|
||||||
boxNo: json['box_no'] as int?,
|
boxNo: json['box_no'] as int?,
|
||||||
|
quantity: json['quantity'] as int?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +209,10 @@ class BoxSaveResult {
|
|||||||
return BoxSaveResult(
|
return BoxSaveResult(
|
||||||
success: false,
|
success: false,
|
||||||
isDuplicate: true,
|
isDuplicate: true,
|
||||||
|
errorCode: json['error_code']?.toString(),
|
||||||
|
errorMessage: json['message']?.toString(),
|
||||||
paichanNo: json['paichan_no'] as String?,
|
paichanNo: json['paichan_no'] as String?,
|
||||||
|
zongpaiNo: json['zongpai_no'] as String?,
|
||||||
boxNo: json['box_no'] as int?,
|
boxNo: json['box_no'] as int?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -153,6 +222,13 @@ class BoxSaveResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BoxDeleteResult {
|
||||||
|
final bool success;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
BoxDeleteResult({required this.success, this.errorMessage});
|
||||||
|
}
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
final http.Client _client;
|
final http.Client _client;
|
||||||
final Duration timeout;
|
final Duration timeout;
|
||||||
@@ -181,6 +257,10 @@ class ApiService {
|
|||||||
|
|
||||||
switch (response.statusCode) {
|
switch (response.statusCode) {
|
||||||
case 200:
|
case 200:
|
||||||
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
if (body['previous_location'] != null) {
|
||||||
|
return RegistrationResult.offShelfSuccess();
|
||||||
|
}
|
||||||
return RegistrationResult.ok();
|
return RegistrationResult.ok();
|
||||||
case 400:
|
case 400:
|
||||||
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
@@ -188,7 +268,15 @@ class ApiService {
|
|||||||
return RegistrationResult.error(msg);
|
return RegistrationResult.error(msg);
|
||||||
case 409:
|
case 409:
|
||||||
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
final errorCode = body['error_code']?.toString();
|
||||||
|
if (errorCode == 'DUPLICATE_LOCATION') {
|
||||||
return RegistrationResult.duplicate(body);
|
return RegistrationResult.duplicate(body);
|
||||||
|
}
|
||||||
|
if (errorCode == 'ALREADY_OFF_SHELF') {
|
||||||
|
return RegistrationResult.alreadyOffShelf(body);
|
||||||
|
}
|
||||||
|
final msg = body['message']?.toString() ?? '提交失败';
|
||||||
|
return RegistrationResult.error(msg);
|
||||||
default:
|
default:
|
||||||
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
final msg =
|
final msg =
|
||||||
@@ -237,7 +325,6 @@ class ApiService {
|
|||||||
required String zongpaiNo,
|
required String zongpaiNo,
|
||||||
required int boxNo,
|
required int boxNo,
|
||||||
required int quantity,
|
required int quantity,
|
||||||
required String boxMode,
|
|
||||||
}) async {
|
}) async {
|
||||||
final uri = Uri.parse('$baseUrl/CargoTrace/box');
|
final uri = Uri.parse('$baseUrl/CargoTrace/box');
|
||||||
try {
|
try {
|
||||||
@@ -249,7 +336,6 @@ class ApiService {
|
|||||||
'zongpai_no': zongpaiNo,
|
'zongpai_no': zongpaiNo,
|
||||||
'box_no': boxNo,
|
'box_no': boxNo,
|
||||||
'quantity': quantity,
|
'quantity': quantity,
|
||||||
'box_mode': boxMode,
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.timeout(timeout);
|
.timeout(timeout);
|
||||||
@@ -275,6 +361,71 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 更新装箱明细 — 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: '网络异常,请检查网络连接');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Test connectivity by making a HEAD request to the base URL.
|
/// Test connectivity by making a HEAD request to the base URL.
|
||||||
Future<bool> testConnection(String baseUrl) async {
|
Future<bool> testConnection(String baseUrl) async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -38,11 +38,11 @@ class AppConfigService {
|
|||||||
Future<void> setString(String key, String value) async {
|
Future<void> setString(String key, String value) async {
|
||||||
final config = await loadConfig();
|
final config = await loadConfig();
|
||||||
config[key.toString()] = value.toString();
|
config[key.toString()] = value.toString();
|
||||||
await _saveConfig(config);
|
await saveConfig(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 完整写入 JSON 文件
|
/// 完整写入 JSON 文件
|
||||||
Future<void> _saveConfig(Map<String, dynamic> config) async {
|
Future<void> saveConfig(Map<String, dynamic> config) async {
|
||||||
try {
|
try {
|
||||||
final file = File(await _filePath);
|
final file = File(await _filePath);
|
||||||
await file.writeAsString(jsonEncode(config));
|
await file.writeAsString(jsonEncode(config));
|
||||||
|
|||||||
38
lib/services/boxing_context.dart
Normal file
38
lib/services/boxing_context.dart
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
enum PaichanContextChange { firstScan, samePaichan, switchedPaichan }
|
||||||
|
|
||||||
|
class PaichanContextDecision {
|
||||||
|
final PaichanContextChange change;
|
||||||
|
final int? boxNoToApply;
|
||||||
|
|
||||||
|
const PaichanContextDecision({required this.change, this.boxNoToApply});
|
||||||
|
|
||||||
|
bool get isSwitched => change == PaichanContextChange.switchedPaichan;
|
||||||
|
}
|
||||||
|
|
||||||
|
PaichanContextDecision decideMultiCodePaichanContext({
|
||||||
|
required String? lastPaichanNo,
|
||||||
|
required String? currentPaichanNo,
|
||||||
|
required String currentBoxNoText,
|
||||||
|
required int maxBoxNo,
|
||||||
|
}) {
|
||||||
|
final recommendedBoxNo = maxBoxNo + 1;
|
||||||
|
if (currentPaichanNo == null || currentPaichanNo.isEmpty) {
|
||||||
|
return const PaichanContextDecision(change: PaichanContextChange.firstScan);
|
||||||
|
}
|
||||||
|
if (lastPaichanNo == null || lastPaichanNo.isEmpty) {
|
||||||
|
return PaichanContextDecision(
|
||||||
|
change: PaichanContextChange.firstScan,
|
||||||
|
boxNoToApply: currentBoxNoText.trim().isEmpty ? recommendedBoxNo : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (lastPaichanNo == currentPaichanNo) {
|
||||||
|
return PaichanContextDecision(
|
||||||
|
change: PaichanContextChange.samePaichan,
|
||||||
|
boxNoToApply: currentBoxNoText.trim().isEmpty ? recommendedBoxNo : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return PaichanContextDecision(
|
||||||
|
change: PaichanContextChange.switchedPaichan,
|
||||||
|
boxNoToApply: recommendedBoxNo,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ enum FeedbackEvent {
|
|||||||
networkError, // Network error → failure sound + short vibrate
|
networkError, // Network error → failure sound + short vibrate
|
||||||
duplicateBoxNo, // Duplicate box no → failure sound + short vibrate
|
duplicateBoxNo, // Duplicate box no → failure sound + short vibrate
|
||||||
modeSwitch, // Mode toggle → beep only
|
modeSwitch, // Mode toggle → beep only
|
||||||
|
paichanSwitch, // Paichan changed → beep + short vibrate
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Centralized feedback dispatcher.
|
/// Centralized feedback dispatcher.
|
||||||
@@ -56,6 +57,10 @@ class FeedbackService {
|
|||||||
|
|
||||||
case FeedbackEvent.modeSwitch:
|
case FeedbackEvent.modeSwitch:
|
||||||
_soundService.playBeep();
|
_soundService.playBeep();
|
||||||
|
|
||||||
|
case FeedbackEvent.paichanSwitch:
|
||||||
|
_soundService.playBeep();
|
||||||
|
_vibrateShort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ void main() {
|
|||||||
test('returns ok on 200', () async {
|
test('returns ok on 200', () async {
|
||||||
final mockClient = _MockClient((request) async {
|
final mockClient = _MockClient((request) async {
|
||||||
return http.Response(
|
return http.Response(
|
||||||
jsonEncode({'zongpai_no': '26B1', 'location_code': 'A01-02-03', 'created_at': '2026-05-11T10:00:00'}),
|
jsonEncode({
|
||||||
|
'zongpai_no': '26B1',
|
||||||
|
'location_code': 'A01-02-03',
|
||||||
|
'created_at': '2026-05-11T10:00:00',
|
||||||
|
}),
|
||||||
200,
|
200,
|
||||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
);
|
);
|
||||||
@@ -21,9 +25,36 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(result.success, isTrue);
|
expect(result.success, isTrue);
|
||||||
expect(result.isDuplicate, isFalse);
|
expect(result.isDuplicate, isFalse);
|
||||||
|
expect(result.isOffShelfSuccess, isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns duplicate on 409 with location_code and registered_at', () async {
|
test('returns off-shelf success on 200 with previous_location', () async {
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'zongpai_no': '26B1',
|
||||||
|
'location_code': 'TRANS-01',
|
||||||
|
'previous_location': 'A01-02-03',
|
||||||
|
'created_at': '2026-05-13T10:00:00',
|
||||||
|
}),
|
||||||
|
200,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.registerLocation(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
zongpaiNo: '26B1',
|
||||||
|
locationCode: 'TRANS-01',
|
||||||
|
);
|
||||||
|
expect(result.success, isTrue);
|
||||||
|
expect(result.isOffShelfSuccess, isTrue);
|
||||||
|
expect(result.isDuplicate, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'returns duplicate on 409 with location_code and registered_at',
|
||||||
|
() async {
|
||||||
final mockClient = _MockClient((request) async {
|
final mockClient = _MockClient((request) async {
|
||||||
return http.Response(
|
return http.Response(
|
||||||
jsonEncode({
|
jsonEncode({
|
||||||
@@ -46,12 +77,42 @@ void main() {
|
|||||||
expect(result.isDuplicate, isTrue);
|
expect(result.isDuplicate, isTrue);
|
||||||
expect(result.duplicateInfo?['location_code'], 'A01-02-03');
|
expect(result.duplicateInfo?['location_code'], 'A01-02-03');
|
||||||
expect(result.duplicateInfo?['registered_at'], '2026-05-11T10:00:00');
|
expect(result.duplicateInfo?['registered_at'], '2026-05-11T10:00:00');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('returns already off shelf on 409 ALREADY_OFF_SHELF', () async {
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'error_code': 'ALREADY_OFF_SHELF',
|
||||||
|
'message': '该总排号已下架至转运区域,不可重新上架',
|
||||||
|
'location_code': 'TRANS-01',
|
||||||
|
'registered_at': '2026-05-13T10:00:00',
|
||||||
|
}),
|
||||||
|
409,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.registerLocation(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
zongpaiNo: '26B1',
|
||||||
|
locationCode: 'A02-01-01',
|
||||||
|
);
|
||||||
|
expect(result.success, isFalse);
|
||||||
|
expect(result.isDuplicate, isFalse);
|
||||||
|
expect(result.isAlreadyOffShelf, isTrue);
|
||||||
|
expect(result.errorMessage, '该总排号已下架至转运区域,不可重新上架');
|
||||||
|
expect(result.conflictInfo?['location_code'], 'TRANS-01');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns error on 400 with message', () async {
|
test('returns error on 400 with message', () async {
|
||||||
final mockClient = _MockClient((request) async {
|
final mockClient = _MockClient((request) async {
|
||||||
return http.Response(
|
return http.Response(
|
||||||
jsonEncode({'error_code': 'INVALID_ZONGPAI', 'message': 'INVALID_ZONGPAI'}),
|
jsonEncode({
|
||||||
|
'error_code': 'INVALID_ZONGPAI',
|
||||||
|
'message': 'INVALID_ZONGPAI',
|
||||||
|
}),
|
||||||
400,
|
400,
|
||||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
);
|
);
|
||||||
@@ -81,6 +142,156 @@ void main() {
|
|||||||
expect(result.errorMessage, '网络异常,请检查网络连接');
|
expect(result.errorMessage, '网络异常,请检查网络连接');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('ApiService boxing APIs', () {
|
||||||
|
test('fetchBoxInfo parses current boxes and box item ids', () async {
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'zongpai_no': '26BW0011',
|
||||||
|
'paichan_no': 'W00009',
|
||||||
|
'work_order_no': '6-1(7)',
|
||||||
|
'quantity': 80,
|
||||||
|
'current_zongpai_boxes': [
|
||||||
|
{'box_item_id': 101, 'box_no': 3, 'quantity': 30},
|
||||||
|
],
|
||||||
|
'existing_boxes': [
|
||||||
|
{
|
||||||
|
'box_no': 3,
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'box_item_id': 101,
|
||||||
|
'zongpai_no': '26BW0011',
|
||||||
|
'work_order_no': '6-1(7)',
|
||||||
|
'quantity': 30,
|
||||||
|
'total_quantity': 80,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'max_box_no': 3,
|
||||||
|
'suggested_box_no': 4,
|
||||||
|
}),
|
||||||
|
200,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.fetchBoxInfo(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
zongpaiNo: '26BW0011',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success, isTrue);
|
||||||
|
expect(result.currentZongpaiBoxes.single.boxItemId, 101);
|
||||||
|
expect(result.existingBoxes.single.items.single.boxItemId, 101);
|
||||||
|
expect(result.existingBoxes.single.items.single.totalQuantity, 80);
|
||||||
|
expect(result.suggestedBoxNo, 4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('saveBoxRecord sends simplified request body', () async {
|
||||||
|
late Map<String, dynamic> body;
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
final req = request as http.Request;
|
||||||
|
body = jsonDecode(req.body) as Map<String, dynamic>;
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'box_item_id': 102,
|
||||||
|
'paichan_no': 'W00009',
|
||||||
|
'box_no': 4,
|
||||||
|
'zongpai_no': '26BW0012',
|
||||||
|
'quantity': 34,
|
||||||
|
'created_at': '2026-05-13T10:00:00',
|
||||||
|
}),
|
||||||
|
200,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.saveBoxRecord(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
zongpaiNo: '26BW0012',
|
||||||
|
boxNo: 4,
|
||||||
|
quantity: 34,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success, isTrue);
|
||||||
|
expect(result.boxItemId, 102);
|
||||||
|
expect(result.zongpaiNo, '26BW0012');
|
||||||
|
expect(body, {'zongpai_no': '26BW0012', 'box_no': 4, 'quantity': 34});
|
||||||
|
expect(body.containsKey('box_mode'), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updateBoxRecord sends simplified request body', () async {
|
||||||
|
late Map<String, dynamic> body;
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
final req = request as http.Request;
|
||||||
|
body = jsonDecode(req.body) as Map<String, dynamic>;
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'box_item_id': 102,
|
||||||
|
'paichan_no': 'W00009',
|
||||||
|
'box_no': 4,
|
||||||
|
'zongpai_no': '26BW0012',
|
||||||
|
'quantity': 20,
|
||||||
|
'updated_at': '2026-05-13T10:00:00',
|
||||||
|
}),
|
||||||
|
200,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.updateBoxRecord(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
boxItemId: 102,
|
||||||
|
boxNo: 4,
|
||||||
|
quantity: 20,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success, isTrue);
|
||||||
|
expect(body, {'box_no': 4, 'quantity': 20});
|
||||||
|
expect(body.containsKey('box_mode'), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deleteBoxRecord handles success and not found', () async {
|
||||||
|
var calls = 0;
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
calls += 1;
|
||||||
|
if (calls == 1) {
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({'box_item_id': 102, 'deleted': true}),
|
||||||
|
200,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'error_code': 'BOX_ITEM_NOT_FOUND',
|
||||||
|
'message': '指定装箱明细不存在',
|
||||||
|
}),
|
||||||
|
404,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final ok = await svc.deleteBoxRecord(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
boxItemId: 102,
|
||||||
|
);
|
||||||
|
final missing = await svc.deleteBoxRecord(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
boxItemId: 999,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(ok.success, isTrue);
|
||||||
|
expect(missing.success, isFalse);
|
||||||
|
expect(missing.errorMessage, '指定装箱明细不存在');
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MockClient extends http.BaseClient {
|
class _MockClient extends http.BaseClient {
|
||||||
|
|||||||
60
test/services/boxing_context_test.dart
Normal file
60
test/services/boxing_context_test.dart
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pad_scanner/services/boxing_context.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('decideMultiCodePaichanContext', () {
|
||||||
|
test('first scan records context and fills recommended box if empty', () {
|
||||||
|
final decision = decideMultiCodePaichanContext(
|
||||||
|
lastPaichanNo: null,
|
||||||
|
currentPaichanNo: 'W00009',
|
||||||
|
currentBoxNoText: '',
|
||||||
|
maxBoxNo: 3,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(decision.change, PaichanContextChange.firstScan);
|
||||||
|
expect(decision.boxNoToApply, 4);
|
||||||
|
expect(decision.isSwitched, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('same paichan keeps current box number', () {
|
||||||
|
final decision = decideMultiCodePaichanContext(
|
||||||
|
lastPaichanNo: 'W00009',
|
||||||
|
currentPaichanNo: 'W00009',
|
||||||
|
currentBoxNoText: '8',
|
||||||
|
maxBoxNo: 3,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(decision.change, PaichanContextChange.samePaichan);
|
||||||
|
expect(decision.boxNoToApply, isNull);
|
||||||
|
expect(decision.isSwitched, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('switched paichan resets to new paichan max box plus one', () {
|
||||||
|
final decision = decideMultiCodePaichanContext(
|
||||||
|
lastPaichanNo: 'W00009',
|
||||||
|
currentPaichanNo: 'W00010',
|
||||||
|
currentBoxNoText: '8',
|
||||||
|
maxBoxNo: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(decision.change, PaichanContextChange.switchedPaichan);
|
||||||
|
expect(decision.boxNoToApply, 2);
|
||||||
|
expect(decision.isSwitched, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'same paichan defensively fills recommended box when box is empty',
|
||||||
|
() {
|
||||||
|
final decision = decideMultiCodePaichanContext(
|
||||||
|
lastPaichanNo: 'W00009',
|
||||||
|
currentPaichanNo: 'W00009',
|
||||||
|
currentBoxNoText: '',
|
||||||
|
maxBoxNo: 4,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(decision.change, PaichanContextChange.samePaichan);
|
||||||
|
expect(decision.boxNoToApply, 5);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user