import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_remix/flutter_remix.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:url_launcher/url_launcher_string.dart'; import '../../../common/config/app_h5_urls.dart'; import '../../../common/router/app_router.dart'; import '../../../common/theme/app_text.dart'; import '../../../common/theme/spacing.dart'; import '../../../common/theme/theme_controller.dart'; import '../../../common/widgets/adaptive.dart'; import '../../../common/widgets/app_toast.dart'; import '../../../common/widgets/circle_back_button.dart'; import '../../../common/widgets/prototype_ui.dart'; import '../../auth/application/auth_controller.dart'; import '../../auth/data/auth_models.dart'; import '../../profile/application/profile_settings_controller.dart'; import '../../profile/data/profile_models.dart'; import '../../profile/presentation/profile_page.dart'; class SettingsPage extends HookConsumerWidget { const SettingsPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final cs = ShadTheme.of(context).colorScheme; final themeMode = ref.watch(themeModeControllerProvider); final profileSettings = ref.watch(profileSettingsControllerProvider); final authAsync = ref.watch(authProvider); final loggedIn = authAsync.valueOrNull is LoggedInAuthState; final contactDebugTapCount = useState(0); final contactLastTapAt = useRef(null); return Scaffold( backgroundColor: cs.background, appBar: AppBar( backgroundColor: cs.background, surfaceTintColor: Colors.transparent, elevation: 0, scrolledUnderElevation: 0, centerTitle: true, title: Text( '设置', style: TextStyle( fontSize: 17, fontWeight: FontWeight.w700, color: cs.foreground, ), ), leading: const CircleBackButton(), ), body: ListView( padding: Adaptive.screenPadding(context, top: 8, bottom: 32), children: [ const _Section('外观'), _ThemeSegmentCard( themeMode: themeMode, onChanged: (mode) => ref.read(themeModeControllerProvider.notifier).set(mode), ), const SizedBox(height: 22), const _Section('资产参数'), PrototypeCard( padding: EdgeInsets.zero, child: Column( children: [ SettingRow( icon: Icons.palette_outlined, title: '涨跌颜色', trailing: profileSettings.priceColorMode.label, onTap: () { final nextMode = profileSettings.priceColorMode == PriceColorMode.redUpGreenDown ? PriceColorMode.greenUpRedDown : PriceColorMode.redUpGreenDown; ref .read(profileSettingsControllerProvider.notifier) .setPriceColorMode(nextMode); toastInfo(msg: '已切换为「${nextMode.label}」'); }, ), const PrototypeDivider(margin: EdgeInsets.zero), SettingRow( icon: Icons.tune_outlined, title: '纯度系数管理', trailing: '${profileSettings.purityPresets.length} 项', onTap: () => context.push(AppRoutes.profilePurity), ), const PrototypeDivider(margin: EdgeInsets.zero), SettingRow( icon: Icons.percent_outlined, title: '回收折扣设置', trailing: '金 ${(profileSettings.recycleDiscounts.values.first * 100).toStringAsFixed(0)}%', onTap: () => context.push(AppRoutes.profileRecycle), ), ], ), ), // const SizedBox(height: 22), // const _Section('备份'), // _Card( // children: [ // SettingRow( // icon: FlutterRemix.download_2_line, // label: '本地导出备份', // trailing: '导出', // onTap: () => _showLocalBackupSheet(context), // ), // const PrototypeDivider(margin: EdgeInsets.zero), // SettingRow( // icon: FlutterRemix.cloud_line, // label: '云备份', // trailing: '占位', // onTap: () => _showCloudBackupSheet(context), // ), // ], // ), const SizedBox(height: 22), const _Section('工具'), PrototypeCard( padding: EdgeInsets.zero, child: Column( children: [ SettingRow( icon: FlutterRemix.layout_grid_line, title: '桌面小组件', trailing: '占位', onTap: () => _showWidgetSheet(context), ), if (kDebugMode) ...[ const PrototypeDivider(margin: EdgeInsets.zero), SettingRow( icon: Icons.bug_report_outlined, title: '调试工具', onTap: () => context.push(AppRoutes.debug), ), ], ], ), ), const SizedBox(height: 22), const _Section('法律'), PrototypeCard( padding: EdgeInsets.zero, child: Column( children: [ SettingRow( icon: FlutterRemix.file_list_3_line, title: '用户协议', onTap: () => launchUrlString( AppH5UrlsHelper.withCacheBuster(AppH5Urls.userProtocolUrl), mode: LaunchMode.inAppBrowserView, ), ), const PrototypeDivider(margin: EdgeInsets.zero), SettingRow( icon: FlutterRemix.shield_check_line, title: '隐私政策', onTap: () => launchUrlString( AppH5UrlsHelper.withCacheBuster(AppH5Urls.privacyUrl), mode: LaunchMode.inAppBrowserView, ), ), const PrototypeDivider(margin: EdgeInsets.zero), SettingRow( icon: FlutterRemix.alert_line, title: '免责声明', onTap: () => context.push(AppRoutes.profileDisclaimer), ), ], ), ), if (loggedIn) ...[ const SizedBox(height: 22), const _Section('账户'), PrototypeCard( padding: EdgeInsets.zero, child: Column( children: [ SettingRow( icon: FlutterRemix.logout_box_r_line, title: '退出登录', onTap: () => _confirmLogout(context, ref), ), const PrototypeDivider(margin: EdgeInsets.zero), SettingRow( icon: FlutterRemix.delete_bin_line, title: '注销账号', destructive: true, onTap: () => context.push(AppRoutes.deleteAccount), ), ], ), ), ], const SizedBox(height: 22), const _Section('关于'), PrototypeCard( padding: EdgeInsets.zero, child: Column( children: [ const _AboutRow(), const PrototypeDivider(margin: EdgeInsets.zero), SettingRow( icon: FlutterRemix.mail_line, title: '联系我们', trailing: 'business@hnsiderui.cn', onTap: () { final now = DateTime.now(); final lastTapAt = contactLastTapAt.value; if (lastTapAt == null || now.difference(lastTapAt) > const Duration(milliseconds: 500)) { contactDebugTapCount.value = 0; } contactLastTapAt.value = now; contactDebugTapCount.value += 1; if (contactDebugTapCount.value >= 10) { contactDebugTapCount.value = 0; contactLastTapAt.value = null; context.push(AppRoutes.debug); } }, ), ], ), ), const SizedBox(height: 14), _RiskFooter(cs: cs), ], ), ); } Future _confirmLogout(BuildContext context, WidgetRef ref) async { final cs = ShadTheme.of(context).colorScheme; final ok = await showModalBottomSheet( context: context, backgroundColor: cs.card, barrierColor: Colors.black.withValues(alpha: 0.5), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(Spacing.radiusCard), ), ), builder: (ctx) => SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( width: 38, height: 4, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: cs.border, borderRadius: BorderRadius.circular(9999), ), ), ), Text( '退出登录', style: TextStyle( fontSize: 19, fontWeight: FontWeight.w700, color: cs.foreground, ), ), const SizedBox(height: 8), Text( '确定要退出登录吗?', style: TextStyle(fontSize: 13, color: cs.mutedForeground), ), const SizedBox(height: 22), _LogoutSheetButton( label: '退出', primary: true, onTap: () => Navigator.of(ctx).pop(true), ), const SizedBox(height: 12), _LogoutSheetButton( label: '取消', primary: false, onTap: () => Navigator.of(ctx).pop(false), ), ], ), ), ), ); if (ok == true && context.mounted) { await ref.read(authProvider.notifier).logout(); } } Future _showWidgetSheet(BuildContext context) async { final cs = ShadTheme.of(context).colorScheme; await showModalBottomSheet( context: context, backgroundColor: cs.card, barrierColor: Colors.black.withValues(alpha: 0.5), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(Spacing.radiusCard), ), ), builder: (ctx) => SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( width: 38, height: 4, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: cs.border, borderRadius: BorderRadius.circular(9999), ), ), ), Text( '桌面小组件', style: TextStyle( fontSize: 19, fontWeight: FontWeight.w700, color: cs.foreground, ), ), const SizedBox(height: 8), Text( '桌面行情小组件正在整理中,先保留入口。', style: TextStyle(fontSize: 13, color: cs.mutedForeground), ), const SizedBox(height: 18), _SheetPrimaryButton( label: '知道了', onTap: () => Navigator.of(ctx).pop(), ), ], ), ), ), ); } } class _Section extends StatelessWidget { const _Section(this.title); final String title; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return Padding( padding: const EdgeInsets.only(bottom: 10), child: Text( title, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: cs.foreground, ), ), ); } } class _ThemeSegmentCard extends StatelessWidget { const _ThemeSegmentCard({required this.themeMode, required this.onChanged}); final ThemeMode themeMode; final ValueChanged onChanged; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; final options = <(ThemeMode, String, IconData)>[ (ThemeMode.system, '系统主题', Icons.brightness_auto_outlined), (ThemeMode.light, '浅色', Icons.light_mode_outlined), (ThemeMode.dark, '深色', Icons.dark_mode_outlined), ]; return Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: cs.secondary, border: Border.all(color: cs.border), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ for (final opt in options) Expanded( child: Padding( padding: EdgeInsets.only( left: opt == options.first ? 0 : 2, right: opt == options.last ? 0 : 2, ), child: _ThemeSegmentButton( label: opt.$2, icon: opt.$3, selected: themeMode == opt.$1, onTap: () => onChanged(opt.$1), ), ), ), ], ), ); } } class _ThemeSegmentButton extends StatelessWidget { const _ThemeSegmentButton({ required this.label, required this.icon, required this.selected, required this.onTap, }); final String label; final IconData icon; final bool selected; final VoidCallback onTap; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; final fg = selected ? cs.foreground : cs.mutedForeground; return Material( color: selected ? cs.background : Colors.transparent, borderRadius: BorderRadius.circular(10), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(10), child: Container( height: 44, alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), boxShadow: selected ? [ const BoxShadow( color: Color(0x0A000000), blurRadius: 10, offset: Offset(0, 2), ), ] : null, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 17, color: fg), const SizedBox(width: 8), Text( label, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: fg, ), ), ], ), ), ), ); } } // class SettingRow extends StatelessWidget { // const SettingRow({ // required this.icon, // required this.label, // this.trailing, // this.onTap, // this.destructive = false, // }); // // final IconData icon; // final String label; // final String? trailing; // final VoidCallback? onTap; // final bool destructive; // // @override // Widget build(BuildContext context) { // final cs = ShadTheme.of(context).colorScheme; // final color = destructive ? cs.destructive : cs.foreground; // return InkWell( // onTap: onTap, // child: Padding( // padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), // child: Row( // children: [ // Icon(icon, size: 20, color: const Color(0xFF8A8780)), // const SizedBox(width: 12), // Expanded( // child: Text( // label, // style: TextStyle( // fontSize: 14, // fontWeight: FontWeight.w600, // color: color, // ), // ), // ), // if (trailing != null) // Text( // trailing!, // style: TextStyle(fontSize: 12, color: cs.mutedForeground), // ), // const SizedBox(width: 6), // Icon(Icons.chevron_right, size: 16, color: cs.mutedForeground), // ], // ), // ), // ); // } // } class _AboutRow extends StatelessWidget { const _AboutRow(); @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return FutureBuilder( future: PackageInfo.fromPlatform(), builder: (context, snap) { final v = snap.hasData ? 'v${snap.data!.version}+${snap.data!.buildNumber}' : ''; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15), child: Row( children: [ SizedBox( width: 24, child: Icon( FlutterRemix.information_line, size: 20, color: cs.foreground, ), ), const SizedBox(width: 13), Expanded( child: Text( '金值', style: TextStyle(fontSize: 15, color: cs.foreground), ), ), Text( v, style: AppText.meta.copyWith( fontSize: 13, color: cs.mutedForeground, ), ), ], ), ); }, ); } } class _RiskFooter extends StatelessWidget { const _RiskFooter({required this.cs}); final ShadColorScheme cs; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), decoration: BoxDecoration( color: cs.card, border: Border.all(color: cs.border), borderRadius: BorderRadius.circular(Spacing.radiusCard), ), child: Text( '风险提示 · 数据来源 · 免责\n价格、资讯与估值均为本地 mock 数据,仅用于产品原型验证,不构成投资建议。', style: AppText.meta.copyWith( fontSize: 12, color: cs.mutedForeground, height: 1.65, ), ), ); } } class _SheetPrimaryButton extends StatelessWidget { const _SheetPrimaryButton({required this.label, required this.onTap}); final String label; final VoidCallback onTap; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, child: Container( width: double.infinity, height: 52, alignment: Alignment.center, decoration: BoxDecoration( color: cs.primary, borderRadius: BorderRadius.circular(12), ), child: Text( label, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: cs.primaryForeground, ), ), ), ); } } class _LogoutSheetButton extends StatelessWidget { const _LogoutSheetButton({ required this.label, required this.primary, required this.onTap, }); final String label; final bool primary; final VoidCallback onTap; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return GestureDetector( onTap: onTap, behavior: HitTestBehavior.opaque, child: Container( width: double.infinity, height: 52, alignment: Alignment.center, decoration: BoxDecoration( color: primary ? cs.primary : cs.secondary, borderRadius: BorderRadius.circular(12), border: primary ? null : Border.all(color: cs.border), ), child: Text( label, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: primary ? cs.primaryForeground : cs.foreground, ), ), ), ); } }