feat(registration): merge shelf overview into registration

This commit is contained in:
Misaka_Company
2026-05-15 08:45:22 +08:00
parent ce80e1a1f6
commit b2f2c536cb
3 changed files with 706 additions and 313 deletions

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pad_scanner/services/app_config_service.dart';
@@ -20,22 +22,29 @@ class _RegistrationPageState extends State<RegistrationPage> {
final _feedbackService = FeedbackService();
final _focusNode = FocusNode();
StreamSubscription<ScanResult>? _scanSubscription;
final _zongpaiNos = <String>[];
String? _locationCode;
CodeType? _locationType;
bool _isLocked = false;
bool _isSubmitting = false;
// Status bar state
StatusDotColor _statusDot = StatusDotColor.blue;
String _statusText = '等待扫描总排号或货位号…';
String? _statusOverrideText;
StatusDotColor? _statusOverrideDot;
bool _overviewLoading = false;
bool _overviewNotFound = false;
String? _overviewError;
String? _overviewZongpaiNo;
PaichaOverviewResult? _overview;
int _overviewRequestId = 0;
@override
void initState() {
super.initState();
_scannerService.scanResults.listen(_onScan);
_scanSubscription = _scannerService.scanResults.listen(_onScan);
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus();
});
@@ -43,6 +52,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
@override
void dispose() {
_scanSubscription?.cancel();
_focusNode.dispose();
_feedbackService.dispose();
super.dispose();
@@ -58,24 +68,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
final parsed = CodeParser.parse(result.barcode);
switch (parsed.type) {
case CodeType.zongpaiNo:
_feedbackService.trigger(FeedbackEvent.scanValid);
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();
});
_handleZongpaiScan(parsed.value);
case CodeType.locationNormal:
case CodeType.locationTransit:
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(() {
_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() {
_statusOverrideText = null;
@@ -130,15 +143,15 @@ class _RegistrationPageState extends State<RegistrationPage> {
_statusText = '货位已锁定,请扫描下一张执行卡';
return;
}
final hasZ = _zongpaiNos.isNotEmpty;
final hasL = _locationCode != null;
if (hasZ && hasL) {
final hasZongpai = _zongpaiNos.isNotEmpty;
final hasLocation = _locationCode != null;
if (hasZongpai && hasLocation) {
_statusDot = StatusDotColor.blue;
_statusText = '请确认信息并提交';
} else if (hasZ && !hasL) {
} else if (hasZongpai) {
_statusDot = StatusDotColor.blue;
_statusText = '请扫描目标货位号';
} else if (!hasZ && hasL) {
} else if (hasLocation) {
_statusDot = StatusDotColor.blue;
_statusText = '请扫描执行卡';
} else {
@@ -150,12 +163,70 @@ class _RegistrationPageState extends State<RegistrationPage> {
bool get _canSubmit =>
_zongpaiNos.isNotEmpty && _locationCode != null && !_isSubmitting;
Future<void> _submit() async {
if (!_canSubmit) return;
Future<String?> _baseUrl() async {
final configService = AppConfigService();
final baseUrl = await configService.getString('api_url') ?? '';
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);
_showStatusOverride(
'未配置 API 地址,请前往设置',
@@ -202,6 +273,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
StatusDotColor.green,
const Duration(milliseconds: 1500),
);
await _refreshCurrentOverview();
} else if (result.isDuplicate) {
_feedbackService.trigger(FeedbackEvent.duplicateError);
_showDuplicateDialog(zongpaiNo, result.duplicateInfo);
@@ -231,10 +303,10 @@ class _RegistrationPageState extends State<RegistrationPage> {
final failed = <String>[];
final toRemove = <String>[];
for (final zp in List.of(_zongpaiNos)) {
for (final zongpaiNo in List.of(_zongpaiNos)) {
final result = await _apiService.registerLocation(
baseUrl: baseUrl,
zongpaiNo: zp,
zongpaiNo: zongpaiNo,
locationCode: _locationCode!,
);
if (result.success) {
@@ -242,21 +314,21 @@ class _RegistrationPageState extends State<RegistrationPage> {
if (result.isOffShelfSuccess) {
offShelfCount++;
}
toRemove.add(zp);
toRemove.add(zongpaiNo);
} else {
failed.add(zp);
failed.add(zongpaiNo);
if (result.isDuplicate) {
setState(() {
_zongpaiNos.removeWhere((e) => toRemove.contains(e));
_zongpaiNos.removeWhere((item) => toRemove.contains(item));
_isSubmitting = false;
});
_feedbackService.trigger(FeedbackEvent.duplicateError);
_showDuplicateDialog(zp, result.duplicateInfo);
_showDuplicateDialog(zongpaiNo, result.duplicateInfo);
return;
}
if (result.isAlreadyOffShelf) {
setState(() {
_zongpaiNos.removeWhere((e) => toRemove.contains(e));
_zongpaiNos.removeWhere((item) => toRemove.contains(item));
_isSubmitting = false;
});
_feedbackService.trigger(FeedbackEvent.submitFailure);
@@ -273,7 +345,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
if (!mounted) return;
setState(() {
_zongpaiNos.removeWhere((e) => toRemove.contains(e));
_zongpaiNos.removeWhere((item) => toRemove.contains(item));
_isSubmitting = false;
});
@@ -287,6 +359,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
StatusDotColor.green,
const Duration(milliseconds: 1500),
);
await _refreshCurrentOverview();
} else {
_feedbackService.trigger(FeedbackEvent.submitFailure);
_showStatusOverride(
@@ -351,10 +424,8 @@ class _RegistrationPageState extends State<RegistrationPage> {
}
setState(() {
_isLocked = value;
if (!value) {
if (_zongpaiNos.length > 1) {
_zongpaiNos.removeRange(0, _zongpaiNos.length - 1);
}
if (!value && _zongpaiNos.length > 1) {
_zongpaiNos.removeRange(0, _zongpaiNos.length - 1);
}
});
}
@@ -369,11 +440,20 @@ class _RegistrationPageState extends State<RegistrationPage> {
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
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
// Compute effective status
_updateBaseStatus();
final effectiveDot = _statusOverrideDot ?? _statusDot;
final effectiveText = _statusOverrideText ?? _statusText;
@@ -398,285 +478,420 @@ class _RegistrationPageState extends State<RegistrationPage> {
),
body: Column(
children: [
// Main form area (no top banner)
Expanded(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// ---- 目标货位 (上方) ----
Row(
children: [
const Text(
'目标货位',
style: TextStyle(
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),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 12,
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,
),
Padding(
padding: const EdgeInsets.fromLTRB(12, 10, 12, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'登记操作区',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700),
),
const SizedBox(height: 6),
_isLocked
? _buildLockedInputPanel()
: _buildSingleInputPanel(),
const SizedBox(height: 10),
SizedBox(
width: double.infinity,
height: 46,
child: ElevatedButton(
onPressed: _canSubmit ? _submit : null,
style: ElevatedButton.styleFrom(
backgroundColor: _canSubmit
? colorScheme.primary
: Colors.grey.shade300,
foregroundColor: _canSubmit
? Colors.white
: Colors.grey.shade600,
shape: RoundedRectangleBorder(
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(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: _canSubmit ? _submit : null,
style: ElevatedButton.styleFrom(
backgroundColor: _canSubmit
? colorScheme.primary
: Colors.grey.shade300,
foregroundColor: _canSubmit
? Colors.white
: Colors.grey.shade600,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: _isSubmitting
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text(
_isLocked && _zongpaiNos.length > 1
? '批量上架(${_zongpaiNos.length} 条)'
: '确认上架(P1)',
style: const TextStyle(fontSize: 18),
child: _isSubmitting
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
),
)
: Text(
_isLocked && _zongpaiNos.length > 1
? '批量上架(${_zongpaiNos.length} 条)'
: '确 认 上 架',
style: const TextStyle(fontSize: 17),
),
),
],
),
),
],
),
),
// Bottom status bar
Divider(height: 1, color: Colors.grey.shade300),
Expanded(child: _buildOverviewSection()),
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);

View File

@@ -46,6 +46,76 @@ class RegistrationResult {
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
Future<BoxInfoResult> fetchBoxInfo({
required String baseUrl,