feat(registration): merge shelf overview into registration
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:pad_scanner/services/app_config_service.dart';
|
import 'package:pad_scanner/services/app_config_service.dart';
|
||||||
@@ -20,22 +22,29 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
final _feedbackService = FeedbackService();
|
final _feedbackService = FeedbackService();
|
||||||
final _focusNode = FocusNode();
|
final _focusNode = FocusNode();
|
||||||
|
|
||||||
|
StreamSubscription<ScanResult>? _scanSubscription;
|
||||||
final _zongpaiNos = <String>[];
|
final _zongpaiNos = <String>[];
|
||||||
String? _locationCode;
|
String? _locationCode;
|
||||||
CodeType? _locationType;
|
CodeType? _locationType;
|
||||||
bool _isLocked = false;
|
bool _isLocked = false;
|
||||||
bool _isSubmitting = false;
|
bool _isSubmitting = false;
|
||||||
|
|
||||||
// Status bar state
|
|
||||||
StatusDotColor _statusDot = StatusDotColor.blue;
|
StatusDotColor _statusDot = StatusDotColor.blue;
|
||||||
String _statusText = '等待扫描总排号或货位号…';
|
String _statusText = '等待扫描总排号或货位号…';
|
||||||
String? _statusOverrideText;
|
String? _statusOverrideText;
|
||||||
StatusDotColor? _statusOverrideDot;
|
StatusDotColor? _statusOverrideDot;
|
||||||
|
|
||||||
|
bool _overviewLoading = false;
|
||||||
|
bool _overviewNotFound = false;
|
||||||
|
String? _overviewError;
|
||||||
|
String? _overviewZongpaiNo;
|
||||||
|
PaichaOverviewResult? _overview;
|
||||||
|
int _overviewRequestId = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_scannerService.scanResults.listen(_onScan);
|
_scanSubscription = _scannerService.scanResults.listen(_onScan);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_focusNode.requestFocus();
|
_focusNode.requestFocus();
|
||||||
});
|
});
|
||||||
@@ -43,6 +52,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_scanSubscription?.cancel();
|
||||||
_focusNode.dispose();
|
_focusNode.dispose();
|
||||||
_feedbackService.dispose();
|
_feedbackService.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@@ -58,24 +68,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
final parsed = CodeParser.parse(result.barcode);
|
final parsed = CodeParser.parse(result.barcode);
|
||||||
switch (parsed.type) {
|
switch (parsed.type) {
|
||||||
case CodeType.zongpaiNo:
|
case CodeType.zongpaiNo:
|
||||||
_feedbackService.trigger(FeedbackEvent.scanValid);
|
_handleZongpaiScan(parsed.value);
|
||||||
setState(() {
|
|
||||||
if (_isLocked) {
|
|
||||||
final idx = _zongpaiNos.indexOf(parsed.value);
|
|
||||||
if (idx >= 0) {
|
|
||||||
_zongpaiNos[idx] = parsed.value;
|
|
||||||
} else {
|
|
||||||
_zongpaiNos.add(parsed.value);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (_zongpaiNos.isNotEmpty) {
|
|
||||||
_zongpaiNos[0] = parsed.value;
|
|
||||||
} else {
|
|
||||||
_zongpaiNos.add(parsed.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_clearStatusOverride();
|
|
||||||
});
|
|
||||||
case CodeType.locationNormal:
|
case CodeType.locationNormal:
|
||||||
case CodeType.locationTransit:
|
case CodeType.locationTransit:
|
||||||
if (!_isLocked) {
|
if (!_isLocked) {
|
||||||
@@ -96,13 +89,33 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _removeZongpai(int index) {
|
void _handleZongpaiScan(String zongpaiNo) {
|
||||||
|
final shouldRefresh = _overviewZongpaiNo != zongpaiNo;
|
||||||
|
_feedbackService.trigger(FeedbackEvent.scanValid);
|
||||||
setState(() {
|
setState(() {
|
||||||
_zongpaiNos.removeAt(index);
|
if (_isLocked) {
|
||||||
|
if (!_zongpaiNos.contains(zongpaiNo)) {
|
||||||
|
_zongpaiNos.add(zongpaiNo);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_zongpaiNos.isEmpty) {
|
||||||
|
_zongpaiNos.add(zongpaiNo);
|
||||||
|
} else {
|
||||||
|
_zongpaiNos[0] = zongpaiNo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_clearStatusOverride();
|
||||||
});
|
});
|
||||||
|
if (shouldRefresh) {
|
||||||
|
_loadOverview(zongpaiNo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Status bar management ---
|
void _removeZongpai(String zongpaiNo) {
|
||||||
|
setState(() {
|
||||||
|
_zongpaiNos.remove(zongpaiNo);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _clearStatusOverride() {
|
void _clearStatusOverride() {
|
||||||
_statusOverrideText = null;
|
_statusOverrideText = null;
|
||||||
@@ -130,15 +143,15 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
_statusText = '货位已锁定,请扫描下一张执行卡';
|
_statusText = '货位已锁定,请扫描下一张执行卡';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final hasZ = _zongpaiNos.isNotEmpty;
|
final hasZongpai = _zongpaiNos.isNotEmpty;
|
||||||
final hasL = _locationCode != null;
|
final hasLocation = _locationCode != null;
|
||||||
if (hasZ && hasL) {
|
if (hasZongpai && hasLocation) {
|
||||||
_statusDot = StatusDotColor.blue;
|
_statusDot = StatusDotColor.blue;
|
||||||
_statusText = '请确认信息并提交';
|
_statusText = '请确认信息并提交';
|
||||||
} else if (hasZ && !hasL) {
|
} else if (hasZongpai) {
|
||||||
_statusDot = StatusDotColor.blue;
|
_statusDot = StatusDotColor.blue;
|
||||||
_statusText = '请扫描目标货位号';
|
_statusText = '请扫描目标货位号';
|
||||||
} else if (!hasZ && hasL) {
|
} else if (hasLocation) {
|
||||||
_statusDot = StatusDotColor.blue;
|
_statusDot = StatusDotColor.blue;
|
||||||
_statusText = '请扫描执行卡';
|
_statusText = '请扫描执行卡';
|
||||||
} else {
|
} else {
|
||||||
@@ -150,12 +163,70 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
bool get _canSubmit =>
|
bool get _canSubmit =>
|
||||||
_zongpaiNos.isNotEmpty && _locationCode != null && !_isSubmitting;
|
_zongpaiNos.isNotEmpty && _locationCode != null && !_isSubmitting;
|
||||||
|
|
||||||
Future<void> _submit() async {
|
Future<String?> _baseUrl() async {
|
||||||
if (!_canSubmit) return;
|
|
||||||
|
|
||||||
final configService = AppConfigService();
|
final configService = AppConfigService();
|
||||||
final baseUrl = await configService.getString('api_url') ?? '';
|
final baseUrl = await configService.getString('api_url') ?? '';
|
||||||
if (baseUrl.isEmpty) {
|
if (baseUrl.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadOverview(String zongpaiNo, {bool force = false}) async {
|
||||||
|
if (!force && _overviewZongpaiNo == zongpaiNo && _overview != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final requestId = ++_overviewRequestId;
|
||||||
|
setState(() {
|
||||||
|
_overviewZongpaiNo = zongpaiNo;
|
||||||
|
_overviewLoading = true;
|
||||||
|
_overviewNotFound = false;
|
||||||
|
_overviewError = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
final baseUrl = await _baseUrl();
|
||||||
|
if (!mounted || requestId != _overviewRequestId) return;
|
||||||
|
if (baseUrl == null) {
|
||||||
|
setState(() {
|
||||||
|
_overviewLoading = false;
|
||||||
|
_overviewError = '未配置 API 地址,请前往设置';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await _apiService.fetchPaichaOverview(
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
zongpaiNo: zongpaiNo,
|
||||||
|
);
|
||||||
|
if (!mounted || requestId != _overviewRequestId) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_overviewLoading = false;
|
||||||
|
if (result.success) {
|
||||||
|
_overview = result;
|
||||||
|
_overviewNotFound = false;
|
||||||
|
_overviewError = null;
|
||||||
|
} else if (result.notFound) {
|
||||||
|
_overview = null;
|
||||||
|
_overviewNotFound = true;
|
||||||
|
_overviewError = null;
|
||||||
|
} else {
|
||||||
|
_overviewError = result.errorMessage ?? '加载失败,点击重试';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _refreshCurrentOverview() async {
|
||||||
|
final zongpaiNo = _overviewZongpaiNo;
|
||||||
|
if (zongpaiNo == null) return;
|
||||||
|
await _loadOverview(zongpaiNo, force: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _submit() async {
|
||||||
|
if (!_canSubmit) return;
|
||||||
|
|
||||||
|
final baseUrl = await _baseUrl();
|
||||||
|
if (baseUrl == null) {
|
||||||
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||||||
_showStatusOverride(
|
_showStatusOverride(
|
||||||
'未配置 API 地址,请前往设置',
|
'未配置 API 地址,请前往设置',
|
||||||
@@ -202,6 +273,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
StatusDotColor.green,
|
StatusDotColor.green,
|
||||||
const Duration(milliseconds: 1500),
|
const Duration(milliseconds: 1500),
|
||||||
);
|
);
|
||||||
|
await _refreshCurrentOverview();
|
||||||
} else if (result.isDuplicate) {
|
} else if (result.isDuplicate) {
|
||||||
_feedbackService.trigger(FeedbackEvent.duplicateError);
|
_feedbackService.trigger(FeedbackEvent.duplicateError);
|
||||||
_showDuplicateDialog(zongpaiNo, result.duplicateInfo);
|
_showDuplicateDialog(zongpaiNo, result.duplicateInfo);
|
||||||
@@ -231,10 +303,10 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
final failed = <String>[];
|
final failed = <String>[];
|
||||||
final toRemove = <String>[];
|
final toRemove = <String>[];
|
||||||
|
|
||||||
for (final zp in List.of(_zongpaiNos)) {
|
for (final zongpaiNo in List.of(_zongpaiNos)) {
|
||||||
final result = await _apiService.registerLocation(
|
final result = await _apiService.registerLocation(
|
||||||
baseUrl: baseUrl,
|
baseUrl: baseUrl,
|
||||||
zongpaiNo: zp,
|
zongpaiNo: zongpaiNo,
|
||||||
locationCode: _locationCode!,
|
locationCode: _locationCode!,
|
||||||
);
|
);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -242,21 +314,21 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
if (result.isOffShelfSuccess) {
|
if (result.isOffShelfSuccess) {
|
||||||
offShelfCount++;
|
offShelfCount++;
|
||||||
}
|
}
|
||||||
toRemove.add(zp);
|
toRemove.add(zongpaiNo);
|
||||||
} else {
|
} else {
|
||||||
failed.add(zp);
|
failed.add(zongpaiNo);
|
||||||
if (result.isDuplicate) {
|
if (result.isDuplicate) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_zongpaiNos.removeWhere((e) => toRemove.contains(e));
|
_zongpaiNos.removeWhere((item) => toRemove.contains(item));
|
||||||
_isSubmitting = false;
|
_isSubmitting = false;
|
||||||
});
|
});
|
||||||
_feedbackService.trigger(FeedbackEvent.duplicateError);
|
_feedbackService.trigger(FeedbackEvent.duplicateError);
|
||||||
_showDuplicateDialog(zp, result.duplicateInfo);
|
_showDuplicateDialog(zongpaiNo, result.duplicateInfo);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result.isAlreadyOffShelf) {
|
if (result.isAlreadyOffShelf) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_zongpaiNos.removeWhere((e) => toRemove.contains(e));
|
_zongpaiNos.removeWhere((item) => toRemove.contains(item));
|
||||||
_isSubmitting = false;
|
_isSubmitting = false;
|
||||||
});
|
});
|
||||||
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||||||
@@ -273,7 +345,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_zongpaiNos.removeWhere((e) => toRemove.contains(e));
|
_zongpaiNos.removeWhere((item) => toRemove.contains(item));
|
||||||
_isSubmitting = false;
|
_isSubmitting = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -287,6 +359,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
StatusDotColor.green,
|
StatusDotColor.green,
|
||||||
const Duration(milliseconds: 1500),
|
const Duration(milliseconds: 1500),
|
||||||
);
|
);
|
||||||
|
await _refreshCurrentOverview();
|
||||||
} else {
|
} else {
|
||||||
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||||||
_showStatusOverride(
|
_showStatusOverride(
|
||||||
@@ -351,11 +424,9 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLocked = value;
|
_isLocked = value;
|
||||||
if (!value) {
|
if (!value && _zongpaiNos.length > 1) {
|
||||||
if (_zongpaiNos.length > 1) {
|
|
||||||
_zongpaiNos.removeRange(0, _zongpaiNos.length - 1);
|
_zongpaiNos.removeRange(0, _zongpaiNos.length - 1);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,11 +440,20 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
return Colors.blue;
|
return Colors.blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Color _overviewRowColor(String status) {
|
||||||
|
switch (status) {
|
||||||
|
case 'on_shelf':
|
||||||
|
return const Color(0xFFE3F2FD);
|
||||||
|
case 'transferred':
|
||||||
|
return const Color(0xFFFFF3E0);
|
||||||
|
default:
|
||||||
|
return const Color(0xFFF5F5F5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
// Compute effective status
|
|
||||||
_updateBaseStatus();
|
_updateBaseStatus();
|
||||||
final effectiveDot = _statusOverrideDot ?? _statusDot;
|
final effectiveDot = _statusOverrideDot ?? _statusDot;
|
||||||
final effectiveText = _statusOverrideText ?? _statusText;
|
final effectiveText = _statusOverrideText ?? _statusText;
|
||||||
@@ -398,245 +478,23 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
// Main form area (no top banner)
|
Padding(
|
||||||
Expanded(
|
padding: const EdgeInsets.fromLTRB(12, 10, 12, 8),
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
// ---- 目标货位 (上方) ----
|
|
||||||
Row(
|
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'目标货位',
|
'登记操作区',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700),
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_locationCode != null) ...[
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 6,
|
|
||||||
vertical: 2,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: _locationLabelColor(
|
|
||||||
_locationType,
|
|
||||||
).withValues(alpha: 0.15),
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
_locationLabel(_locationType),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: _locationLabelColor(_locationType),
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Container(
|
_isLocked
|
||||||
width: double.infinity,
|
? _buildLockedInputPanel()
|
||||||
padding: const EdgeInsets.symmetric(
|
: _buildSingleInputPanel(),
|
||||||
horizontal: 12,
|
const SizedBox(height: 10),
|
||||||
vertical: 14,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: _locationCode != null
|
|
||||||
? Colors.green
|
|
||||||
: Colors.grey.shade400,
|
|
||||||
width: _locationCode != null ? 2 : 1,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
_locationCode ?? '',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: _locationCode != null
|
|
||||||
? Colors.black87
|
|
||||||
: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_isLocked)
|
|
||||||
const Icon(
|
|
||||||
Icons.lock,
|
|
||||||
color: Colors.orange,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// ---- 总排号 (下方) ----
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'总排号',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_isLocked && _zongpaiNos.isNotEmpty) ...[
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 6,
|
|
||||||
vertical: 2,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blue.withValues(alpha: 0.15),
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'${_zongpaiNos.length} 条',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: Colors.blue,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
|
|
||||||
if (_isLocked) ...[
|
|
||||||
// 锁定模式:列表显示多条总排号
|
|
||||||
Expanded(
|
|
||||||
child: _zongpaiNos.isEmpty
|
|
||||||
? Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(14),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: Colors.grey.shade400,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
'',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: ListView.separated(
|
|
||||||
itemCount: _zongpaiNos.length,
|
|
||||||
separatorBuilder: (_, __) =>
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return Dismissible(
|
|
||||||
key: ValueKey(
|
|
||||||
'${_zongpaiNos[index]}-$index',
|
|
||||||
),
|
|
||||||
direction: DismissDirection.endToStart,
|
|
||||||
onDismissed: (_) => _removeZongpai(index),
|
|
||||||
background: Container(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
padding: const EdgeInsets.only(right: 16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.red.shade100,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.delete,
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: Colors.green,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
_zongpaiNos[index],
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.close,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
onPressed: () =>
|
|
||||||
_removeZongpai(index),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
// 单次模式:单个显示
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 14,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: _zongpaiNos.isNotEmpty
|
|
||||||
? Colors.green
|
|
||||||
: Colors.grey.shade400,
|
|
||||||
width: _zongpaiNos.isNotEmpty ? 2 : 1,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
_zongpaiNos.isNotEmpty ? _zongpaiNos.first : '',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: _zongpaiNos.isNotEmpty
|
|
||||||
? Colors.black87
|
|
||||||
: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Submit button
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 48,
|
height: 46,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _canSubmit ? _submit : null,
|
onPressed: _canSubmit ? _submit : null,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
@@ -662,21 +520,378 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
|||||||
: Text(
|
: Text(
|
||||||
_isLocked && _zongpaiNos.length > 1
|
_isLocked && _zongpaiNos.length > 1
|
||||||
? '批量上架(${_zongpaiNos.length} 条)'
|
? '批量上架(${_zongpaiNos.length} 条)'
|
||||||
: '确认上架(P1)',
|
: '确 认 上 架',
|
||||||
style: const TextStyle(fontSize: 18),
|
style: const TextStyle(fontSize: 17),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Divider(height: 1, color: Colors.grey.shade300),
|
||||||
|
Expanded(child: _buildOverviewSection()),
|
||||||
// Bottom status bar
|
|
||||||
StatusBar(dotColor: effectiveDot, text: effectiveText),
|
StatusBar(dotColor: effectiveDot, text: effectiveText),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildSingleInputPanel() {
|
||||||
|
final hasLocation = _locationCode != null;
|
||||||
|
final hasZongpai = _zongpaiNos.isNotEmpty;
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
constraints: const BoxConstraints(minHeight: 58),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: hasLocation || hasZongpai
|
||||||
|
? Colors.green
|
||||||
|
: Colors.grey.shade400,
|
||||||
|
width: hasLocation || hasZongpai ? 2 : 1,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
flex: 5,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
_locationCode ?? '',
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: hasLocation ? Colors.black87 : Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (hasLocation) ...[
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
_buildLocationChip(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Text(
|
||||||
|
'|',
|
||||||
|
style: TextStyle(fontSize: 20, color: Colors.grey.shade500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
flex: 3,
|
||||||
|
child: Text(
|
||||||
|
hasZongpai ? _zongpaiNos.first : '',
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: hasZongpai ? Colors.black87 : Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLockedInputPanel() {
|
||||||
|
final hasLocation = _locationCode != null;
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
constraints: const BoxConstraints(minHeight: 82),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: hasLocation || _zongpaiNos.isNotEmpty
|
||||||
|
? Colors.green
|
||||||
|
: Colors.grey.shade400,
|
||||||
|
width: hasLocation || _zongpaiNos.isNotEmpty ? 2 : 1,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_locationCode ?? '',
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: hasLocation ? Colors.black87 : Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Icon(Icons.lock, color: Colors.orange, size: 18),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
if (hasLocation) _buildLocationChip(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (_zongpaiNos.isEmpty)
|
||||||
|
const SizedBox(height: 24)
|
||||||
|
else
|
||||||
|
Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 6,
|
||||||
|
children: _zongpaiNos
|
||||||
|
.map(
|
||||||
|
(zongpaiNo) => InputChip(
|
||||||
|
label: Text(zongpaiNo),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onDeleted: () => _removeZongpai(zongpaiNo),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLocationChip() {
|
||||||
|
final color = _locationLabelColor(_locationType);
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 0.15),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
_locationLabel(_locationType),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: color,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildOverviewSection() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Expanded(
|
||||||
|
child: Text(
|
||||||
|
'排产号货架总览',
|
||||||
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_overviewLoading)
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Expanded(child: _buildOverviewBody()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildOverviewBody() {
|
||||||
|
if (_overviewZongpaiNo == null) {
|
||||||
|
return _buildOverviewMessage('扫描总排号后自动显示排产号上架情况');
|
||||||
|
}
|
||||||
|
if (_overviewLoading && _overview == null) {
|
||||||
|
return _buildOverviewMessage('正在加载排产号上架情况…');
|
||||||
|
}
|
||||||
|
if (_overviewNotFound) {
|
||||||
|
return _buildOverviewMessage('暂无排产信息');
|
||||||
|
}
|
||||||
|
if (_overviewError != null && _overview == null) {
|
||||||
|
return _buildRetryMessage(_overviewError!);
|
||||||
|
}
|
||||||
|
final overview = _overview;
|
||||||
|
if (overview == null) {
|
||||||
|
return _buildOverviewMessage('扫描总排号后自动显示排产号上架情况');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (_overviewError != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 6),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_overviewError!,
|
||||||
|
style: const TextStyle(fontSize: 12, color: Colors.red),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _overviewZongpaiNo == null
|
||||||
|
? null
|
||||||
|
: () => _loadOverview(_overviewZongpaiNo!, force: true),
|
||||||
|
child: const Text('重试'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${overview.paichaNo ?? "--"} 共 ${overview.totalCount} 个总排号',
|
||||||
|
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_buildOverviewHeader(),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: overview.items.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return _buildOverviewRow(overview.items[index]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildOverviewHeader() {
|
||||||
|
return Container(
|
||||||
|
height: 32,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade200,
|
||||||
|
border: Border.all(color: Colors.grey.shade400),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
Expanded(flex: 20, child: Text('总排号', style: _headerStyle)),
|
||||||
|
Expanded(flex: 20, child: Text('工令号', style: _headerStyle)),
|
||||||
|
Expanded(
|
||||||
|
flex: 14,
|
||||||
|
child: Text('数量', textAlign: TextAlign.center, style: _headerStyle),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 26,
|
||||||
|
child: Text('货位号', textAlign: TextAlign.right, style: _headerStyle),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildOverviewRow(PaichaOverviewItem item) {
|
||||||
|
final current = item.zongpaiNo == _overviewZongpaiNo;
|
||||||
|
final rowStyle = TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: current ? FontWeight.w700 : FontWeight.w500,
|
||||||
|
color: Colors.black87,
|
||||||
|
);
|
||||||
|
return Container(
|
||||||
|
constraints: const BoxConstraints(minHeight: 34),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _overviewRowColor(item.status),
|
||||||
|
border: Border(
|
||||||
|
left: BorderSide(
|
||||||
|
color: current ? Colors.green : Colors.grey.shade300,
|
||||||
|
width: current ? 2 : 1,
|
||||||
|
),
|
||||||
|
right: BorderSide(
|
||||||
|
color: current ? Colors.green : Colors.grey.shade300,
|
||||||
|
width: current ? 2 : 1,
|
||||||
|
),
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: current ? Colors.green : Colors.grey.shade300,
|
||||||
|
width: current ? 2 : 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 20,
|
||||||
|
child: Text(
|
||||||
|
item.zongpaiNo,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: rowStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 20,
|
||||||
|
child: Text(
|
||||||
|
item.workOrderNo ?? '--',
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: rowStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 14,
|
||||||
|
child: Text(
|
||||||
|
item.quantity.toString(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: rowStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 26,
|
||||||
|
child: Text(
|
||||||
|
item.locationCode ?? '—',
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: rowStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildOverviewMessage(String text) {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 13, color: Colors.grey.shade600),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRetryMessage(String text) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 13, color: Colors.red),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _overviewZongpaiNo == null
|
||||||
|
? null
|
||||||
|
: () => _loadOverview(_overviewZongpaiNo!, force: true),
|
||||||
|
child: const Text('重试'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _headerStyle = TextStyle(fontSize: 12, fontWeight: FontWeight.w700);
|
||||||
|
|||||||
@@ -46,6 +46,76 @@ class RegistrationResult {
|
|||||||
RegistrationResult(success: false, errorMessage: 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);
|
||||||
|
}
|
||||||
|
|
||||||
// === 装箱模块数据类 ===
|
// === 装箱模块数据类 ===
|
||||||
|
|
||||||
/// 箱号内单个总排号明细
|
/// 箱号内单个总排号明细
|
||||||
@@ -290,6 +360,34 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
/// 查询装箱信息 — GET /CargoTrace/box/info
|
||||||
Future<BoxInfoResult> fetchBoxInfo({
|
Future<BoxInfoResult> fetchBoxInfo({
|
||||||
required String baseUrl,
|
required String baseUrl,
|
||||||
|
|||||||
@@ -144,6 +144,86 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('ApiService boxing APIs', () {
|
group('ApiService boxing APIs', () {
|
||||||
|
test('fetchPaichaOverview parses overview items', () async {
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'paicha_no': 'R00001',
|
||||||
|
'total_count': 2,
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'zongpai_no': '26B1',
|
||||||
|
'work_order_no': '6-1(7)',
|
||||||
|
'quantity': 80,
|
||||||
|
'location_code': 'A01-02-03',
|
||||||
|
'status': 'on_shelf',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'zongpai_no': '26B2',
|
||||||
|
'work_order_no': '6-2(3)',
|
||||||
|
'quantity': 45,
|
||||||
|
'location_code': null,
|
||||||
|
'status': 'not_shelved',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
200,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.fetchPaichaOverview(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
zongpaiNo: '26B1',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success, isTrue);
|
||||||
|
expect(result.paichaNo, 'R00001');
|
||||||
|
expect(result.totalCount, 2);
|
||||||
|
expect(result.items.first.status, 'on_shelf');
|
||||||
|
expect(result.items.last.locationCode, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fetchPaichaOverview maps 404 to not found result', () async {
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({'error_code': 'PAICHA_NOT_FOUND', 'message': '暂无排产信息'}),
|
||||||
|
404,
|
||||||
|
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.fetchPaichaOverview(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
zongpaiNo: '26B404',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success, isFalse);
|
||||||
|
expect(result.notFound, isTrue);
|
||||||
|
expect(result.errorMessage, '暂无排产信息');
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'fetchPaichaOverview maps network failure to retryable error',
|
||||||
|
() async {
|
||||||
|
final mockClient = _MockClient((request) async {
|
||||||
|
throw Exception('Connection refused');
|
||||||
|
});
|
||||||
|
|
||||||
|
final svc = ApiService(client: mockClient);
|
||||||
|
final result = await svc.fetchPaichaOverview(
|
||||||
|
baseUrl: 'http://localhost',
|
||||||
|
zongpaiNo: '26B1',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success, isFalse);
|
||||||
|
expect(result.notFound, isFalse);
|
||||||
|
expect(result.errorMessage, '加载失败,点击重试');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('fetchBoxInfo parses current boxes and box item ids', () async {
|
test('fetchBoxInfo parses current boxes and box item ids', () async {
|
||||||
final mockClient = _MockClient((request) async {
|
final mockClient = _MockClient((request) async {
|
||||||
return http.Response(
|
return http.Response(
|
||||||
|
|||||||
Reference in New Issue
Block a user