diff --git a/docs/lock-location-redesign-preview.html b/docs/lock-location-redesign-preview.html new file mode 100644 index 0000000..78f7b81 --- /dev/null +++ b/docs/lock-location-redesign-preview.html @@ -0,0 +1,443 @@ + + + + + +锁定货位 - 布局重设计预览 + + + + +

锁定货位 — 布局重设计预览

+ +
+ + +
+
现有设计
+ +
+

未锁定

+
+
+ + 上架登记 + +
+ 锁定货位 + P2 +
+
+
+
+ +
+
+ A-01-02 + 普通货架 + | + ZP2024001 +
+
+ +
+
+
+
排产号货架总览
+
扫描总排号后自动显示排产号上架情况
+
+
请确认信息并提交
+
+

锁定开关挤在 AppBar 右侧,字小、触控区小

+
+ +
+

已锁定

+
+
+ + 上架登记 + +
+ 锁定货位 + P2 +
+
+
+
+ +
+
+ A-01-02 + 🔒 + 普通货架 +
+
+ ZP2024001 × + ZP2024002 × + ZP2024003 × +
+
+ +
+
+
+
排产号货架总览
+
扫描总排号后自动显示排产号上架情况
+
+
货位已锁定,请扫描下一张执行卡
+
+

锁定状态仅靠边框颜色区分,不够醒目

+
+
+ + +
+
新设计(与装箱编号统一设计语言)
+ + +
+

未锁定

+
+
+ + 上架登记 + + +
+ + 未锁定 + P2 +
+
+
+ +
+
+ A-01-02 + 普通货架 + | + ZP2024001 +
+
+ +
+
+
+
排产号货架总览
+
扫描总排号后自动显示排产号上架情况
+
+
请确认信息并提交
+
+

+ 蓝色药丸 = 未锁定(与装箱页"单码装箱"同色)
+ 点击或按 P2 切换为锁定 +

+
+ + +
+

已锁定

+
+
+ + 上架登记 + + +
+ + 货位锁定 + P2 +
+
+
+ +
+
+ A-01-02 + 🔒 + 普通货架 +
+
+ ZP2024001 × + ZP2024002 × + ZP2024003 × +
+
+ +
+
+
+
排产号货架总览
+
扫描总排号后自动显示排产号上架情况
+
+
货位已锁定,请扫描下一张执行卡
+
+

+ 橙色药丸 = 已锁定(与装箱页"多码凑箱"同色)
+ 输入面板下方内容不变,仅切换控件统一风格 +

+
+
+ +
+ + +
+

设计语言对照

