import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../providers/auth_provider.dart'; import '../../providers/locale_provider.dart'; import '../../providers/theme_provider.dart'; import '../../providers/display_provider.dart'; import '../../services/api_client.dart'; import '../../services/sys_service.dart'; import '../../utils/constants.dart'; import '../../utils/i10n.dart'; import '../../utils/app_update.dart'; import 'package:package_info_plus/package_info_plus.dart'; /// 设置页(底部导航第 3 个 tab):主题切换 / 语言占位 / 关于 / 退出登录。 class SettingsPage extends ConsumerWidget { const SettingsPage({super.key}); Future _showChangePasswordDialog(BuildContext context, WidgetRef ref) async { final t = AppLocalizations.of(context); final oldCtrl = TextEditingController(); final newCtrl = TextEditingController(); final confirmCtrl = TextEditingController(); bool saving = false; await showDialog( context: context, builder: (ctx) => StatefulBuilder( builder: (ctx, setState) => AlertDialog( title: Text(t.passwordDialogTitle), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: oldCtrl, obscureText: true, decoration: InputDecoration( labelText: t.oldPassword, border: const OutlineInputBorder(), ), ), const SizedBox(height: 12), TextField( controller: newCtrl, obscureText: true, decoration: InputDecoration( labelText: t.newPassword, border: const OutlineInputBorder(), ), ), const SizedBox(height: 12), TextField( controller: confirmCtrl, obscureText: true, decoration: InputDecoration( labelText: t.confirmNewPassword, border: const OutlineInputBorder(), ), ), ], ), ), actions: [ TextButton( onPressed: saving ? null : () => Navigator.pop(ctx), child: Text(t.cancel), ), FilledButton( onPressed: saving ? null : () async { final oldPwd = oldCtrl.text.trim(); final newPwd = newCtrl.text.trim(); final confirmPwd = confirmCtrl.text.trim(); if (oldPwd.isEmpty || newPwd.isEmpty) { ScaffoldMessenger.of(ctx).showSnackBar( SnackBar(content: Text(t.pleaseFillPassword)), ); return; } if (newPwd != confirmPwd) { ScaffoldMessenger.of(ctx).showSnackBar( SnackBar(content: Text(t.passwordsDoNotMatch)), ); return; } setState(() => saving = true); try { await SystemService().updatePassword(oldPwd, newPwd); if (!ctx.mounted) return; Navigator.pop(ctx); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(t.passwordChangeSuccess)), ); // 修改成功后安全登出,要求用新密码重新登录。 Future.delayed(const Duration(seconds: 1), () { if (context.mounted) { ref.read(authProvider.notifier).logout(); } }); } on ApiException catch (e) { setState(() => saving = false); ScaffoldMessenger.of(ctx).showSnackBar( SnackBar(content: Text(e.message)), ); } catch (e) { setState(() => saving = false); ScaffoldMessenger.of(ctx).showSnackBar( SnackBar(content: Text(t.passwordChangeFailed(formatApiError(e)))), ); } }, child: saving ? const SizedBox( height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : Text(t.save), ), ], ), ), ); oldCtrl.dispose(); newCtrl.dispose(); confirmCtrl.dispose(); } String _themeModeLabel(ThemeMode mode, AppLocalizations t) { switch (mode) { case ThemeMode.dark: return t.themeDark; case ThemeMode.light: return t.themeLight; case ThemeMode.system: return t.themeSystem; } } String _orientationLabel(DisplayOrientation mode, AppLocalizations t) { switch (mode) { case DisplayOrientation.system: return t.orientationSystem; case DisplayOrientation.portrait: return t.orientationPortrait; case DisplayOrientation.landscape: return t.orientationLandscape; } } @override Widget build(BuildContext context, WidgetRef ref) { final themeMode = ref.watch(themeModeProvider); final appLocale = ref.watch(localeProvider); final displayOrientation = ref.watch(displayOrientationProvider); final authState = ref.watch(authProvider); final t = AppLocalizations.of(context); return Scaffold( appBar: AppBar(title: Text(t.settings)), body: ListView( children: [ if (authState.user != null) ListTile( leading: const CircleAvatar(child: Icon(Icons.person)), title: Text(authState.user!.displayName), subtitle: Text( authState.user!.role.isEmpty ? '${t.account}:${authState.user!.account}' : authState.user!.role, ), ), const Divider(), // 主题切换:深色 / 浅色 / 跟随系统 _AppSettingsTile( leading: const Icon(Icons.dark_mode_outlined), title: t.themeMode, subtitle: _themeModeLabel(themeMode, t), control: SegmentedButton( segments: [ ButtonSegment( value: ThemeMode.dark, icon: const Icon(Icons.dark_mode), label: Text(t.themeDark), ), ButtonSegment( value: ThemeMode.light, icon: const Icon(Icons.light_mode), label: Text(t.themeLight), ), ButtonSegment( value: ThemeMode.system, icon: const Icon(Icons.settings_suggest), label: Text(t.themeSystem), ), ], selected: {themeMode}, onSelectionChanged: (selection) => ref .read(themeModeProvider.notifier) .setThemeMode(selection.first), ), ), // 语言切换:简体中文 / English _AppSettingsTile( leading: const Icon(Icons.language_outlined), title: t.language, subtitle: appLocale.label, control: SegmentedButton( segments: const [ ButtonSegment(value: AppLocale.zh, label: Text('中文')), ButtonSegment(value: AppLocale.en, label: Text('EN')), ], selected: {appLocale}, onSelectionChanged: (selection) { try { ref.read(localeProvider.notifier).setLocale(selection.first); } catch (e) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('语言切换失败:$e')), ); } }, ), ), const Divider(), // 显示方向:跟随系统 / 竖屏 / 横屏(手动切换屏幕方向,布局自动适配)。 _AppSettingsTile( leading: const Icon(Icons.screen_rotation_outlined), title: t.orientationMode, subtitle: _orientationLabel(displayOrientation, t), control: SegmentedButton( segments: [ ButtonSegment( value: DisplayOrientation.system, icon: const Icon(Icons.screen_rotation), label: Text(t.orientationSystem), ), ButtonSegment( value: DisplayOrientation.portrait, icon: const Icon(Icons.stay_current_portrait), label: Text(t.orientationPortrait), ), ButtonSegment( value: DisplayOrientation.landscape, icon: const Icon(Icons.stay_current_landscape), label: Text(t.orientationLandscape), ), ], selected: {displayOrientation}, onSelectionChanged: (selection) => ref .read(displayOrientationProvider.notifier) .set(selection.first), ), ), const Divider(), // 修改密码 ListTile( leading: const Icon(Icons.lock_outline), title: Text(t.changePassword), onTap: () => _showChangePasswordDialog(context, ref), ), const Divider(), // 检查更新 ListTile( leading: const Icon(Icons.system_update_alt_outlined), title: Text(t.checkUpdate), subtitle: _VersionSubtitle(t: t), onTap: () => checkAndPromptUpdate( context, silentIfNoUpdate: false, orgId: ref.read(authProvider).user?.orgId, ), ), const Divider(), // 关于 ListTile( leading: const Icon(Icons.info_outline), title: Text(t.about), subtitle: _VersionSubtitle(t: t, includeName: true), onTap: () async { final pkg = await currentPackageInfo; if (!context.mounted) return; showAboutDialog( context: context, applicationName: AppConstants.appName, applicationVersion: '${pkg.version} (${pkg.buildNumber})', applicationIcon: Image.asset( 'assets/images/logo.png', width: 64, height: 64, ), applicationLegalese: 'Device/system operation admin mobile demo', ); }, ), const Divider(), // 退出登录 ListTile( leading: Icon( Icons.logout, color: Theme.of(context).colorScheme.error, ), title: Text( t.logout, style: TextStyle(color: Theme.of(context).colorScheme.error), ), onTap: () async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(t.logout), content: const Text('确定要退出当前账号吗?'), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: Text(t.cancel), ), FilledButton( style: FilledButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.error, foregroundColor: Theme.of(context).colorScheme.onError, ), onPressed: () => Navigator.pop(ctx, true), child: Text(t.logout), ), ], ), ); if (confirmed == true && context.mounted) { ref.read(authProvider.notifier).logout(); } }, ), ], ), ); } } /// 设置项:图标 + 标题 + 副标题在上,控件整行铺满在下。 /// /// 用于 [SegmentedButton] 等较宽的控件,避免塞进 [ListTile.trailing] /// 在窄屏溢出。 class _AppSettingsTile extends StatelessWidget { const _AppSettingsTile({ required this.title, required this.control, this.subtitle, this.leading, }); final String title; final Widget control; final String? subtitle; final Widget? leading; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ if (leading != null) ...[ leading!, const SizedBox(width: 12), ], Expanded( child: Text( title, style: theme.textTheme.titleMedium ?.copyWith(fontWeight: FontWeight.w600), ), ), ], ), if (subtitle != null) ...[ const SizedBox(height: 2), Padding( padding: EdgeInsets.only(left: leading != null ? 32 : 0), child: Text( subtitle!, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ), ], const SizedBox(height: 12), SizedBox(width: double.infinity, child: control), ], ), ); } } /// 当前 App 版本副标题:异步读取 [PackageInfo] 展示版本名与构建号。 /// /// [includeName]=true 时前缀应用名(用于「关于」项)。 class _VersionSubtitle extends StatelessWidget { const _VersionSubtitle({required this.t, this.includeName = false}); final AppLocalizations t; final bool includeName; @override Widget build(BuildContext context) { return FutureBuilder( future: currentPackageInfo, builder: (ctx, snap) { final info = snap.data; if (includeName) { // 「关于」项:副标题只展示应用名,版本详情交由「关于」弹窗承载, // 避免与应用名 + 版本号 + 构建号叠加成冗余且暴露内部构建号的文案。 return const Text(AppConstants.appName); } // 「检查更新」项:展示当前已安装版本名(不暴露内部构建号)。 final version = info != null ? info.version : t.loading; return Text('${t.currentVersion} $version'); }, ); } }