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:
Misaka_Company
2026-05-19 15:57:39 +08:00
parent d9f8e066b3
commit 8c967a0f99

View File

@@ -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/api_service.dart';
import 'package:pad_scanner/services/feedback_service.dart'; import 'package:pad_scanner/services/feedback_service.dart';
import 'package:pad_scanner/services/registration_linking.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/pages/boxing_page.dart';
import 'package:pad_scanner/widgets/status_bar.dart'; import 'package:pad_scanner/widgets/status_bar.dart';
@@ -45,6 +46,9 @@ class _RegistrationPageState extends State<RegistrationPage> {
PaichaOverviewResult? _overview; PaichaOverviewResult? _overview;
int _overviewRequestId = 0; int _overviewRequestId = 0;
/// Whether a cross-paicha warning dialog is currently showing.
bool _crossPaichaPending = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -136,6 +140,23 @@ class _RegistrationPageState extends State<RegistrationPage> {
} }
void _handleZongpaiScan(String zongpaiNo) { 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; final shouldRefresh = _overviewZongpaiNo != zongpaiNo;
_feedbackService.trigger(FeedbackEvent.scanValid); _feedbackService.trigger(FeedbackEvent.scanValid);
setState(() { setState(() {
@@ -157,6 +178,10 @@ class _RegistrationPageState extends State<RegistrationPage> {
} }
} }
void _triggerDoubleVibration() {
Vibration.vibrate(pattern: [0, 200, 100, 200]);
}
void _removeZongpai(String zongpaiNo) { void _removeZongpai(String zongpaiNo) {
setState(() { setState(() {
_zongpaiNos.remove(zongpaiNo); _zongpaiNos.remove(zongpaiNo);
@@ -211,6 +236,18 @@ class _RegistrationPageState extends State<RegistrationPage> {
bool get _isTransitTarget => isTransitTarget(_locationType, _locationCode); 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 { Future<String?> _baseUrl() async {
final configService = AppConfigService(); final configService = AppConfigService();
final baseUrl = await configService.getString('api_url') ?? ''; final baseUrl = await configService.getString('api_url') ?? '';
@@ -298,6 +335,19 @@ class _RegistrationPageState extends State<RegistrationPage> {
); );
return; 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); await _submitBatch(baseUrl);
} else { } else {
await _submitOne(baseUrl, _zongpaiNos.first); 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) { void _toggleLock(bool value) {
if (value && _locationCode == null) { if (value && _locationCode == null) {
_feedbackService.trigger(FeedbackEvent.submitFailure); _feedbackService.trigger(FeedbackEvent.submitFailure);
@@ -623,17 +811,6 @@ 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;
@@ -657,62 +834,10 @@ class _RegistrationPageState extends State<RegistrationPage> {
), ),
body: Column( body: Column(
children: [ children: [
Padding( _buildPaichaHeader(),
padding: const EdgeInsets.fromLTRB(12, 10, 12, 8), if (_overview?.success == true) _buildTableHead(),
child: Column( Expanded(child: _buildListBody()),
crossAxisAlignment: CrossAxisAlignment.start, _buildBottomBar(colorScheme),
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()),
StatusBar(dotColor: effectiveDot, text: effectiveText), 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() { Widget _buildLocationChip() {
final color = _locationLabelColor(_locationType); final color = _locationLabelColor(_locationType);
return Container( return Container(
@@ -913,59 +914,188 @@ class _RegistrationPageState extends State<RegistrationPage> {
); );
} }
Widget _buildOverviewSection() { // === Paicha Header ===
return Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), Widget _buildPaichaHeader() {
child: Column( final overview = _overview;
crossAxisAlignment: CrossAxisAlignment.start, 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: [ children: [
Row( Text(
children: [ hasData ? (overview!.paichaNo ?? '--') : '--',
const Expanded( style: TextStyle(
child: Text( fontSize: 24,
'排产号货架总览', fontWeight: FontWeight.w800,
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700), color: hasData ? Colors.black87 : Colors.grey.shade400,
), ),
), ),
if (_overviewLoading) const SizedBox(width: 12),
const SizedBox( Expanded(
width: 16, child: Column(
height: 16, crossAxisAlignment: CrossAxisAlignment.end,
child: CircularProgressIndicator(strokeWidth: 2), 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) { if (_overviewZongpaiNo == null) {
return _buildOverviewMessage('扫描总排号后自动显示排产号上架情况'); return _buildListMessage('扫描总排号后自动显示排产号上架情况');
} }
if (_overviewLoading && _overview == null) { if (_overviewLoading && _overview == null) {
return _buildOverviewMessage('正在加载排产号上架情况…'); return _buildListMessage('正在加载排产号上架情况…');
} }
if (_overviewNotFound) { if (_overviewNotFound) {
return _buildOverviewMessage('暂无排产信息'); return _buildListMessage('暂无排产信息');
} }
if (_overviewError != null && _overview == null) { if (_overviewError != null && _overview == null) {
return _buildRetryMessage(_overviewError!); return _buildListError(_overviewError!);
} }
final overview = _overview; final overview = _overview;
if (overview == null) { if (overview == null || !overview.success) {
return _buildOverviewMessage('扫描总排号后自动显示排产号上架情况'); 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( return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (_overviewError != null) if (_overviewError != null)
Padding( Container(
padding: const EdgeInsets.only(bottom: 6), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
color: Colors.red.shade50,
child: Row( child: Row(
children: [ children: [
Expanded( 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( Expanded(
child: ListView.builder( child: ListView.builder(
itemCount: overview.items.length, itemCount: scanned.length +
(hasDivider ? 1 : 0) +
unscanned.length,
itemBuilder: (context, index) { 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( return Container(
height: 32, height: 24,
padding: const EdgeInsets.symmetric(horizontal: 6), padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade200, border: Border(bottom: BorderSide(color: Colors.grey.shade200)),
border: Border.all(color: Colors.grey.shade400),
), ),
child: const Row( child: Row(
children: [ children: [
Expanded(flex: 20, child: Text('总排号', style: _headerStyle)), const Expanded(child: Divider(height: 0)),
Expanded(flex: 20, child: Text('工令号', style: _headerStyle)), Padding(
Expanded( padding: const EdgeInsets.symmetric(horizontal: 8),
flex: 14, child: Text(
child: Text('数量', textAlign: TextAlign.center, style: _headerStyle), '以下 $count 项未操作',
), style: TextStyle(
Expanded( fontSize: 10,
flex: 26, fontWeight: FontWeight.w600,
child: Text('货位号', textAlign: TextAlign.right, style: _headerStyle), color: Colors.grey.shade500,
),
),
), ),
const Expanded(child: Divider(height: 0)),
], ],
), ),
); );
} }
Widget _buildOverviewRow(PaichaOverviewItem item) { Widget _buildListRow(PaichaOverviewItem item, {required bool isScanned}) {
final current = item.zongpaiNo == _overviewZongpaiNo;
final rowStyle = TextStyle( final rowStyle = TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: current ? FontWeight.w700 : FontWeight.w500, fontWeight: isScanned ? FontWeight.w700 : FontWeight.w500,
color: Colors.black87, 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( return Container(
constraints: const BoxConstraints(minHeight: 34), constraints: const BoxConstraints(minHeight: 36),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: _overviewRowColor(item.status), color: bgColor,
border: Border( border: Border(
left: BorderSide( left: BorderSide(color: barColor, width: 3),
color: current ? Colors.green : Colors.grey.shade300, bottom: BorderSide(color: Colors.grey.shade200),
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( child: Row(
children: [ children: [
Expanded( Expanded(
flex: 20, flex: 22,
child: Text( child: Text(
item.zongpaiNo, item.zongpaiNo,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@@ -1064,7 +1206,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
), ),
), ),
Expanded( Expanded(
flex: 20, flex: 18,
child: Text( child: Text(
item.workOrderNo ?? '--', item.workOrderNo ?? '--',
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@@ -1072,7 +1214,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
), ),
), ),
Expanded( Expanded(
flex: 14, flex: 12,
child: Text( child: Text(
item.quantity.toString(), item.quantity.toString(),
textAlign: TextAlign.center, textAlign: TextAlign.center,
@@ -1081,20 +1223,56 @@ class _RegistrationPageState extends State<RegistrationPage> {
), ),
), ),
Expanded( Expanded(
flex: 26, flex: 28,
child: Text( child: Text(
item.locationCode ?? '', item.locationCode ?? '\u2014',
textAlign: TextAlign.right, textAlign: TextAlign.right,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: rowStyle, 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( return Center(
child: Text( child: Text(
text, text,
@@ -1104,7 +1282,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
); );
} }
Widget _buildRetryMessage(String text) { Widget _buildListError(String text) {
return Center( return Center(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, 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);