import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import '../../data/api/report_data_source.dart'; import '../../data/content_providers.dart'; import '../../data/models/models.dart'; import '../../data/providers.dart'; import '../../data/state/app_interaction_state.dart'; import '../../routing/app_routes.dart'; import '../../theme/app_icons.dart'; import '../../theme/yanting_text.dart'; import '../../theme/yanting_tokens.dart'; import '../../widgets/app_buttons.dart'; import '../../widgets/app_card.dart'; import '../../widgets/page_header.dart'; import '../../widgets/sheets.dart'; import '../../widgets/states.dart'; import '../shared/report_card_widget.dart'; class ProfilePage extends ConsumerWidget { const ProfilePage({required this.dataSource, super.key}); final ReportDataSource dataSource; @override Widget build(BuildContext context, WidgetRef ref) { final colors = ShadTheme.of(context).colorScheme; final auth = ref.watch(authControllerProvider); final profile = ref.watch(profileControllerProvider); final historySnapshot = ref.watch(profileHistoryReportsProvider); final favoriteSnapshot = ref.watch(profileFavoriteReportsProvider); final savedListenSnapshot = ref.watch(profileSavedListenReportsProvider); final historyCount = historySnapshot.maybeWhen( data: (items) => items.length, orElse: () => profile.history.length, ); final favoriteCount = favoriteSnapshot.maybeWhen( data: (items) => items.length, orElse: () => profile.favorites.length, ); final savedListenCount = savedListenSnapshot.maybeWhen( data: (items) => items.length, orElse: () => profile.savedListens.length, ); return ListView( padding: const EdgeInsets.fromLTRB( YantingSpacing.screenX, 4, YantingSpacing.screenX, 16, ), children: [ const PageHeader(title: '我的'), AppCard( color: colors.secondary, child: Row( children: [ CircleAvatar( radius: 27, backgroundColor: colors.background, foregroundColor: colors.mutedForeground, child: const Icon(AppIcons.user, size: 28), ), const SizedBox(width: 15), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( auth.loggedIn ? '已登录' : '未登录', style: YantingText.cardTitle.copyWith( fontSize: 18, color: colors.foreground, ), ), const SizedBox(height: 5), Text( auth.loggedIn ? '收藏、历史和听单已在本地同步' : '登录后同步收藏、历史和听单', style: YantingText.meta.copyWith( height: 1.5, color: colors.mutedForeground, ), ), ], ), ), ], ), ), const SizedBox(height: YantingSpacing.x3), AppButton( label: auth.loggedIn ? '退出登录' : '登录 / 注册', expand: true, onPressed: auth.loggedIn ? () => ref.read(authControllerProvider.notifier).logout() : () => showLoginSheet( context, reason: '登录后同步收藏、历史和听单', onPhoneLogin: () => _login(ref, LoginMethod.phone), onSecondaryLogin: () => _login(ref, LoginMethod.wechat), ), ), const SizedBox(height: 18), _MenuGroup( children: [ _MenuRow( icon: AppIcons.history, title: '本地浏览记录', trailing: '$historyCount 条 · 本地临时', onTap: () => _showProfileListSheet( context, ref, title: '本地浏览记录', snapshot: historySnapshot, emptyTitle: '暂无本地浏览记录', emptyMessage: '打开研报详情或播放音频后会出现在这里', ), ), _MenuRow( icon: AppIcons.heart, title: '我的收藏', trailing: '$favoriteCount 条', onTap: () => _showLoginAwareList( context, ref, auth, title: '我的收藏', snapshot: favoriteSnapshot, emptyTitle: '暂无收藏', emptyMessage: '在研报详情页点击收藏后会出现在这里', ), ), _MenuRow( icon: AppIcons.headphones, title: '已存听单', trailing: '$savedListenCount 条', onTap: () => _showLoginAwareList( context, ref, auth, title: '已存听单', snapshot: savedListenSnapshot, emptyTitle: '暂无已存听单', emptyMessage: '在音频研报详情页保存听单后会出现在这里', ), ), ], ), const SizedBox(height: YantingSpacing.x3), _MenuGroup( children: [ _MenuRow( icon: AppIcons.settings, title: '设置', onTap: () => context.push(AppRoutes.settings), ), _MenuRow( icon: AppIcons.fileList, title: '用户协议', onTap: () => _showOutbound(context, ref, 'user_agreement', '用户协议'), ), _MenuRow( icon: AppIcons.shield, title: '隐私政策', onTap: () => _showOutbound(context, ref, 'privacy_policy', '隐私政策'), ), ], ), const SizedBox(height: YantingSpacing.x3), AppCard( color: colors.secondary, onTap: () => _showOutbound(context, ref, 'profile_services', '相关服务'), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( '了解相关服务', style: YantingText.body.copyWith( color: colors.foreground, fontWeight: FontWeight.w600, ), ), const SizedBox(width: 3), const Icon(AppIcons.arrowRight, size: 18), ], ), const SizedBox(height: 6), Text( '与你关注主题相关的延伸服务,内容不构成投资建议。', style: YantingText.meta.copyWith( height: 1.5, color: colors.mutedForeground, ), ), ], ), ), const SizedBox(height: 22), Text( '研听 · 全球机构研报中文解读\n登录不阻断游客完整收听第一期 · 内容不构成投资建议', textAlign: TextAlign.center, style: YantingText.meta.copyWith(fontSize: 12, height: 1.7), ), ], ); } Future _login(WidgetRef ref, LoginMethod method) async { await ref.read(authControllerProvider.notifier).login(method); await ref.read(profileControllerProvider.notifier).refresh(); } void _showOutbound( BuildContext context, WidgetRef ref, String scene, String title, ) { showOutboundSheet( context, title: title, onConfirm: () => ref .read(outboundRepositoryProvider) .recordOutbound(OutboundEvent(scene: scene)), ); } void _showLoginAwareList( BuildContext context, WidgetRef ref, AuthState auth, { required String title, required AsyncValue> snapshot, required String emptyTitle, required String emptyMessage, }) { if (!auth.loggedIn) { showLoginSheet( context, reason: '登录后查看$title', onPhoneLogin: () => _login(ref, LoginMethod.phone), onSecondaryLogin: () => _login(ref, LoginMethod.wechat), ); return; } _showProfileListSheet( context, ref, title: title, snapshot: snapshot, emptyTitle: emptyTitle, emptyMessage: emptyMessage, ); } void _showProfileListSheet( BuildContext context, WidgetRef ref, { required String title, required AsyncValue> snapshot, required String emptyTitle, required String emptyMessage, }) { showShadSheet( context: context, side: ShadSheetSide.bottom, builder: (sheetContext) => ShadSheet( title: Text(title), description: const Text('本地状态列表,真实同步接口后续接入。'), child: ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.sizeOf(sheetContext).height * 0.66, ), child: snapshot.when( loading: () => const LoadingState(label: '正在加载列表'), error: (error, _) => ErrorState(message: error.toString()), data: (items) { if (items.isEmpty) { return EmptyState( title: emptyTitle, message: emptyMessage, icon: Icons.inbox_outlined, ); } return ListView.separated( shrinkWrap: true, itemCount: items.length, separatorBuilder: (_, _) => const SizedBox(height: YantingSpacing.x3), itemBuilder: (_, index) { final report = items[index]; return ReportCardWidget( report: report, onTap: () { Navigator.pop(sheetContext); ref .read(profileControllerProvider.notifier) .addHistory(report.id); openReportDetail(context, dataSource, report); }, ); }, ); }, ), ), ), ); } } class _MenuGroup extends StatelessWidget { const _MenuGroup({required this.children}); final List children; @override Widget build(BuildContext context) { final colors = ShadTheme.of(context).colorScheme; return AppCard( padding: EdgeInsets.zero, child: Column( children: [ for (var index = 0; index < children.length; index++) ...[ children[index], if (index != children.length - 1) Divider(height: 1, thickness: 1, color: colors.border), ], ], ), ); } } class _MenuRow extends StatelessWidget { const _MenuRow({ required this.icon, required this.title, required this.onTap, this.trailing, }); final IconData icon; final String title; final String? trailing; final VoidCallback onTap; @override Widget build(BuildContext context) { final colors = ShadTheme.of(context).colorScheme; return InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15), child: Row( children: [ Icon(icon, size: 20, color: colors.foreground), const SizedBox(width: 13), Expanded(child: Text(title, style: YantingText.body)), if (trailing != null) DecoratedBox( decoration: BoxDecoration( color: colors.secondary, borderRadius: BorderRadius.circular(YantingRadius.pill), ), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 3, ), child: Text( trailing!, style: YantingText.meta.copyWith( fontSize: 11.5, color: colors.secondaryForeground, ), ), ), ) else Icon( AppIcons.arrowRight, color: colors.mutedForeground, size: 20, ), ], ), ), ); } }