From 8c967a0f99f0b4c7bb7c1fa2e814241282d603b9 Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Tue, 19 May 2026 15:57:39 +0800 Subject: [PATCH] redesign registration page: compact header layout, cross-paicha dialog, and locked-mode enhancements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- lib/pages/registration_page.dart | 773 ++++++++++++++++++++----------- 1 file changed, 501 insertions(+), 272 deletions(-) diff --git a/lib/pages/registration_page.dart b/lib/pages/registration_page.dart index db0de9f..41d2e90 100644 --- a/lib/pages/registration_page.dart +++ b/lib/pages/registration_page.dart @@ -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 { 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 { } 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 { } } + 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 { 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 _baseUrl() async { final configService = AppConfigService(); final baseUrl = await configService.getString('api_url') ?? ''; @@ -298,6 +335,19 @@ class _RegistrationPageState extends State { ); 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 { ); } + 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 { 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 { ), 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 { ); } - 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 { ); } - 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 = []; + final unscanned = []; + 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 { ], ), ), - 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 { ); } - 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 { ), ), Expanded( - flex: 20, + flex: 18, child: Text( item.workOrderNo ?? '--', overflow: TextOverflow.ellipsis, @@ -1072,7 +1214,7 @@ class _RegistrationPageState extends State { ), ), Expanded( - flex: 14, + flex: 12, child: Text( item.quantity.toString(), textAlign: TextAlign.center, @@ -1081,20 +1223,56 @@ class _RegistrationPageState extends State { ), ), 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 { ); } - Widget _buildRetryMessage(String text) { + Widget _buildListError(String text) { return Center( child: Column( mainAxisSize: MainAxisSize.min, @@ -1125,6 +1303,57 @@ class _RegistrationPageState extends State { ), ); } + + // === 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);