feat: add hardware key shortcuts and unify lock toggle design
- Home: add keyboard number shortcuts (1/2/3) for module navigation - Boxing: add P2 key mode switching with pill-style AppBar header - Registration: replace Switch with pill-style lock header matching boxing design - Add Android keyCode fallback for scanner device compatibility - Simplify home page cards to show only module name + shortcut hint - Add lock redesign HTML preview document Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -84,9 +84,12 @@ class BoxingPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _BoxingPageState extends State<BoxingPage> {
|
||||
static const _androidKeyCodeButtonR1 = 103;
|
||||
|
||||
final _scannerService = ScannerService();
|
||||
final _apiService = ApiService();
|
||||
final _feedbackService = FeedbackService();
|
||||
final _focusNode = FocusNode();
|
||||
StreamSubscription<ScanResult>? _scanSubscription;
|
||||
|
||||
// 模式
|
||||
@@ -155,12 +158,14 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
_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<BoxingPage> {
|
||||
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<BoxingPage> {
|
||||
_statusOverrideDot = null;
|
||||
});
|
||||
_focusQuantityInput();
|
||||
_requestFocus();
|
||||
}
|
||||
|
||||
void _applyAutoFill() {
|
||||
@@ -1253,163 +1288,229 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<HomePage> {
|
||||
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<HomePage> {
|
||||
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<HomePage> {
|
||||
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<void> _checkConnection() async {
|
||||
@@ -133,17 +205,22 @@ class _HomePageState extends State<HomePage> {
|
||||
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<HomePage> {
|
||||
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<HomePage> {
|
||||
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<HomePage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ class RegistrationPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RegistrationPageState extends State<RegistrationPage> {
|
||||
static const _androidKeyCodeButtonR1 = 103;
|
||||
|
||||
final _scannerService = ScannerService();
|
||||
final _apiService = ApiService();
|
||||
final _feedbackService = FeedbackService();
|
||||
@@ -82,11 +84,32 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
}
|
||||
|
||||
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<RegistrationPage> {
|
||||
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<RegistrationPage> {
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user