import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:im_app/core/foundation/config.dart'; import 'package:im_app/features/settings/presentation/settings_view_model.dart'; /// 我的页(#39 / #40 / #41) /// /// ## 结构(iOS SettingsView.swift 对齐) /// /// - ProfileHeroCard:72pt 渐变头像 + 昵称 + @J{uid} handle + 手机号 + bio /// - AppBar:compact,右侧 QR 图标 + 编辑铅笔 /// - 卡片组 1(账户):我的钱包 / 账户安全 /// - 卡片组 2(工具):收藏 / 最近呼叫 / 链接设备 / 聊天文件夹 /// - 卡片组 3「偏好设置」:通知和声音 / 隐私设置 / 黑名单 / 语言 / 主题 /// - 卡片组 4「关于」:用户协议 / 隐私政策 / 版本号 /// - 退出登录(全宽红色按钮) class SettingsPage extends ConsumerWidget { const SettingsPage({super.key}); // ── iOS 色板 ────────────────────────────────────────────────────────────── static const Color _walletColor = Color(0xFFFFAA5B); static const Color _securityColor = Color(0xFF8A5CF6); static const Color _favoriteColor = Color(0xFFFFAF45); static const Color _callColor = Color(0xFF4CB050); static const Color _deviceColor = Color(0xFF5667FF); static const Color _folderColor = Color(0xFFF2994A); static const Color _notifColor = Color(0xFFFF8B5E); static const Color _privacyColor = Color(0xFF0BB8A9); static const Color _blockColor = Color(0xFFFF4B4B); static const Color _langColor = Color(0xFF5667FF); static const Color _themeColor = Color(0xFF8A5CF6); @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(settingsViewModelProvider); final vm = ref.read(settingsViewModelProvider.notifier); return Scaffold( backgroundColor: Theme.of(context).colorScheme.surfaceContainerLowest, body: CustomScrollView( slivers: [ // ── AppBar(compact,QR + edit) ──────────────────────────────────── SliverAppBar( title: const Text('我的'), pinned: true, automaticallyImplyLeading: false, actions: [ IconButton( icon: const Icon(Icons.qr_code_scanner_rounded), tooltip: '我的二维码', onPressed: () {}, // TODO: QR code page ), IconButton( icon: const Icon(Icons.edit_outlined), tooltip: '编辑资料', onPressed: () => vm.navigateToEditProfile(context), ), const SizedBox(width: 4), ], ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── ProfileHeroCard ─────────────────────────────────────── _ProfileHeroCard( state: state, onTap: () => vm.navigateToEditProfile(context), ), const SizedBox(height: 16), // ── 卡片组 1:账户 ──────────────────────────────────────── _SettingsCard( items: [ _RowConfig( icon: Icons.credit_card_rounded, iconColor: _walletColor, title: '我的钱包', onTap: () {}, // TODO: 钱包页 ), _RowConfig( icon: Icons.shield_rounded, iconColor: _securityColor, title: '账户安全', onTap: () {}, // TODO: 账户安全页 ), ], ), const SizedBox(height: 16), // ── 卡片组 2:工具 ──────────────────────────────────────── _SettingsCard( items: [ _RowConfig( icon: Icons.star_rounded, iconColor: _favoriteColor, title: '收藏', onTap: () {}, // TODO: 收藏页 ), _RowConfig( icon: Icons.phone_rounded, iconColor: _callColor, title: '最近呼叫', onTap: () {}, // TODO: 呼叫记录页 ), _RowConfig( icon: Icons.laptop_rounded, iconColor: _deviceColor, title: '链接设备', onTap: () {}, // TODO: 设备管理页 ), _RowConfig( icon: Icons.folder_rounded, iconColor: _folderColor, title: '聊天文件夹', onTap: () {}, // TODO: Issue #11 ), ], ), const SizedBox(height: 16), // ── 卡片组 3:偏好设置 ──────────────────────────────────── _SectionLabel('偏好设置'), _SettingsCard( items: [ _RowConfig( icon: Icons.notifications_rounded, iconColor: _notifColor, title: '通知和声音', onTap: () {}, // TODO: 通知设置页 ), _RowConfig( icon: Icons.lock_rounded, iconColor: _privacyColor, title: '隐私设置', onTap: () {}, // TODO: 隐私设置页 ), _RowConfig( icon: Icons.block_rounded, iconColor: _blockColor, title: '黑名单', onTap: () => vm.navigateToBlocklist(context), ), _RowConfig( icon: Icons.language_rounded, iconColor: _langColor, title: '语言', onTap: () => vm.navigateToLanguage(context), ), _RowConfig( icon: Icons.palette_rounded, iconColor: _themeColor, title: '主题', onTap: () => vm.navigateToTheme(context), ), ], ), const SizedBox(height: 16), // ── 卡片组 4:关于 ──────────────────────────────────────── _SectionLabel('关于'), _SettingsCard( items: [ _RowConfig( icon: Icons.description_outlined, iconColor: Colors.grey, title: '用户协议', onTap: () => vm.navigateToAbout(context), ), _RowConfig( icon: Icons.privacy_tip_outlined, iconColor: Colors.grey, title: '隐私政策', onTap: () => vm.navigateToAbout(context), ), _RowConfig( icon: Icons.info_outline_rounded, iconColor: Colors.grey, title: '版本号', subtitle: AppConfig.appVersion.isEmpty ? '1.0.0' : AppConfig.appVersion, onTap: () {}, showChevron: false, ), ], ), const SizedBox(height: 24), // ── 退出登录 ─────────────────────────────────────────────── _LogoutButton( isLoading: state.isLoggingOut, onTap: () => _confirmLogout(context, ref), ), const SizedBox(height: 32), ], ), ), ), ], ), ); } Future _confirmLogout(BuildContext context, WidgetRef ref) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('退出登录'), content: const Text('确定要退出当前账号吗?'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('取消'), ), TextButton( onPressed: () => Navigator.of(ctx).pop(true), style: TextButton.styleFrom(foregroundColor: Colors.red), child: const Text('退出'), ), ], ), ); if (confirmed == true) { ref.read(settingsViewModelProvider.notifier).logout(); } } } // ── ProfileHeroCard (#39 / #41) ─────────────────────────────────────────────── /// iOS SettingsView.swift ProfileHeroCard 对齐 /// /// 8 色渐变占位(uid % 8)+ 昵称 + @J{uid} handle + 手机号 + bio class _ProfileHeroCard extends StatelessWidget { const _ProfileHeroCard({required this.state, required this.onTap}); final SettingsState state; final VoidCallback onTap; // 8 色渐变方案(iOS 端 ProfileHeroCard 同款) static const _gradients = [ [Color(0xFF4776E6), Color(0xFF8E54E9)], // 0: 蓝紫 [Color(0xFF11998E), Color(0xFF38EF7D)], // 1: 青绿 [Color(0xFFFC466B), Color(0xFF3F5EFB)], // 2: 粉蓝 [Color(0xFFF7971E), Color(0xFFFFD200)], // 3: 橙黄 [Color(0xFF56CCF2), Color(0xFF2F80ED)], // 4: 天蓝 [Color(0xFFEB3349), Color(0xFFF45C43)], // 5: 红橙 [Color(0xFF1FA2FF), Color(0xFF12D8FA)], // 6: 蓝青 [Color(0xFF9D50BB), Color(0xFF6E48AA)], // 7: 深紫 ]; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Card( child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(16), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ _HeroAvatar( avatarUrl: state.avatarUrl, uid: state.uid, isLoading: state.isLoading, ), const SizedBox(width: 16), Expanded( child: state.isLoading ? _ProfileSkeleton() : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( state.nickname.isEmpty ? '加载中…' : state.nickname, style: Theme.of(context) .textTheme .titleMedium ?.copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: 2), if (state.uid > 0) Text( '@J${state.uid}', style: Theme.of(context) .textTheme .bodySmall ?.copyWith(color: cs.onSurfaceVariant), ), if (state.maskedContact.isNotEmpty) Text( state.maskedContact, style: Theme.of(context) .textTheme .bodySmall ?.copyWith(color: cs.onSurfaceVariant), ), const SizedBox(height: 2), Text( state.bio.isNotEmpty ? state.bio : '添加一句话简介', style: Theme.of(context) .textTheme .bodySmall ?.copyWith( color: state.bio.isNotEmpty ? cs.onSurfaceVariant : cs.onSurfaceVariant.withOpacity(0.5), fontStyle: state.bio.isEmpty ? FontStyle.italic : FontStyle.normal, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), const Icon(Icons.chevron_right, color: Colors.grey), ], ), ), ), ); } } class _HeroAvatar extends StatelessWidget { const _HeroAvatar({ required this.avatarUrl, required this.uid, required this.isLoading, }); final String? avatarUrl; final int uid; final bool isLoading; @override Widget build(BuildContext context) { const radius = 36.0; // 72pt diameter if (isLoading) { return const CircleAvatar( radius: radius, child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2), ), ); } if (avatarUrl != null && avatarUrl!.isNotEmpty) { return CircleAvatar( radius: radius, backgroundImage: NetworkImage(avatarUrl!), ); } // 渐变占位(uid % 8) final colors = _ProfileHeroCard._gradients[uid.abs() % 8]; return Container( width: radius * 2, height: radius * 2, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( colors: colors, begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), child: const Icon(Icons.person, size: 36, color: Colors.white), ); } } class _ProfileSkeleton extends StatelessWidget { @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( height: 16, width: 120, decoration: BoxDecoration( color: Colors.grey.shade300, borderRadius: BorderRadius.circular(4), ), ), const SizedBox(height: 8), Container( height: 12, width: 80, decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(4), ), ), ], ); } } // ── 设置分组 (#40) ───────────────────────────────────────────────────────────── class _SectionLabel extends StatelessWidget { const _SectionLabel(this.text); final String text; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(left: 4, bottom: 8), child: Text( text, style: Theme.of(context).textTheme.labelMedium?.copyWith( color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.w600, ), ), ); } } class _RowConfig { const _RowConfig({ required this.icon, required this.iconColor, required this.title, this.subtitle, required this.onTap, this.showChevron = true, }); final IconData icon; final Color iconColor; final String title; final String? subtitle; final VoidCallback onTap; final bool showChevron; } class _SettingsCard extends StatelessWidget { const _SettingsCard({required this.items}); final List<_RowConfig> items; @override Widget build(BuildContext context) { return Card( child: Column( children: [ for (int i = 0; i < items.length; i++) ...[ if (i > 0) Divider( height: 1, indent: 60, color: Theme.of(context).dividerColor.withOpacity(0.5), ), _SettingsRow(config: items[i]), ], ], ), ); } } /// iOS 风格行:36pt 彩色圆角正方形图标 + 标题 + 可选副标题 + chevron class _SettingsRow extends StatelessWidget { const _SettingsRow({required this.config}); final _RowConfig config; @override Widget build(BuildContext context) { return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2), leading: _IconBox(icon: config.icon, color: config.iconColor), title: Text(config.title), subtitle: config.subtitle != null ? Text( config.subtitle!, style: Theme.of(context) .textTheme .bodySmall ?.copyWith(color: Colors.grey), ) : null, trailing: config.showChevron ? const Icon(Icons.chevron_right, size: 18, color: Colors.grey) : null, onTap: config.onTap, ); } } /// 36pt 圆角正方形彩色图标盒(iOS Settings icon style) class _IconBox extends StatelessWidget { const _IconBox({required this.icon, required this.color}); final IconData icon; final Color color; @override Widget build(BuildContext context) { return Container( width: 36, height: 36, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(8), ), child: Icon(icon, color: Colors.white, size: 20), ); } } // ── 退出登录 ─────────────────────────────────────────────────────────────────── class _LogoutButton extends StatelessWidget { const _LogoutButton({required this.isLoading, required this.onTap}); final bool isLoading; final VoidCallback onTap; @override Widget build(BuildContext context) { return SizedBox( width: double.infinity, child: FilledButton( onPressed: isLoading ? null : onTap, style: FilledButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), child: isLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2), ) : const Text('退出登录', style: TextStyle(fontSize: 16)), ), ); } }