import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import '../../core/app_colors.dart'; import '../../core/app_design_tokens.dart'; import '../../core/app_module_visuals.dart'; import '../../core/app_theme.dart'; import '../../core/navigation_provider.dart'; import '../../providers/auth_provider.dart'; import '../../providers/data_providers.dart'; import '../../widgets/app_toast.dart'; import '../../widgets/authenticated_network_image.dart'; class ProfilePage extends ConsumerStatefulWidget { const ProfilePage({super.key}); @override ConsumerState createState() => _ProfilePageState(); } class _ProfilePageState extends ConsumerState { bool _uploadingAvatar = false; Future _changeAvatar() async { final source = await showModalBottomSheet( context: context, builder: (sheetContext) => SafeArea( child: Wrap( children: [ ListTile( leading: const Icon(Icons.photo_library_outlined), title: const Text('从相册选择'), onTap: () => Navigator.pop(sheetContext, ImageSource.gallery), ), ListTile( leading: const Icon(Icons.camera_alt_outlined), title: const Text('拍照上传'), onTap: () => Navigator.pop(sheetContext, ImageSource.camera), ), ], ), ), ); if (source == null || !mounted) return; final picked = await ImagePicker().pickImage( source: source, imageQuality: 85, maxWidth: 1200, ); if (picked == null || !mounted) return; setState(() => _uploadingAvatar = true); try { final api = ref.read(apiClientProvider); final avatarUrl = await api.uploadFile( '/api/files/upload', File(picked.path), ); if (avatarUrl == null) throw StateError('上传未返回头像地址'); final updatedProfile = await ref .read(userServiceProvider) .updateProfile(avatarUrl: avatarUrl); final savedAvatarUrl = updatedProfile?['avatarUrl']?.toString(); if (savedAvatarUrl == null || savedAvatarUrl != avatarUrl) { throw StateError('头像地址未保存到个人资料'); } await ref.read(authProvider.notifier).applyProfile(updatedProfile!); if (mounted) { AppToast.show(context, '头像已更新', type: AppToastType.success); } } catch (_) { if (mounted) { AppToast.show(context, '头像上传失败,请稍后重试', type: AppToastType.error); } } finally { if (mounted) setState(() => _uploadingAvatar = false); } } @override Widget build(BuildContext context) { final user = ref.watch(authProvider.select((state) => state.user)); final name = user?.name?.trim().isNotEmpty == true ? user!.name! : '未设置昵称'; final phone = user?.phone.trim().isNotEmpty == true ? user!.phone : '未绑定手机号'; return GradientScaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 19), onPressed: () => popRoute(ref), ), title: const Text('个人信息'), centerTitle: true, ), body: SafeArea( child: ListView( padding: const EdgeInsets.fromLTRB(18, 12, 18, 32), children: [ _AccountSummary( name: name, phone: phone, avatarUrl: user?.avatarUrl, uploading: _uploadingAvatar, onChangeAvatar: _uploadingAvatar ? null : _changeAvatar, ), const SizedBox(height: 24), const _SectionTitle('资料管理'), const SizedBox(height: 9), _SettingsGroup( children: [ _ActionRow( icon: Icons.badge_outlined, iconColor: AppColors.primary, title: '基本资料', subtitle: '姓名、性别和出生日期', onTap: () => pushRoute(ref, 'profileEdit'), ), _ActionRow( icon: LucideIcons.folderHeart, iconColor: AppModuleVisuals.health.color, title: '健康档案', subtitle: '疾病、手术、过敏和生活习惯', onTap: () => pushRoute(ref, 'healthArchive'), ), ], ), const SizedBox(height: 28), SizedBox( height: 52, child: OutlinedButton.icon( onPressed: () => _logout(context, ref), icon: const Icon(Icons.logout_rounded), label: const Text('退出登录'), style: OutlinedButton.styleFrom( foregroundColor: AppColors.errorText, side: const BorderSide(color: Color(0xFFFECACA)), shape: RoundedRectangleBorder( borderRadius: AppRadius.mdBorder, ), textStyle: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), ), ), ], ), ), ); } Future _logout(BuildContext context, WidgetRef ref) async { final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: AppRadius.lgBorder), title: const Text('退出登录'), content: const Text('确定退出当前账号吗?'), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext, false), child: const Text('取消'), ), TextButton( onPressed: () => Navigator.pop(dialogContext, true), style: TextButton.styleFrom(foregroundColor: AppColors.errorText), child: const Text('退出'), ), ], ), ); if (confirmed != true || !context.mounted) return; await ref.read(authProvider.notifier).logout(); goRoute(ref, 'login'); } } class _AccountSummary extends StatelessWidget { final String name; final String phone; final String? avatarUrl; final bool uploading; final VoidCallback? onChangeAvatar; const _AccountSummary({ required this.name, required this.phone, required this.avatarUrl, required this.uploading, required this.onChangeAvatar, }); @override Widget build(BuildContext context) => Container( padding: const EdgeInsets.fromLTRB(20, 22, 20, 20), decoration: BoxDecoration( color: Colors.white, borderRadius: AppRadius.lgBorder, boxShadow: [ BoxShadow( color: AppColors.primary.withValues(alpha: 0.06), blurRadius: 20, offset: const Offset(0, 8), ), ], ), child: Column( children: [ Stack( clipBehavior: Clip.none, children: [ Container( width: 92, height: 92, clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: AppModuleVisuals.health.lightColor, shape: BoxShape.circle, ), child: avatarUrl?.isNotEmpty == true ? AuthenticatedNetworkImage( imageUrl: avatarUrl!, fit: BoxFit.cover, errorBuilder: (_, _, _) => const _AvatarFallback(), ) : const _AvatarFallback(), ), Positioned( right: -2, bottom: -2, child: Material( color: AppColors.primary, shape: const CircleBorder(), child: InkWell( onTap: onChangeAvatar, customBorder: const CircleBorder(), child: SizedBox( width: 34, height: 34, child: Center( child: uploading ? const SizedBox( width: 17, height: 17, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Icon( Icons.camera_alt_outlined, color: Colors.white, size: 18, ), ), ), ), ), ), ], ), const SizedBox(height: 14), Text( name, maxLines: 1, overflow: TextOverflow.ellipsis, style: AppTextStyles.summaryTitle.copyWith(fontSize: 21), ), const SizedBox(height: 5), Text(phone, style: AppTextStyles.listSubtitle), const SizedBox(height: 12), TextButton.icon( onPressed: onChangeAvatar, icon: const Icon(Icons.edit_outlined, size: 17), label: const Text('更换头像'), ), ], ), ); } class _AvatarFallback extends StatelessWidget { const _AvatarFallback(); @override Widget build(BuildContext context) => Icon( Icons.person_rounded, color: AppModuleVisuals.health.color, size: 48, ); } class _SectionTitle extends StatelessWidget { final String text; const _SectionTitle(this.text); @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.symmetric(horizontal: 2), child: Text( text, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.textSecondary, ), ), ); } class _SettingsGroup extends StatelessWidget { final List children; const _SettingsGroup({required this.children}); @override Widget build(BuildContext context) => Container( decoration: BoxDecoration( color: Colors.white, borderRadius: AppRadius.lgBorder, ), child: Column( children: [ for (var i = 0; i < children.length; i++) ...[ children[i], if (i != children.length - 1) const Padding( padding: EdgeInsets.only(left: 62), child: Divider(height: 1, color: AppColors.borderLight), ), ], ], ), ); } class _ActionRow extends StatelessWidget { final IconData icon; final Color iconColor; final String title; final String? subtitle; final VoidCallback onTap; const _ActionRow({ required this.icon, required this.iconColor, required this.title, this.subtitle, required this.onTap, }); @override Widget build(BuildContext context) => Material( color: Colors.transparent, child: InkWell( onTap: onTap, borderRadius: AppRadius.lgBorder, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 14), child: Row( children: [ Container( width: 36, height: 36, decoration: BoxDecoration( color: iconColor.withValues(alpha: 0.10), borderRadius: AppRadius.smBorder, ), child: Icon(icon, color: iconColor, size: 20), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), if (subtitle != null) ...[ const SizedBox(height: 3), Text(subtitle!, style: AppTextStyles.listSubtitle), ], ], ), ), const Icon(Icons.chevron_right_rounded, color: AppColors.textHint), ], ), ), ), ); }