redesign registration page: compact header layout, cross-paicha dialog, and locked-mode enhancements
- Restructure UI with two-column paicha header (left: paicha number, right: location + stats) - Add delete button on scanned rows in locked mode (matching boxing module style) - Add cross-paicha scan warning with double vibration and vertical button dialog - Show red background for already-on-shelf items on normal shelf (both locked/unlocked) - Pre-check batch submit: block entire batch if any item is already on shelf - Support DPAD up/down navigation in cross-paicha dialog, default to "否" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import 'package:pad_scanner/services/code_parser.dart';
|
||||
import 'package:pad_scanner/services/api_service.dart';
|
||||
import 'package:pad_scanner/services/feedback_service.dart';
|
||||
import 'package:pad_scanner/services/registration_linking.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
import 'package:pad_scanner/pages/boxing_page.dart';
|
||||
import 'package:pad_scanner/widgets/status_bar.dart';
|
||||
|
||||
@@ -45,6 +46,9 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
PaichaOverviewResult? _overview;
|
||||
int _overviewRequestId = 0;
|
||||
|
||||
/// Whether a cross-paicha warning dialog is currently showing.
|
||||
bool _crossPaichaPending = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -136,6 +140,23 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
}
|
||||
|
||||
void _handleZongpaiScan(String zongpaiNo) {
|
||||
// If cross-paicha dialog is showing, just vibrate and discard
|
||||
if (_crossPaichaPending) {
|
||||
_triggerDoubleVibration();
|
||||
return;
|
||||
}
|
||||
|
||||
// In locked mode with existing data, check for cross-paicha scan
|
||||
if (_isLocked &&
|
||||
_zongpaiNos.isNotEmpty &&
|
||||
_overview?.success == true &&
|
||||
!_overview!.items.any((item) => item.zongpaiNo == zongpaiNo)) {
|
||||
_crossPaichaPending = true;
|
||||
_triggerDoubleVibration();
|
||||
_showCrossPaichaDialog(zongpaiNo);
|
||||
return;
|
||||
}
|
||||
|
||||
final shouldRefresh = _overviewZongpaiNo != zongpaiNo;
|
||||
_feedbackService.trigger(FeedbackEvent.scanValid);
|
||||
setState(() {
|
||||
@@ -157,6 +178,10 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
}
|
||||
}
|
||||
|
||||
void _triggerDoubleVibration() {
|
||||
Vibration.vibrate(pattern: [0, 200, 100, 200]);
|
||||
}
|
||||
|
||||
void _removeZongpai(String zongpaiNo) {
|
||||
setState(() {
|
||||
_zongpaiNos.remove(zongpaiNo);
|
||||
@@ -211,6 +236,18 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
|
||||
bool get _isTransitTarget => isTransitTarget(_locationType, _locationCode);
|
||||
|
||||
/// Find the first scanned item that is already on shelf (for batch submit blocking).
|
||||
PaichaOverviewItem? _findOnShelfInScanned() {
|
||||
if (_overview?.success != true) return null;
|
||||
final scannedSet = _zongpaiNos.toSet();
|
||||
for (final item in _overview!.items) {
|
||||
if (scannedSet.contains(item.zongpaiNo) && item.status == 'on_shelf') {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String?> _baseUrl() async {
|
||||
final configService = AppConfigService();
|
||||
final baseUrl = await configService.getString('api_url') ?? '';
|
||||
@@ -298,6 +335,19 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Pre-check: block entire batch if any item is already on shelf (normal shelf)
|
||||
if (!_isTransitTarget) {
|
||||
final onShelfItem = _findOnShelfInScanned();
|
||||
if (onShelfItem != null) {
|
||||
setState(() => _isSubmitting = false);
|
||||
_feedbackService.trigger(FeedbackEvent.duplicateError);
|
||||
_showDuplicateDialog(
|
||||
onShelfItem.zongpaiNo,
|
||||
{'location_code': onShelfItem.locationCode ?? '未知'},
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await _submitBatch(baseUrl);
|
||||
} else {
|
||||
await _submitOne(baseUrl, _zongpaiNos.first);
|
||||
@@ -595,6 +645,144 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showCrossPaichaDialog(String newZongpaiNo) {
|
||||
final currentPaicha = _overview?.paichaNo ?? '--';
|
||||
final dialogFocus = FocusNode();
|
||||
var dismissed = false;
|
||||
var selectedIndex = 0; // 0 = "否" (default), 1 = "是"
|
||||
|
||||
void dismissAsNo() {
|
||||
if (dismissed) return;
|
||||
dismissed = true;
|
||||
Navigator.of(context).pop();
|
||||
setState(() {
|
||||
_crossPaichaPending = false;
|
||||
});
|
||||
}
|
||||
|
||||
void confirmSwitch() {
|
||||
if (dismissed) return;
|
||||
dismissed = true;
|
||||
Navigator.of(context).pop();
|
||||
_switchToNewPaicha(newZongpaiNo);
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, setDialogState) {
|
||||
return KeyboardListener(
|
||||
focusNode: dialogFocus,
|
||||
onKeyEvent: (event) {
|
||||
if (event is! KeyDownEvent) return;
|
||||
final androidKey =
|
||||
_androidKeyCodeFromLogicalKey(event.logicalKey);
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowUp ||
|
||||
androidKey == 19) {
|
||||
setDialogState(() => selectedIndex = 0);
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown ||
|
||||
androidKey == 20) {
|
||||
setDialogState(() => selectedIndex = 1);
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.enter) {
|
||||
selectedIndex == 0 ? dismissAsNo() : confirmSwitch();
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
dismissAsNo();
|
||||
}
|
||||
},
|
||||
child: PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (!didPop) dismissAsNo();
|
||||
},
|
||||
child: AlertDialog(
|
||||
backgroundColor: Colors.orange.shade50,
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber, color: Colors.orange.shade800),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'跨排产号扫描',
|
||||
style: TextStyle(color: Colors.orange.shade900),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'总排号 $newZongpaiNo 不属于当前排产号($currentPaicha),'
|
||||
'是否切换到新的排产号?\n\n'
|
||||
'选择「是」将放弃当前已扫描的所有数据。',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_dialogOptionBtn(
|
||||
label: '否',
|
||||
selected: selectedIndex == 0,
|
||||
onTap: dismissAsNo,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_dialogOptionBtn(
|
||||
label: '是,切换排产号',
|
||||
selected: selectedIndex == 1,
|
||||
onTap: confirmSwitch,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
dialogFocus.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
Widget _dialogOptionBtn({
|
||||
required String label,
|
||||
required bool selected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 40,
|
||||
child: ElevatedButton(
|
||||
onPressed: onTap,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
selected ? Colors.blue.shade700 : Colors.grey.shade200,
|
||||
foregroundColor: selected ? Colors.white : Colors.black87,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontWeight: selected ? FontWeight.w800 : FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _switchToNewPaicha(String newZongpaiNo) {
|
||||
_feedbackService.trigger(FeedbackEvent.paichanSwitch);
|
||||
setState(() {
|
||||
_crossPaichaPending = false;
|
||||
_zongpaiNos.clear();
|
||||
_zongpaiNos.add(newZongpaiNo);
|
||||
_overview = null;
|
||||
_overviewNotFound = false;
|
||||
_overviewError = null;
|
||||
_clearStatusOverride();
|
||||
});
|
||||
_loadOverview(newZongpaiNo);
|
||||
}
|
||||
|
||||
void _toggleLock(bool value) {
|
||||
if (value && _locationCode == null) {
|
||||
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||||
@@ -623,17 +811,6 @@ 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;
|
||||
@@ -657,62 +834,10 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
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: registrationSubmitColor(
|
||||
canSubmit: _canSubmit,
|
||||
isTransitTarget: _isTransitTarget,
|
||||
primaryColor: colorScheme.primary,
|
||||
),
|
||||
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(
|
||||
registrationSubmitLabel(
|
||||
isTransitTarget: _isTransitTarget,
|
||||
isLocked: _isLocked,
|
||||
zongpaiCount: _zongpaiNos.length,
|
||||
),
|
||||
style: const TextStyle(fontSize: 17),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(height: 1, color: Colors.grey.shade300),
|
||||
Expanded(child: _buildOverviewSection()),
|
||||
_buildPaichaHeader(),
|
||||
if (_overview?.success == true) _buildTableHead(),
|
||||
Expanded(child: _buildListBody()),
|
||||
_buildBottomBar(colorScheme),
|
||||
StatusBar(dotColor: effectiveDot, text: effectiveText),
|
||||
],
|
||||
),
|
||||
@@ -770,130 +895,6 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -913,59 +914,188 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverviewSection() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// === Paicha Header ===
|
||||
|
||||
Widget _buildPaichaHeader() {
|
||||
final overview = _overview;
|
||||
final hasData = overview?.success == true;
|
||||
final hasLoc = _locationCode != null;
|
||||
|
||||
int totalCount = 0, shelvedCount = 0, transferredCount = 0;
|
||||
if (hasData && overview != null) {
|
||||
totalCount = overview.totalCount;
|
||||
for (final item in overview.items) {
|
||||
switch (item.status) {
|
||||
case 'on_shelf':
|
||||
shelvedCount++;
|
||||
case 'transferred':
|
||||
transferredCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
border: Border(bottom: BorderSide(color: Colors.grey.shade300)),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
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),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
hasData ? (overview!.paichaNo ?? '--') : '--',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: hasData ? Colors.black87 : Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (hasLoc)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_locationCode!,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
_buildLocationChip(),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildStatTag(
|
||||
'共 $totalCount',
|
||||
const Color(0xFFE3F2FD),
|
||||
const Color(0xFF1565C0),
|
||||
hasData,
|
||||
),
|
||||
_buildStatTag(
|
||||
'上架 $shelvedCount',
|
||||
const Color(0xFFE8F5E9),
|
||||
const Color(0xFF2E7D32),
|
||||
hasData,
|
||||
),
|
||||
_buildStatTag(
|
||||
'转运 $transferredCount',
|
||||
const Color(0xFFFFF3E0),
|
||||
const Color(0xFFE65100),
|
||||
hasData,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Expanded(child: _buildOverviewBody()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverviewBody() {
|
||||
Widget _buildStatTag(String text, Color bg, Color fg, bool hasData) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 1),
|
||||
margin: const EdgeInsets.only(left: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: hasData ? bg : const Color(0xFFF5F5F5),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: hasData ? fg : Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// === Table Head ===
|
||||
|
||||
Widget _buildTableHead() {
|
||||
return Container(
|
||||
height: 28,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
border: Border(bottom: BorderSide(color: Colors.grey.shade400)),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Expanded(flex: 22, child: Text('总排号', style: _headerStyle)),
|
||||
Expanded(flex: 18, child: Text('工令号', style: _headerStyle)),
|
||||
Expanded(
|
||||
flex: 12,
|
||||
child: Text(
|
||||
'数量',
|
||||
textAlign: TextAlign.center,
|
||||
style: _headerStyle,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 28,
|
||||
child: Text(
|
||||
'货位号',
|
||||
textAlign: TextAlign.right,
|
||||
style: _headerStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// === List Body ===
|
||||
|
||||
Widget _buildListBody() {
|
||||
if (_overviewZongpaiNo == null) {
|
||||
return _buildOverviewMessage('扫描总排号后自动显示排产号上架情况');
|
||||
return _buildListMessage('扫描总排号后自动显示排产号上架情况');
|
||||
}
|
||||
if (_overviewLoading && _overview == null) {
|
||||
return _buildOverviewMessage('正在加载排产号上架情况…');
|
||||
return _buildListMessage('正在加载排产号上架情况…');
|
||||
}
|
||||
if (_overviewNotFound) {
|
||||
return _buildOverviewMessage('暂无排产信息');
|
||||
return _buildListMessage('暂无排产信息');
|
||||
}
|
||||
if (_overviewError != null && _overview == null) {
|
||||
return _buildRetryMessage(_overviewError!);
|
||||
return _buildListError(_overviewError!);
|
||||
}
|
||||
final overview = _overview;
|
||||
if (overview == null) {
|
||||
return _buildOverviewMessage('扫描总排号后自动显示排产号上架情况');
|
||||
if (overview == null || !overview.success) {
|
||||
return _buildListMessage('扫描总排号后自动显示排产号上架情况');
|
||||
}
|
||||
|
||||
final scannedSet = _zongpaiNos.toSet();
|
||||
final scanned = <PaichaOverviewItem>[];
|
||||
final unscanned = <PaichaOverviewItem>[];
|
||||
for (final item in overview.items) {
|
||||
if (scannedSet.contains(item.zongpaiNo)) {
|
||||
scanned.add(item);
|
||||
} else {
|
||||
unscanned.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
final hasDivider = scanned.isNotEmpty && unscanned.isNotEmpty;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_overviewError != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
color: Colors.red.shade50,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -983,17 +1113,21 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
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,
|
||||
itemCount: scanned.length +
|
||||
(hasDivider ? 1 : 0) +
|
||||
unscanned.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildOverviewRow(overview.items[index]);
|
||||
if (index < scanned.length) {
|
||||
return _buildListRow(scanned[index], isScanned: true);
|
||||
}
|
||||
if (index == scanned.length && hasDivider) {
|
||||
return _buildSectionDivider(unscanned.length);
|
||||
}
|
||||
final unscannedIdx =
|
||||
index - scanned.length - (hasDivider ? 1 : 0);
|
||||
return _buildListRow(unscanned[unscannedIdx], isScanned: false);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -1001,62 +1135,70 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverviewHeader() {
|
||||
Widget _buildSectionDivider(int count) {
|
||||
return Container(
|
||||
height: 32,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
height: 24,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
border: Border(bottom: BorderSide(color: Colors.grey.shade200)),
|
||||
),
|
||||
child: const Row(
|
||||
child: 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),
|
||||
const Expanded(child: Divider(height: 0)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
'以下 $count 项未操作',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Expanded(child: Divider(height: 0)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverviewRow(PaichaOverviewItem item) {
|
||||
final current = item.zongpaiNo == _overviewZongpaiNo;
|
||||
Widget _buildListRow(PaichaOverviewItem item, {required bool isScanned}) {
|
||||
final rowStyle = TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: current ? FontWeight.w700 : FontWeight.w500,
|
||||
fontWeight: isScanned ? FontWeight.w700 : FontWeight.w500,
|
||||
color: Colors.black87,
|
||||
);
|
||||
|
||||
final Color barColor;
|
||||
final Color bgColor;
|
||||
if (isScanned) {
|
||||
final alreadyOnShelf = item.status == 'on_shelf' && !_isTransitTarget;
|
||||
if (alreadyOnShelf) {
|
||||
barColor = Colors.red;
|
||||
bgColor = Colors.red.shade50;
|
||||
} else {
|
||||
barColor = const Color(0xFF43A047);
|
||||
bgColor = const Color(0xFFF1F8E9);
|
||||
}
|
||||
} else {
|
||||
barColor = _barColor(item.status);
|
||||
bgColor = _rowBgColor(item.status);
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: const BoxConstraints(minHeight: 34),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
|
||||
constraints: const BoxConstraints(minHeight: 36),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: _overviewRowColor(item.status),
|
||||
color: bgColor,
|
||||
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,
|
||||
),
|
||||
left: BorderSide(color: barColor, width: 3),
|
||||
bottom: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 20,
|
||||
flex: 22,
|
||||
child: Text(
|
||||
item.zongpaiNo,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -1064,7 +1206,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 20,
|
||||
flex: 18,
|
||||
child: Text(
|
||||
item.workOrderNo ?? '--',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -1072,7 +1214,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 14,
|
||||
flex: 12,
|
||||
child: Text(
|
||||
item.quantity.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
@@ -1081,20 +1223,56 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 26,
|
||||
flex: 28,
|
||||
child: Text(
|
||||
item.locationCode ?? '—',
|
||||
item.locationCode ?? '\u2014',
|
||||
textAlign: TextAlign.right,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: rowStyle,
|
||||
),
|
||||
),
|
||||
if (isScanned && _isLocked) ...[
|
||||
const SizedBox(width: 6),
|
||||
SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.delete_outline, size: 16),
|
||||
color: Colors.red,
|
||||
onPressed: () => _removeZongpai(item.zongpaiNo),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverviewMessage(String text) {
|
||||
Color _barColor(String status) {
|
||||
switch (status) {
|
||||
case 'on_shelf':
|
||||
return const Color(0xFF2196F3);
|
||||
case 'transferred':
|
||||
return const Color(0xFFFF9800);
|
||||
default:
|
||||
return Colors.grey.shade400;
|
||||
}
|
||||
}
|
||||
|
||||
Color _rowBgColor(String status) {
|
||||
switch (status) {
|
||||
case 'on_shelf':
|
||||
return const Color(0xFFE3F2FD);
|
||||
case 'transferred':
|
||||
return const Color(0xFFFFF3E0);
|
||||
default:
|
||||
return const Color(0xFFF5F5F5);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildListMessage(String text) {
|
||||
return Center(
|
||||
child: Text(
|
||||
text,
|
||||
@@ -1104,7 +1282,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRetryMessage(String text) {
|
||||
Widget _buildListError(String text) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -1125,6 +1303,57 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// === Bottom Bar ===
|
||||
|
||||
Widget _buildBottomBar(ColorScheme colorScheme) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: Colors.grey.shade300)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 34,
|
||||
child: ElevatedButton(
|
||||
onPressed: _canSubmit ? _submit : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: registrationSubmitColor(
|
||||
canSubmit: _canSubmit,
|
||||
isTransitTarget: _isTransitTarget,
|
||||
primaryColor: colorScheme.primary,
|
||||
),
|
||||
foregroundColor:
|
||||
_canSubmit ? Colors.white : Colors.grey.shade600,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
child: _isSubmitting
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
registrationSubmitLabel(
|
||||
isTransitTarget: _isTransitTarget,
|
||||
isLocked: _isLocked,
|
||||
zongpaiCount: _zongpaiNos.length,
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const _headerStyle = TextStyle(fontSize: 12, fontWeight: FontWeight.w700);
|
||||
const _headerStyle = TextStyle(fontSize: 10, fontWeight: FontWeight.w700);
|
||||
|
||||
Reference in New Issue
Block a user