Files
pad_scanner/lib/pages/home_page.dart
Misaka_Company 073162dc65 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>
2026-05-18 11:26:02 +08:00

371 lines
10 KiB
Dart

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';
import 'package:pad_scanner/pages/boxing_page.dart';
import 'package:pad_scanner/services/api_service.dart';
/// Module type enum for each available feature card
enum ModuleType { registration, boxing, shelfQuery }
/// Module status for PRD state tracking
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 ModuleStatus status;
final String route;
const FeatureCard({
required this.type,
required this.shortcut,
required this.icon,
required this.title,
required this.status,
required this.route,
});
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
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 = '';
/// Module registry from PRD §3.4
static const _modules = [
FeatureCard(
type: ModuleType.registration,
shortcut: '1',
icon: Icons.local_shipping_outlined,
title: '上架登记',
status: ModuleStatus.online,
route: '/registration',
),
FeatureCard(
type: ModuleType.boxing,
shortcut: '2',
icon: Icons.view_list_outlined,
title: '装箱编号',
status: ModuleStatus.online,
route: '/boxing',
),
FeatureCard(
type: ModuleType.shelfQuery,
shortcut: '3',
icon: Icons.search_outlined,
title: '货架查询',
status: ModuleStatus.planning,
route: '',
),
];
@override
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 {
final configService = AppConfigService();
final url = await configService.getString('api_url') ?? '';
if (url.isEmpty) {
setState(() {
_isConnected = false;
_apiAddress = '';
_isCheckingConnection = false;
});
return;
}
setState(() => _isCheckingConnection = true);
final ok = await _apiService.testConnection(url);
if (!mounted) return;
setState(() {
_isConnected = ok;
_apiAddress = url;
_isCheckingConnection = false;
});
}
void _onCardTap(FeatureCard card) {
switch (card.status) {
case ModuleStatus.online:
_navigateToModule(card.type);
case ModuleStatus.developing:
_showToast('该功能正在开发中');
case ModuleStatus.planning:
_showToast('该功能规划中');
}
}
void _showToast(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), duration: const Duration(seconds: 2)),
);
}
void _navigateToModule(ModuleType type) {
late Widget targetPage;
switch (type) {
case ModuleType.registration:
targetPage = const RegistrationPage();
break;
case ModuleType.boxing:
targetPage = const BoxingPage();
break;
case ModuleType.shelfQuery:
return; // 规划中,不导航
}
Navigator.push(context, MaterialPageRoute(builder: (_) => targetPage)).then(
(_) {
_checkConnection();
_requestFocus();
},
);
}
void _navigateToSettings() {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const SettingsPage()),
).then((_) {
_checkConnection();
_requestFocus();
});
}
Color _getConnectionStatusColor() {
if (_isCheckingConnection) return Colors.orange;
return _isConnected ? Colors.green : Colors.red;
}
String _getConnectionStatusText() {
if (_isCheckingConnection) return '● 正在检测连接...';
if (!_isConnected || _apiAddress.isEmpty) return '● 未连接,请检查设置';
return '● 已连接 $_apiAddress';
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
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.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,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
_getConnectionStatusText(),
style: const TextStyle(fontSize: 13),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
),
),
);
}
Widget _buildFeatureCard(BuildContext context, FeatureCard card) {
final canNavigate = card.status == ModuleStatus.online;
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: InkWell(
onTap: () => _onCardTap(card),
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: canNavigate ? Colors.white : Colors.grey.shade200,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Icon(
card.icon,
size: 22,
color: canNavigate ? Colors.blue : Colors.grey,
),
const SizedBox(width: 8),
Text(
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,
),
),
);
}
}