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: '我的'), if (auth.loggedIn) ...[ _LoggedInHeader(auth: auth), const SizedBox(height: YantingSpacing.x3), _StatsCard( favoriteCount: favoriteCount, historyCount: historyCount, savedListenCount: savedListenCount, ), ] else ...[ 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( '未登录', style: YantingText.cardTitle.copyWith( fontSize: 18, color: colors.foreground, ), ), const SizedBox(height: 5), Text( '登录后同步收藏、历史和听单', style: YantingText.meta.copyWith( height: 1.5, color: colors.mutedForeground, ), ), ], ), ), ], ), ), const SizedBox(height: YantingSpacing.x3), AppButton( label: '登录 / 注册', expand: true, onPressed: () => context.push( '${AppRoutes.login}?next=${Uri.encodeComponent(AppRoutes.profile)}', ), ), ], const SizedBox(height: 18), _MenuGroup( children: [ _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: '在音频研报详情页保存听单后会出现在这里', ), ), _MenuRow( icon: AppIcons.history, title: '浏览/收听历史', trailing: '$historyCount 条', onTap: () => _showProfileListSheet( context, ref, title: '浏览/收听历史', snapshot: historySnapshot, emptyTitle: '暂无浏览/收听历史', emptyMessage: '打开研报详情或播放音频后会出现在这里', ), ), _MenuRow( icon: Icons.download_outlined, title: '下载记录', trailing: 'Phase 1 预留', onTap: () => showAppToast(context, '下载记录将在后续版本接入'), ), ], ), const SizedBox(height: YantingSpacing.x3), _MenuGroup( children: [ _MenuRow( icon: AppIcons.settings, title: '设置', onTap: () => context.push(AppRoutes.settings), ), if (auth.loggedIn) _MenuRow( icon: Icons.logout, title: '退出登录', onTap: () => ref.read(authControllerProvider.notifier).logout(), ), ], ), 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 _LoggedInHeader extends StatelessWidget { const _LoggedInHeader({required this.auth}); final AuthState auth; @override Widget build(BuildContext context) { final colors = ShadTheme.of(context).colorScheme; final phone = auth.phone; final methodLabel = switch (auth.loginMethod) { LoginMethod.phone => '手机号', LoginMethod.wechat => '微信', LoginMethod.apple => 'Apple', _ => '本地登录', }; return AppCard( color: colors.brandSoft, borderColor: colors.brandSoftBorder, padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 30), child: Row( children: [ CircleAvatar( radius: 34, backgroundColor: colors.background, foregroundColor: colors.primary, child: Text( phone == null || phone.isEmpty ? '研' : phone.characters.first, style: YantingText.sectionTitle.copyWith( color: colors.primary, fontSize: 27, ), ), ), const SizedBox(width: 22), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( phone == null || phone.isEmpty ? '研界用户' : '手机号用户', style: YantingText.sectionTitle.copyWith( color: colors.foreground, fontSize: 22, ), ), const SizedBox(height: 4), Text( '已登录 · $methodLabel${phone == null || phone.isEmpty ? '' : ' · ${_maskPhone(phone)}'}', style: YantingText.body.copyWith( color: colors.mutedForeground, fontSize: 15, ), ), ], ), ), ], ), ); } } String _maskPhone(String phone) { if (phone.length < 7) return phone; return '${phone.substring(0, 3)}****${phone.substring(phone.length - 4)}'; } class _StatsCard extends StatelessWidget { const _StatsCard({ required this.favoriteCount, required this.historyCount, required this.savedListenCount, }); final int favoriteCount; final int historyCount; final int savedListenCount; @override Widget build(BuildContext context) { final colors = ShadTheme.of(context).colorScheme; return AppCard( padding: EdgeInsets.zero, child: Row( children: [ Expanded( child: _StatCell(value: favoriteCount, label: '收藏'), ), SizedBox( height: 66, child: VerticalDivider( width: 1, thickness: 1, color: colors.border, ), ), Expanded( child: _StatCell(value: historyCount, label: '历史'), ), SizedBox( height: 66, child: VerticalDivider( width: 1, thickness: 1, color: colors.border, ), ), Expanded( child: _StatCell(value: savedListenCount, label: '听单'), ), ], ), ); } } class _StatCell extends StatelessWidget { const _StatCell({required this.value, required this.label}); final int value; final String label; @override Widget build(BuildContext context) { final colors = ShadTheme.of(context).colorScheme; return Padding( padding: const EdgeInsets.symmetric(vertical: 18), child: Column( children: [ Text( '$value', style: YantingText.sectionTitle.copyWith( color: const Color(0xFF163E08), fontSize: 22, fontFeatures: YantingTypographyFeatures.tabularNums, ), ), const SizedBox(height: 3), Text( label, style: YantingText.meta.copyWith( color: colors.mutedForeground, fontSize: 14, ), ), ], ), ); } } 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: const Color(0xFF143B05)), 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, ), ], ), ), ); } }