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/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);
|
||||
|
||||
Reference in New Issue
Block a user