From 67e2c7c76c4efb68c82c5ff4e4122c855369ed68 Mon Sep 17 00:00:00 2001 From: Developer <91611@user.local> Date: Thu, 4 Jun 2026 15:10:03 +0800 Subject: [PATCH] =?UTF-8?q?refactor(settings):=20=E8=AF=AD=E8=A8=80/?= =?UTF-8?q?=E5=AF=86=E7=A0=81/U=E7=9B=98=E5=AF=BC=E5=85=A5=E6=94=B9?= =?UTF-8?q?=E7=94=A8=E5=8F=B3=E4=BE=A7=E5=86=85=E5=B5=8C=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 LanguagePanel / PasswordPanel / UsbImportPanel 三个 widget - settings_page 移除 AlertDialog 弹窗逻辑 - 5 个菜单项统一走 _buildContent() switch 切换右侧内容 - 验证:flutter analyze 无新增 issue --- .../settings/pages/settings_page.dart | 386 +++++------------- .../settings/widgets/language_panel.dart | 133 ++++++ .../settings/widgets/password_panel.dart | 195 +++++++++ .../settings/widgets/usb_import_panel.dart | 189 +++++++++ 4 files changed, 626 insertions(+), 277 deletions(-) create mode 100644 lib/features/settings/widgets/language_panel.dart create mode 100644 lib/features/settings/widgets/password_panel.dart create mode 100644 lib/features/settings/widgets/usb_import_panel.dart diff --git a/lib/features/settings/pages/settings_page.dart b/lib/features/settings/pages/settings_page.dart index e1e9c01..b258dde 100644 --- a/lib/features/settings/pages/settings_page.dart +++ b/lib/features/settings/pages/settings_page.dart @@ -1,11 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; import '../../../core/localization/app_localizations.dart'; -import '../../../core/localization/locale_provider.dart'; import '../../../core/theme/app_theme.dart'; import '../../../shared/widgets/common_button.dart'; -import '../services/settings_service.dart'; +import '../widgets/language_panel.dart'; +import '../widgets/password_panel.dart'; +import '../widgets/serial_config_panel.dart'; +import '../widgets/usb_import_panel.dart'; + +/// 设置页菜单 +enum _SettingsMenu { upgrade, language, password, usbImport, serialConfig } /// 系统设置页面 class SettingsPage extends ConsumerStatefulWidget { @@ -17,11 +21,11 @@ class SettingsPage extends ConsumerStatefulWidget { class _SettingsPageState extends ConsumerState { String _currentVersion = 'V1.0.0'; + _SettingsMenu _currentMenu = _SettingsMenu.upgrade; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); - // locale 用于语言切换,通过 ref.watch 保持监听 return Scaffold( body: Container( @@ -35,27 +39,6 @@ class _SettingsPageState extends ConsumerState { color: Colors.white, child: Column( children: [ - // 返回按钮 - Container( - height: 50, - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_back), - color: AppTheme.textPrimary, - onPressed: () => context.go('/'), - ), - Text( - '返回首页', - style: TextStyle( - color: AppTheme.textSecondary, - fontSize: 14, - ), - ), - ], - ), - ), // 设置标题 Container( height: 60, @@ -65,7 +48,8 @@ class _SettingsPageState extends ConsumerState { ), child: Row( children: [ - Icon(Icons.settings, color: AppTheme.primaryColor, size: 24), + Icon(Icons.settings, + color: AppTheme.primaryColor, size: 24), const SizedBox(width: 12), Text( l10n?.settings ?? '系统设置', @@ -82,25 +66,41 @@ class _SettingsPageState extends ConsumerState { _buildMenuItem( icon: Icons.system_update, title: l10n?.upgrade ?? '软件升级', - onTap: () {}, + selected: _currentMenu == _SettingsMenu.upgrade, + onTap: () => setState( + () => _currentMenu = _SettingsMenu.upgrade), + ), + // 串口配置 + _buildMenuItem( + icon: Icons.settings_input_hdmi, + title: '串口配置', + selected: _currentMenu == _SettingsMenu.serialConfig, + onTap: () => setState( + () => _currentMenu = _SettingsMenu.serialConfig), ), // 语言设置 _buildMenuItem( icon: Icons.language, title: l10n?.language ?? '语言设置', - onTap: () => _showLanguageDialog(), + selected: _currentMenu == _SettingsMenu.language, + onTap: () => setState( + () => _currentMenu = _SettingsMenu.language), ), - // 安全设置 + // 密码修改 _buildMenuItem( icon: Icons.lock, title: l10n?.password ?? '密码修改', - onTap: () => _showPasswordDialog(), + selected: _currentMenu == _SettingsMenu.password, + onTap: () => setState( + () => _currentMenu = _SettingsMenu.password), ), // U盘导入 _buildMenuItem( icon: Icons.usb, title: l10n?.usbImport ?? 'U盘导入', - onTap: () => _showUsbImportDialog(), + selected: _currentMenu == _SettingsMenu.usbImport, + onTap: () => setState( + () => _currentMenu = _SettingsMenu.usbImport), ), ], ), @@ -116,55 +116,7 @@ class _SettingsPageState extends ConsumerState { color: Colors.white, borderRadius: BorderRadius.circular(12), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n?.upgrade ?? '软件升级', - style: TextStyle( - color: AppTheme.textPrimary, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 24), - - // 版本信息 - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.backgroundColor, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - Icon(Icons.info_outline, color: AppTheme.primaryColor), - const SizedBox(width: 12), - Text( - '当前版本: $_currentVersion', - style: TextStyle(color: AppTheme.textPrimary), - ), - ], - ), - ), - const SizedBox(height: 24), - - // 检查更新按钮 - CommonButton( - text: '检查更新', - icon: Icons.refresh, - type: ButtonType.primary, - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('已是最新版本'), - backgroundColor: AppTheme.successColor, - ), - ); - }, - ), - ], - ), + child: _buildContent(), ), ), ], @@ -173,210 +125,90 @@ class _SettingsPageState extends ConsumerState { ); } + Widget _buildContent() { + return switch (_currentMenu) { + _SettingsMenu.serialConfig => const SerialConfigPanel(), + _SettingsMenu.language => const LanguagePanel(), + _SettingsMenu.password => const PasswordPanel(), + _SettingsMenu.usbImport => const UsbImportPanel(), + _SettingsMenu.upgrade => _buildUpgradeContent(), + }; + } + + Widget _buildUpgradeContent() { + return ListView( + padding: EdgeInsets.zero, + children: [ + Text( + '软件升级', + style: TextStyle( + color: AppTheme.textPrimary, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppTheme.backgroundColor, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon(Icons.info_outline, color: AppTheme.primaryColor), + const SizedBox(width: 12), + Text( + '当前版本: $_currentVersion', + style: TextStyle(color: AppTheme.textPrimary), + ), + ], + ), + ), + const SizedBox(height: 24), + CommonButton( + text: '检查更新', + icon: Icons.refresh, + type: ButtonType.primary, + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('已是最新版本'), + backgroundColor: AppTheme.successColor, + ), + ); + }, + ), + ], + ); + } + /// 导航菜单项 Widget _buildMenuItem({ required IconData icon, required String title, required VoidCallback onTap, + bool selected = false, }) { - return ListTile( - leading: Icon(icon, color: AppTheme.textSecondary), - title: Text(title, style: TextStyle(color: AppTheme.textPrimary)), - trailing: Icon(Icons.chevron_right, color: AppTheme.idleColor), - onTap: onTap, - ); - } - - /// 显示语言选择对话框 - void _showLanguageDialog() { - final locale = ref.read(localeProvider); - final currentLang = locale.languageCode; - - showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: Text('语言设置'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - RadioListTile( - title: Text('简体中文'), - value: 'zh', - groupValue: currentLang, - onChanged: (value) { - ref.read(localeProvider.notifier).setChinese(); - Navigator.of(ctx).pop(); - }, - ), - RadioListTile( - title: Text('English'), - value: 'en', - groupValue: currentLang, - onChanged: (value) { - ref.read(localeProvider.notifier).setEnglish(); - Navigator.of(ctx).pop(); - }, - ), - ], + return Container( + color: selected + ? AppTheme.primaryColor.withValues(alpha: 0.08) + : Colors.transparent, + child: ListTile( + leading: Icon( + icon, + color: selected ? AppTheme.primaryColor : AppTheme.textSecondary, ), + title: Text( + title, + style: TextStyle( + color: selected ? AppTheme.primaryColor : AppTheme.textPrimary, + fontWeight: selected ? FontWeight.w600 : FontWeight.normal, + ), + ), + trailing: Icon(Icons.chevron_right, color: AppTheme.idleColor), + onTap: onTap, ), ); } - - /// 显示密码修改对话框 - void _showPasswordDialog() { - final oldPasswordController = TextEditingController(); - final newPasswordController = TextEditingController(); - final confirmPasswordController = TextEditingController(); - String? errorMessage; - - showDialog( - context: context, - builder: (ctx) => StatefulBuilder( - builder: (context, setState) => AlertDialog( - title: Text('密码修改'), - content: SizedBox( - width: 300, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: oldPasswordController, - decoration: InputDecoration( - labelText: '原密码', - errorText: null, - ), - obscureText: true, - ), - const SizedBox(height: 12), - TextField( - controller: newPasswordController, - decoration: InputDecoration( - labelText: '新密码', - helperText: '至少6位字符', - ), - obscureText: true, - ), - const SizedBox(height: 12), - TextField( - controller: confirmPasswordController, - decoration: InputDecoration(labelText: '确认新密码'), - obscureText: true, - ), - if (errorMessage != null) - Padding( - padding: const EdgeInsets.only(top: 12), - child: Text( - errorMessage!, - style: TextStyle(color: AppTheme.errorColor, fontSize: 12), - ), - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(ctx).pop(), - child: Text('取消'), - ), - ElevatedButton( - onPressed: () async { - // 验证逻辑 - final oldPassword = oldPasswordController.text.trim(); - final newPassword = newPasswordController.text.trim(); - final confirmPassword = confirmPasswordController.text.trim(); - - // 检查空值 - if (oldPassword.isEmpty || newPassword.isEmpty || confirmPassword.isEmpty) { - setState(() => errorMessage = '请填写所有字段'); - return; - } - - // 检查新密码长度 - if (newPassword.length < 6) { - setState(() => errorMessage = '新密码至少6位字符'); - return; - } - - // 检查新密码一致性 - if (newPassword != confirmPassword) { - setState(() => errorMessage = '两次输入的新密码不一致'); - return; - } - - // 验证原密码 - final isValid = await SettingsService.instance.verifyPassword(oldPassword); - if (!isValid) { - setState(() => errorMessage = '原密码错误'); - return; - } - - // 保存新密码 - final success = await SettingsService.instance.setPassword(newPassword); - Navigator.of(ctx).pop(); - - if (success) { - ScaffoldMessenger.of(this.context).showSnackBar( - SnackBar( - content: Text('密码已修改'), - backgroundColor: AppTheme.successColor, - ), - ); - } else { - ScaffoldMessenger.of(this.context).showSnackBar( - SnackBar( - content: Text('密码修改失败'), - backgroundColor: AppTheme.errorColor, - ), - ); - } - }, - child: Text('确认'), - ), - ], - ), - ), - ); - } - - /// 显示U盘导入对话框 - void _showUsbImportDialog() { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('U盘导入'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.usb, size: 48, color: AppTheme.warningColor), - const SizedBox(height: 16), - Text('未检测到U盘'), - const SizedBox(height: 8), - Text( - '请插入U盘后重试', - style: TextStyle(color: AppTheme.textSecondary, fontSize: 12), - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text('关闭'), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('正在检测U盘...'), - backgroundColor: AppTheme.primaryColor, - ), - ); - }, - child: Text('重新检测'), - ), - ], - ), - ); - } -} \ No newline at end of file +} diff --git a/lib/features/settings/widgets/language_panel.dart b/lib/features/settings/widgets/language_panel.dart new file mode 100644 index 0000000..da4bbaa --- /dev/null +++ b/lib/features/settings/widgets/language_panel.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../core/localization/locale_provider.dart'; +import '../../../core/theme/app_theme.dart'; + +/// 语言设置面板 +/// +/// 在设置页右侧主区域渲染语言切换控件,直接调用 +/// [LocaleNotifier] 修改全局 locale。 +class LanguagePanel extends ConsumerWidget { + const LanguagePanel({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final locale = ref.watch(localeProvider); + final currentLang = locale.languageCode; + + return ListView( + padding: EdgeInsets.zero, + children: [ + _SectionCard( + title: '语言设置', + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _langTile( + context: context, + ref: ref, + value: 'zh', + groupValue: currentLang, + label: '简体中文', + ), + const Divider(height: 1), + _langTile( + context: context, + ref: ref, + value: 'en', + groupValue: currentLang, + label: 'English', + ), + ], + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Text( + '切换语言后立即生效', + style: TextStyle(color: AppTheme.textSecondary, fontSize: 12), + ), + ), + ], + ); + } + + Widget _langTile({ + required BuildContext context, + required WidgetRef ref, + required String value, + required String groupValue, + required String label, + }) { + final selected = value == groupValue; + return InkWell( + onTap: () { + if (value == 'zh') { + ref.read(localeProvider.notifier).setChinese(); + } else { + ref.read(localeProvider.notifier).setEnglish(); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + child: Row( + children: [ + Icon( + selected ? Icons.radio_button_checked : Icons.radio_button_off, + color: selected ? AppTheme.primaryColor : AppTheme.idleColor, + size: 20, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + label, + style: TextStyle( + color: AppTheme.textPrimary, + fontWeight: selected ? FontWeight.w600 : FontWeight.normal, + ), + ), + ), + if (selected) + Icon(Icons.check_circle, color: AppTheme.successColor, size: 18), + ], + ), + ), + ); + } +} + +class _SectionCard extends StatelessWidget { + final String title; + final Widget child; + + const _SectionCard({required this.title, required this.child}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: AppTheme.borderSubtle), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + title, + style: TextStyle( + color: AppTheme.textPrimary, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const Divider(), + child, + ], + ), + ); + } +} diff --git a/lib/features/settings/widgets/password_panel.dart b/lib/features/settings/widgets/password_panel.dart new file mode 100644 index 0000000..2fa78c1 --- /dev/null +++ b/lib/features/settings/widgets/password_panel.dart @@ -0,0 +1,195 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../core/theme/app_theme.dart'; +import '../../../shared/services/toast_service.dart'; +import '../services/settings_service.dart'; + +/// 密码修改面板 +/// +/// 在设置页右侧主区域渲染密码修改表单,行为与原弹窗一致: +/// 校验空值、长度(≥6)、两次一致性、原密码正确性。 +class PasswordPanel extends ConsumerStatefulWidget { + const PasswordPanel({super.key}); + + @override + ConsumerState createState() => _PasswordPanelState(); +} + +class _PasswordPanelState extends ConsumerState { + final _oldCtrl = TextEditingController(); + final _newCtrl = TextEditingController(); + final _confirmCtrl = TextEditingController(); + bool _submitting = false; + String? _error; + + @override + void dispose() { + _oldCtrl.dispose(); + _newCtrl.dispose(); + _confirmCtrl.dispose(); + super.dispose(); + } + + Future _submit() async { + if (_submitting) return; + final oldPwd = _oldCtrl.text.trim(); + final newPwd = _newCtrl.text.trim(); + final confirmPwd = _confirmCtrl.text.trim(); + + if (oldPwd.isEmpty || newPwd.isEmpty || confirmPwd.isEmpty) { + setState(() => _error = '请填写所有字段'); + return; + } + if (newPwd.length < 6) { + setState(() => _error = '新密码至少6位字符'); + return; + } + if (newPwd != confirmPwd) { + setState(() => _error = '两次输入的新密码不一致'); + return; + } + + setState(() { + _submitting = true; + _error = null; + }); + + final isValid = await SettingsService.instance.verifyPassword(oldPwd); + if (!isValid) { + if (!mounted) return; + setState(() { + _submitting = false; + _error = '原密码错误'; + }); + return; + } + + final ok = await SettingsService.instance.setPassword(newPwd); + if (!mounted) return; + setState(() => _submitting = false); + + if (ok) { + _oldCtrl.clear(); + _newCtrl.clear(); + _confirmCtrl.clear(); + ToastService.showSuccess(context, '密码已修改'); + } else { + ToastService.showError(context, '密码修改失败'); + } + } + + @override + Widget build(BuildContext context) { + return ListView( + padding: EdgeInsets.zero, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: AppTheme.borderSubtle), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + '密码修改', + style: TextStyle( + color: AppTheme.textPrimary, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const Divider(), + _field( + controller: _oldCtrl, + label: '原密码', + hint: '请输入当前密码', + ), + const SizedBox(height: 16), + _field( + controller: _newCtrl, + label: '新密码', + hint: '至少6位字符', + ), + const SizedBox(height: 16), + _field( + controller: _confirmCtrl, + label: '确认新密码', + hint: '再次输入新密码', + ), + if (_error != null) ...[ + const SizedBox(height: 12), + Text( + _error!, + style: TextStyle(color: AppTheme.errorColor, fontSize: 12), + ), + ], + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton( + onPressed: _submitting + ? null + : () { + _oldCtrl.clear(); + _newCtrl.clear(); + _confirmCtrl.clear(); + setState(() => _error = null); + }, + child: const Text('重置'), + ), + const SizedBox(width: 12), + ElevatedButton.icon( + onPressed: _submitting ? null : _submit, + icon: _submitting + ? const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.check, size: 18), + label: const Text('确认'), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Text( + '默认密码为 123456', + style: TextStyle(color: AppTheme.textSecondary, fontSize: 12), + ), + ), + ], + ); + } + + Widget _field({ + required TextEditingController controller, + required String label, + required String hint, + }) { + return TextField( + controller: controller, + obscureText: true, + decoration: InputDecoration( + labelText: label, + hintText: hint, + border: const OutlineInputBorder(), + isDense: true, + contentPadding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + ), + ); + } +} diff --git a/lib/features/settings/widgets/usb_import_panel.dart b/lib/features/settings/widgets/usb_import_panel.dart new file mode 100644 index 0000000..e44a39e --- /dev/null +++ b/lib/features/settings/widgets/usb_import_panel.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../core/theme/app_theme.dart'; +import '../../../shared/services/toast_service.dart'; +import '../services/usb_detection_service.dart'; + +/// U盘导入面板 +/// +/// 在设置页右侧主区域渲染 U盘检测与导入操作。 +class UsbImportPanel extends ConsumerStatefulWidget { + const UsbImportPanel({super.key}); + + @override + ConsumerState createState() => _UsbImportPanelState(); +} + +class _UsbImportPanelState extends ConsumerState { + bool _detecting = false; + + Future _detect() async { + if (_detecting) return; + setState(() => _detecting = true); + final connected = await UsbDetectionService.instance.detectUsb(); + if (!mounted) return; + setState(() => _detecting = false); + if (connected) { + ToastService.showInfo(context, '正在检测U盘...'); + } else { + ToastService.showWarning(context, '未检测到U盘'); + } + } + + @override + Widget build(BuildContext context) { + final state = ref.watch(_usbStateProvider); + final connected = state.maybeWhen( + data: (s) => s.isConnected, + orElse: () => UsbDetectionService.instance.isConnected, + ); + final path = state.maybeWhen( + data: (s) => s.path, + orElse: () => UsbDetectionService.instance.usbPath, + ); + + return ListView( + padding: EdgeInsets.zero, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: AppTheme.borderSubtle), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Icon( + connected ? Icons.usb : Icons.usb_off, + color: + connected ? AppTheme.primaryColor : AppTheme.warningColor, + size: 28, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + connected ? 'U盘已连接' : '未检测到U盘', + style: TextStyle( + color: AppTheme.textPrimary, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + connected + ? '挂载路径: ${path ?? "未知"}' + : '请插入U盘后点击"重新检测"', + style: TextStyle( + color: AppTheme.textSecondary, fontSize: 13), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton.icon( + onPressed: _detecting ? null : _detect, + icon: _detecting + ? const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.refresh, size: 18), + label: const Text('重新检测'), + ), + const SizedBox(width: 12), + ElevatedButton.icon( + onPressed: connected ? () => _import(path) : null, + icon: const Icon(Icons.download, size: 18), + label: const Text('导入程序'), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: AppTheme.borderSubtle), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '使用说明', + style: TextStyle( + color: AppTheme.textPrimary, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + _bullet('将程序文件 (.json) 放入 U盘根目录的 programs 文件夹'), + _bullet('插入 U盘后点击"重新检测"'), + _bullet('检测成功后点击"导入程序"加载程序列表'), + ], + ), + ), + ], + ); + } + + void _import(String? path) { + if (path == null) { + ToastService.showWarning(context, 'U盘路径无效'); + return; + } + ToastService.showInfo(context, '正在导入程序...'); + // TODO: 接入 program_import_service 实现真正的导入流程 + } + + Widget _bullet(String text) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 6, right: 8), + child: Container( + width: 4, + height: 4, + decoration: BoxDecoration( + color: AppTheme.primaryColor, + shape: BoxShape.circle, + ), + ), + ), + Expanded( + child: Text( + text, + style: TextStyle( + color: AppTheme.textSecondary, + fontSize: 13, + height: 1.5, + ), + ), + ), + ], + ), + ); + } +} + +/// 监听 USB 状态变化的 Riverpod StreamProvider +final _usbStateProvider = StreamProvider((ref) { + return UsbDetectionService.instance.stateStream; +});