+ +
+ 装箱编号 +
+ + 单码装箱 + P2 +
+
+ + 多码凑箱 + P2 +
+
+ +
+ 上架登记 +
+ + 未锁定 + P2 +
+
+ + 货位锁定 + P2 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
属性装箱编号上架登记(新)
组件位置AppBar 标题行内(标题 + 药丸)AppBar 标题行内(标题 + 药丸)
交互方式点击药丸 / 按 P2 切换点击药丸 / 按 P2 切换
默认态(蓝色)单码装箱未锁定
切换态(橙色)多码凑箱货位锁定
药丸结构swap 图标 + 模式名 + P2swap 图标 + 状态名 + P2
圆角 / 字重8px / w8008px / w800
+
+ + + diff --git a/lib/pages/boxing_page.dart b/lib/pages/boxing_page.dart index 7931437..1fb585f 100644 --- a/lib/pages/boxing_page.dart +++ b/lib/pages/boxing_page.dart @@ -84,9 +84,12 @@ class BoxingPage extends StatefulWidget { } class _BoxingPageState extends State { + static const _androidKeyCodeButtonR1 = 103; + final _scannerService = ScannerService(); final _apiService = ApiService(); final _feedbackService = FeedbackService(); + final _focusNode = FocusNode(); StreamSubscription? _scanSubscription; // 模式 @@ -155,12 +158,14 @@ class _BoxingPageState extends State { _processAutoScanCodes(arguments.autoScanCodes); }); } + WidgetsBinding.instance.addPostFrameCallback((_) => _requestFocus()); } @override void dispose() { _boxNoController.removeListener(_onBoxNoTextChanged); _scanSubscription?.cancel(); + _focusNode.dispose(); _boxNoController.dispose(); _quantityController.dispose(); _boxNoFocusNode.dispose(); @@ -169,6 +174,35 @@ class _BoxingPageState extends State { super.dispose(); } + void _requestFocus() { + if (mounted) { + _focusNode.requestFocus(); + } + } + + void _onKeyEvent(KeyEvent event) { + if (event is! KeyDownEvent || _isAutoProcessing) return; + if (_isModeSwitchKey(event)) { + _cycleMode(); + } + } + + bool _isModeSwitchKey(KeyEvent event) { + final key = event.logicalKey; + return key == LogicalKeyboardKey.select || + key == LogicalKeyboardKey.gameButtonRight1 || + _androidKeyCodeFromLogicalKey(key) == _androidKeyCodeButtonR1; + } + + int? _androidKeyCodeFromLogicalKey(LogicalKeyboardKey key) { + final keyId = key.keyId; + const androidPlane = LogicalKeyboardKey.androidPlane; + if (keyId >= androidPlane && keyId < androidPlane + 0x100000000) { + return keyId - androidPlane; + } + return null; + } + void _onBoxNoTextChanged() { if (!mounted || _mode != BoxingMode.multiCode) return; setState(() { @@ -360,6 +394,7 @@ class _BoxingPageState extends State { _statusOverrideDot = null; }); _focusQuantityInput(); + _requestFocus(); } void _applyAutoFill() { @@ -1253,163 +1288,229 @@ class _BoxingPageState extends State { final effectiveDot = _statusOverrideDot ?? _statusDot; final effectiveText = _statusOverrideText ?? _statusText; - return Scaffold( - appBar: AppBar( - title: const Text('装箱编号'), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 4), - child: TextButton.icon( - onPressed: _isAutoProcessing ? null : _cycleMode, - icon: const Icon(Icons.swap_horiz, size: 18), - label: Text(_modeLabel, style: const TextStyle(fontSize: 13)), - style: TextButton.styleFrom( - foregroundColor: _mode == BoxingMode.multiCode - ? Colors.orange - : colorScheme.primary, - ), - ), + return KeyboardListener( + focusNode: _focusNode, + onKeyEvent: _onKeyEvent, + child: Scaffold( + appBar: AppBar( + title: Row( + children: [ + const Text('装箱编号'), + const SizedBox(width: 12), + Expanded(child: _buildModeHeader()), + ], ), - ], - ), - body: Column( - children: [ - if (_isAutoProcessing) - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - color: Colors.orange.shade50, - child: Row( - children: [ - SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.orange.shade700, - ), - ), - const SizedBox(width: 8), - Text( - '正在同步转运数据...', - style: TextStyle( - color: Colors.orange.shade900, - fontSize: 13, - ), - ), - ], - ), - ), - if (_autoWarnings.isNotEmpty) - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - color: Colors.amber.shade100, - child: Text( - _autoWarnings.join(';'), - style: TextStyle(color: Colors.amber.shade900, fontSize: 13), - ), - ), - // 重复箱号警告条(保留,因为这是输入区关联的即时反馈) - if (_paichanSwitchNotice != null) - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - color: Colors.blue.shade50, - child: Text( - _paichanSwitchNotice!, - style: TextStyle(color: Colors.blue.shade900, fontSize: 13), - ), - ), - if (_isDuplicateBoxNo && _phase == _Phase.scanned) - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - color: Colors.amber.shade100, - child: Text( - '箱号 ${_boxNoController.text} 已存在,请重新输入', - style: TextStyle(color: Colors.amber.shade900, fontSize: 13), - ), - ), - - Expanded( - child: _mode == BoxingMode.multiCode - ? _buildManyToOneBody(colorScheme) - : SingleChildScrollView( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildScanArea(isWaiting), - const SizedBox(height: 12), - _buildInfoArea(isWaiting, colorScheme), - const Divider(height: 24), - if (_mode == BoxingMode.singleCode && - !isWaiting && - _currentZongpaiBoxes.isNotEmpty) ...[ - _buildAssignedBoxes(), - const Divider(height: 24), - ], - _buildInputArea(isWaiting), - ], - ), - ), - ), - - // 操作按钮(单码装箱 / 多码凑箱) - if (showActionButtons) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - child: Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: _onGoBack, - style: OutlinedButton.styleFrom( - minimumSize: const Size.fromHeight(40), + titleSpacing: 12, + ), + body: Column( + children: [ + if (_isAutoProcessing) + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + color: Colors.orange.shade50, + child: Row( + children: [ + SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.orange.shade700, + ), + ), + const SizedBox(width: 8), + Text( + '正在同步转运数据...', + style: TextStyle( + color: Colors.orange.shade900, + fontSize: 13, ), - child: const Text('返回'), ), - ), - if (_mode == BoxingMode.singleCode && - _editingBox == null) ...[ - const SizedBox(width: 12), - const Expanded(child: SizedBox.shrink()), ], - if (_mode == BoxingMode.singleCode && - _editingBox != null) ...[ - const SizedBox(width: 12), + ), + ), + if (_autoWarnings.isNotEmpty) + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + color: Colors.amber.shade100, + child: Text( + _autoWarnings.join(';'), + style: TextStyle(color: Colors.amber.shade900, fontSize: 13), + ), + ), + // 重复箱号警告条(保留,因为这是输入区关联的即时反馈) + if (_paichanSwitchNotice != null) + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + color: Colors.blue.shade50, + child: Text( + _paichanSwitchNotice!, + style: TextStyle(color: Colors.blue.shade900, fontSize: 13), + ), + ), + if (_isDuplicateBoxNo && _phase == _Phase.scanned) + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + color: Colors.amber.shade100, + child: Text( + '箱号 ${_boxNoController.text} 已存在,请重新输入', + style: TextStyle(color: Colors.amber.shade900, fontSize: 13), + ), + ), + + Expanded( + child: _mode == BoxingMode.multiCode + ? _buildManyToOneBody(colorScheme) + : SingleChildScrollView( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildScanArea(isWaiting), + const SizedBox(height: 12), + _buildInfoArea(isWaiting, colorScheme), + const Divider(height: 24), + if (_mode == BoxingMode.singleCode && + !isWaiting && + _currentZongpaiBoxes.isNotEmpty) ...[ + _buildAssignedBoxes(), + const Divider(height: 24), + ], + _buildInputArea(isWaiting), + ], + ), + ), + ), + + // 操作按钮(单码装箱 / 多码凑箱) + if (showActionButtons) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + child: Row( + children: [ Expanded( - child: ElevatedButton( - onPressed: _cancelEditAssigned, - style: ElevatedButton.styleFrom( + child: OutlinedButton( + onPressed: _onGoBack, + style: OutlinedButton.styleFrom( minimumSize: const Size.fromHeight(40), ), - child: const Text('取消'), + child: const Text('返回'), ), ), + if (_mode == BoxingMode.singleCode && + _editingBox == null) ...[ + const SizedBox(width: 12), + const Expanded(child: SizedBox.shrink()), + ], + if (_mode == BoxingMode.singleCode && + _editingBox != null) ...[ + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: _cancelEditAssigned, + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(40), + ), + child: const Text('取消'), + ), + ), + ], ], - ], - ), - ), - - if (_mode == BoxingMode.multiCode) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - child: OutlinedButton( - onPressed: _visibleManyToOnePackedItems.isEmpty - ? null - : _finishManyToOneBox, - style: OutlinedButton.styleFrom( - minimumSize: const Size.fromHeight(40), ), - child: const Text('完成本箱'), ), - ), - // 底部状态栏 - StatusBar(dotColor: effectiveDot, text: effectiveText), - ], + if (_mode == BoxingMode.multiCode) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + child: OutlinedButton( + onPressed: _visibleManyToOnePackedItems.isEmpty + ? null + : _finishManyToOneBox, + style: OutlinedButton.styleFrom( + minimumSize: const Size.fromHeight(40), + ), + child: const Text('完成本箱'), + ), + ), + + // 底部状态栏 + StatusBar(dotColor: effectiveDot, text: effectiveText), + ], + ), + ), + ); + } + + Widget _buildModeHeader() { + final isSingle = _mode == BoxingMode.singleCode; + final backgroundColor = isSingle + ? Colors.blue.shade700 + : Colors.orange.shade700; + final foregroundColor = Colors.white; + + return Material( + borderRadius: BorderRadius.circular(8), + color: backgroundColor, + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: _isAutoProcessing ? null : _cycleMode, + child: Container( + width: double.infinity, + height: 36, + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.swap_horiz, color: foregroundColor, size: 18), + const SizedBox(width: 6), + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + _modeLabel, + maxLines: 1, + style: TextStyle( + color: foregroundColor, + fontSize: 18, + fontWeight: FontWeight.w800, + ), + ), + ), + ), + const SizedBox(width: 8), + Text( + 'P2', + style: TextStyle( + color: foregroundColor.withValues(alpha: 0.88), + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), ), ); } diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index b823f29..1896a6a 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:pad_scanner/services/app_config_service.dart'; import 'package:pad_scanner/pages/settings_page.dart'; import 'package:pad_scanner/pages/registration_page.dart'; @@ -14,20 +15,18 @@ enum ModuleStatus { online, developing, planning } /// Function card data model per PRD §3.3 class FeatureCard { final ModuleType type; + final String shortcut; final IconData icon; final String title; - final String description; final ModuleStatus status; - final String? version; final String route; const FeatureCard({ required this.type, + required this.shortcut, required this.icon, required this.title, - required this.description, required this.status, - this.version, required this.route, }); } @@ -40,7 +39,12 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { + static const _androidKeyCode1 = 8; + static const _androidKeyCode2 = 9; + static const _androidKeyCode3 = 10; + final _apiService = ApiService(); + final _focusNode = FocusNode(); bool _isCheckingConnection = false; bool _isConnected = false; String _apiAddress = ''; @@ -49,29 +53,26 @@ class _HomePageState extends State { static const _modules = [ FeatureCard( type: ModuleType.registration, + shortcut: '1', icon: Icons.local_shipping_outlined, title: '上架登记', - description: '扫码绑定总排号与货位', status: ModuleStatus.online, - version: 'v1.0', route: '/registration', ), FeatureCard( type: ModuleType.boxing, + shortcut: '2', icon: Icons.view_list_outlined, title: '装箱编号', - description: '查询箱号、录入装箱明细', status: ModuleStatus.online, - version: 'v1.0', route: '/boxing', ), FeatureCard( type: ModuleType.shelfQuery, + shortcut: '3', icon: Icons.search_outlined, title: '货架查询', - description: '按合同查询货架库存', status: ModuleStatus.planning, - version: null, route: '', ), ]; @@ -80,6 +81,77 @@ class _HomePageState extends State { void initState() { super.initState(); _checkConnection(); + WidgetsBinding.instance.addPostFrameCallback((_) => _requestFocus()); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + void _requestFocus() { + if (mounted) { + _focusNode.requestFocus(); + } + } + + void _onKeyEvent(KeyEvent event) { + if (event is! KeyDownEvent) return; + final module = _moduleFromShortcut(event); + if (module == null) return; + _onCardTap(module); + } + + FeatureCard? _moduleFromShortcut(KeyEvent event) { + final shortcut = _shortcutNumberFromKeyEvent(event); + if (shortcut == 1) { + return _modules[0]; + } + if (shortcut == 2) { + return _modules[1]; + } + if (shortcut == 3) { + return _modules[2]; + } + return null; + } + + int? _shortcutNumberFromKeyEvent(KeyEvent event) { + final key = event.logicalKey; + if (key == LogicalKeyboardKey.digit1 || + key == LogicalKeyboardKey.numpad1 || + event.character == '1') { + return 1; + } + if (key == LogicalKeyboardKey.digit2 || + key == LogicalKeyboardKey.numpad2 || + event.character == '2') { + return 2; + } + if (key == LogicalKeyboardKey.digit3 || + key == LogicalKeyboardKey.numpad3 || + event.character == '3') { + return 3; + } + + // Android keyCode fallback for scanner devices: 1/2/3 => 8/9/10. + final androidKeyCode = _androidKeyCodeFromLogicalKey(key); + return switch (androidKeyCode) { + _androidKeyCode1 => 1, + _androidKeyCode2 => 2, + _androidKeyCode3 => 3, + _ => null, + }; + } + + int? _androidKeyCodeFromLogicalKey(LogicalKeyboardKey key) { + final keyId = key.keyId; + const androidPlane = LogicalKeyboardKey.androidPlane; + if (keyId >= androidPlane && keyId < androidPlane + 0x100000000) { + return keyId - androidPlane; + } + return null; } Future _checkConnection() async { @@ -133,17 +205,22 @@ class _HomePageState extends State { case ModuleType.shelfQuery: return; // 规划中,不导航 } - Navigator.push( - context, - MaterialPageRoute(builder: (_) => targetPage), - ).then((_) => _checkConnection()); + Navigator.push(context, MaterialPageRoute(builder: (_) => targetPage)).then( + (_) { + _checkConnection(); + _requestFocus(); + }, + ); } void _navigateToSettings() { Navigator.push( context, MaterialPageRoute(builder: (_) => const SettingsPage()), - ).then((_) => _checkConnection()); + ).then((_) { + _checkConnection(); + _requestFocus(); + }); } Color _getConnectionStatusColor() { @@ -161,87 +238,70 @@ class _HomePageState extends State { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - return Scaffold( - appBar: AppBar( - title: const Text('CargoTrace'), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 8), - child: IconButton( - icon: const Icon(Icons.settings), - onPressed: _navigateToSettings, + return KeyboardListener( + focusNode: _focusNode, + onKeyEvent: _onKeyEvent, + child: Scaffold( + appBar: AppBar( + title: const SizedBox.shrink(), + toolbarHeight: 44, + actions: [ + Padding( + padding: const EdgeInsets.only(right: 8), + child: IconButton( + icon: const Icon(Icons.settings), + onPressed: _navigateToSettings, + ), ), - ), - ], - ), - body: Column( - children: [ - // Function cards area - Expanded( - child: ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - itemCount: _modules.length, - itemBuilder: (context, index) { - final card = _modules[index]; - return _buildFeatureCard(context, card); - }, + ], + ), + body: Column( + children: [ + // Function cards area + Expanded( + child: ListView.builder( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 4), + itemCount: _modules.length, + itemBuilder: (context, index) { + final card = _modules[index]; + return _buildFeatureCard(context, card); + }, + ), ), - ), - // Bottom connection status bar - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), - color: colorScheme.surfaceContainerHighest, - child: Row( - children: [ - Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: _getConnectionStatusColor(), - shape: BoxShape.circle, + // Bottom connection status bar + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), + color: colorScheme.surfaceContainerHighest, + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: _getConnectionStatusColor(), + shape: BoxShape.circle, + ), ), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - _getConnectionStatusText(), - style: const TextStyle(fontSize: 13), - overflow: TextOverflow.ellipsis, + const SizedBox(width: 8), + Expanded( + child: Text( + _getConnectionStatusText(), + style: const TextStyle(fontSize: 13), + overflow: TextOverflow.ellipsis, + ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ); } Widget _buildFeatureCard(BuildContext context, FeatureCard card) { - final Color statusBgColor; - final TextStyle statusTextStyle; - final String statusText; - - switch (card.status) { - case ModuleStatus.online: - statusBgColor = Colors.green.shade700; - statusTextStyle = const TextStyle(color: Colors.white, fontSize: 12); - statusText = card.version ?? '已上线'; - break; - case ModuleStatus.developing: - statusBgColor = Colors.blue.shade700; - statusTextStyle = const TextStyle(color: Colors.white, fontSize: 12); - statusText = '开发中'; - break; - case ModuleStatus.planning: - statusBgColor = Colors.grey.shade500; - statusTextStyle = const TextStyle(color: Colors.white, fontSize: 12); - statusText = '规划中'; - break; - } - final canNavigate = card.status == ModuleStatus.online; return Padding( @@ -250,7 +310,7 @@ class _HomePageState extends State { onTap: () => _onCardTap(card), borderRadius: BorderRadius.circular(8), child: Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: canNavigate ? Colors.white : Colors.grey.shade200, borderRadius: BorderRadius.circular(8), @@ -262,80 +322,49 @@ class _HomePageState extends State { ), ], ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Row( children: [ - // First row: icon + module name - Row( - children: [ - Icon( - card.icon, - size: 22, - color: canNavigate ? Colors.blue : Colors.grey, - ), - const SizedBox(width: 8), - Text( - card.title, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: canNavigate ? Colors.black87 : Colors.grey, - ), - ), - ], + Icon( + card.icon, + size: 22, + color: canNavigate ? Colors.blue : Colors.grey, ), - const SizedBox(height: 6), - // Second row: description + const SizedBox(width: 8), Text( - card.description, - style: TextStyle(fontSize: 13, color: Colors.grey.shade600), - ), - const SizedBox(height: 10), - // Third row: status badge (right aligned) - Align( - alignment: Alignment.centerRight, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 3, - ), - decoration: BoxDecoration( - color: statusBgColor, - borderRadius: BorderRadius.circular(4), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(statusText, style: statusTextStyle), - if (card.status == ModuleStatus.online) ...[ - const SizedBox(width: 6), - Container( - width: 6, - height: 6, - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - ), - ] else ...[ - const SizedBox(width: 6), - Container( - width: 6, - height: 6, - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.5), - shape: BoxShape.circle, - ), - ), - ], - ], - ), + card.title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: canNavigate ? Colors.black87 : Colors.grey, ), ), + const Spacer(), + _buildShortcutHint(card, canNavigate), ], ), ), ), ); } + + Widget _buildShortcutHint(FeatureCard card, bool canNavigate) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: canNavigate ? Colors.blue.shade50 : Colors.grey.shade300, + border: Border.all( + color: canNavigate ? Colors.blue.shade200 : Colors.grey.shade400, + ), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + '按 ${card.shortcut} 进入', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: canNavigate ? Colors.blue.shade800 : Colors.grey.shade600, + ), + ), + ); + } } diff --git a/lib/pages/registration_page.dart b/lib/pages/registration_page.dart index 722ffdb..db0de9f 100644 --- a/lib/pages/registration_page.dart +++ b/lib/pages/registration_page.dart @@ -19,6 +19,8 @@ class RegistrationPage extends StatefulWidget { } class _RegistrationPageState extends State { + static const _androidKeyCodeButtonR1 = 103; + final _scannerService = ScannerService(); final _apiService = ApiService(); final _feedbackService = FeedbackService(); @@ -82,11 +84,32 @@ class _RegistrationPageState extends State { } void _onKeyEvent(KeyEvent event) { - if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.enter) { + if (event is! KeyDownEvent) return; + if (_isLockToggleKey(event)) { + _toggleLock(!_isLocked); + return; + } + if (event.logicalKey == LogicalKeyboardKey.enter) { _submit(); } } + bool _isLockToggleKey(KeyEvent event) { + final key = event.logicalKey; + return key == LogicalKeyboardKey.select || + key == LogicalKeyboardKey.gameButtonRight1 || + _androidKeyCodeFromLogicalKey(key) == _androidKeyCodeButtonR1; + } + + int? _androidKeyCodeFromLogicalKey(LogicalKeyboardKey key) { + final keyId = key.keyId; + const androidPlane = LogicalKeyboardKey.androidPlane; + if (keyId >= androidPlane && keyId < androidPlane + 0x100000000) { + return keyId - androidPlane; + } + return null; + } + void _onScan(ScanResult result) { final parsed = CodeParser.parse(result.barcode); switch (parsed.type) { @@ -623,18 +646,14 @@ class _RegistrationPageState extends State { onKeyEvent: _onKeyEvent, child: Scaffold( appBar: AppBar( - title: const Text('上架登记'), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 4), - child: Row( - children: [ - const Text('锁定货位', style: TextStyle(fontSize: 13)), - Switch(value: _isLocked, onChanged: _toggleLock), - ], - ), - ), - ], + title: Row( + children: [ + const Text('上架登记'), + const SizedBox(width: 12), + Expanded(child: _buildLockHeader()), + ], + ), + titleSpacing: 12, ), body: Column( children: [ @@ -701,6 +720,56 @@ class _RegistrationPageState extends State { ); } + Widget _buildLockHeader() { + final backgroundColor = + _isLocked ? Colors.orange.shade700 : Colors.blue.shade700; + final label = _isLocked ? '货位锁定' : '未锁定'; + + return Material( + borderRadius: BorderRadius.circular(8), + color: backgroundColor, + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () => _toggleLock(!_isLocked), + child: Container( + width: double.infinity, + height: 36, + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.swap_horiz, color: Colors.white, size: 18), + const SizedBox(width: 6), + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + label, + maxLines: 1, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w800, + ), + ), + ), + ), + const SizedBox(width: 8), + Text( + 'P2', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.88), + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ), + ); + } + Widget _buildSingleInputPanel() { final hasLocation = _locationCode != null; final hasZongpai = _zongpaiNos.isNotEmpty;