// 个人中心页面 — 用户信息 + 功能入口 + 设置 import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:nuanji_app/core/theme/app_colors.dart'; import 'package:nuanji_app/core/theme/app_radius.dart'; import 'package:nuanji_app/core/constants/design_tokens.dart'; import 'package:nuanji_app/features/auth/bloc/auth_bloc.dart'; import 'package:nuanji_app/features/profile/bloc/settings_bloc.dart'; import 'package:nuanji_app/data/models/user.dart'; import 'package:nuanji_app/data/repositories/journal_repository.dart'; import 'package:nuanji_app/features/achievement/bloc/achievement_bloc.dart'; import 'package:nuanji_app/data/remote/api_client.dart'; /// 个人中心页面 class ProfilePage extends StatelessWidget { const ProfilePage({super.key}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final authState = context.watch().state; final user = authState is Authenticated ? authState.user : null; final displayName = user?.displayLabel ?? '用户'; final role = user?.primaryRoleType; final isDark = theme.brightness == Brightness.dark; // 柔和背景色(根据明暗模式) final accentSoft = isDark ? const Color(0xFF3A2A22) : const Color(0xFFFFE0D6); final tertiarySoft = isDark ? AppColors.tertiarySoftDark : AppColors.tertiarySoftLight; final roseSoft = isDark ? AppColors.roseSoftDark : AppColors.roseSoftLight; final secondarySoft = isDark ? AppColors.secondarySoftDark : AppColors.secondarySoftLight; final surfaceWarm = isDark ? AppColors.surfaceWarmDark : AppColors.surfaceWarmLight; final borderSoft = isDark ? AppColors.borderSoftDark : AppColors.borderSoftLight; final greyBg = isDark ? const Color(0xFF2A2520) : const Color(0xFFF5F0EB); return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: [ // ---- 用户头像卡片 ---- Card( elevation: 0, shape: RoundedRectangleBorder(borderRadius: AppRadius.lgBorder), color: colorScheme.surface, child: Padding( padding: const EdgeInsets.all(24), child: Column( children: [ // 头像 — 渐变背景 + emoji Container( width: 80, height: 80, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [AppColors.accent, AppColors.tertiary], ), ), alignment: Alignment.center, child: Text( displayName.isNotEmpty ? displayName[0] : '😊', style: const TextStyle(fontSize: 36, color: Colors.white), ), ), const SizedBox(height: 12), // 用户名 Text(displayName, style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 4), // 角色 Text( _roleLabel(role), style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.6), ), ), const SizedBox(height: 4), // 签名 Text( user?.displayName ?? '这个人很懒,什么都没写', style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.5), ), ), ], ), ), ), const SizedBox(height: 12), // ---- 统计栏(真实数据) ---- _LiveStatsBar(borderSoft: borderSoft, colorScheme: colorScheme), const SizedBox(height: 20), // ---- 成就徽章(动态加载) ---- Align( alignment: Alignment.centerLeft, child: Text('成就徽章', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, )), ), const SizedBox(height: 12), SizedBox( height: 100, child: _AchievementBadges( accentSoft: accentSoft, tertiarySoft: tertiarySoft, roseSoft: roseSoft, secondarySoft: secondarySoft, ), ), const SizedBox(height: 20), // ---- 功能入口 (emoji 图标) ---- _EmojiMenuItem(emoji: '🏆', bg: tertiarySoft, title: '我的成就', onTap: () => context.go('/achievements')), _EmojiMenuItem(emoji: '🎨', bg: roseSoft, title: '贴纸收藏', onTap: () => context.go('/stickers')), _EmojiMenuItem(emoji: '📋', bg: secondarySoft, title: '日记模板', onTap: () => context.go('/templates')), _EmojiMenuItem(emoji: '👥', bg: accentSoft, title: '我的班级', onTap: () => context.go('/class')), _EmojiMenuItem(emoji: '😊', bg: tertiarySoft, title: '心情统计', onTap: () => context.go('/mood')), _EmojiMenuItem(emoji: '⚙️', bg: greyBg, title: '设置', onTap: () => context.go('/settings')), if (role != null && role.name == 'teacher') _EmojiMenuItem(emoji: '📚', bg: accentSoft, title: '教师管理', onTap: () => context.go('/teacher')), if (role != null && role.name == 'parent') _EmojiMenuItem(emoji: '👨‍👩‍👧', bg: roseSoft, title: '家长中心', onTap: () => context.go('/parent')), const Divider(height: 32), // ---- 开关项 ---- _EmojiToggleItem( emoji: '🔔', bg: tertiarySoft, title: '消息通知', value: true, onChanged: (v) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(v ? '已开启通知' : '已关闭通知')), ); }, activeColor: AppColors.accent, ), _EmojiToggleItem( emoji: '🌙', bg: surfaceWarm, title: '深色模式', value: theme.brightness == Brightness.dark, onChanged: (v) { final settings = context.read(); settings.changeTheme(v ? ThemeMode.dark : ThemeMode.light); }, activeColor: AppColors.accent, ), const Divider(height: 32), // ---- 更多设置 ---- _EmojiMenuItem(emoji: '📤', bg: secondarySoft, title: '导出数据', onTap: () { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('导出功能即将上线'))); }), _EmojiMenuItem(emoji: '💬', bg: accentSoft, title: '意见反馈', onTap: () { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('感谢你的反馈'))); }), _EmojiMenuItem( emoji: 'ℹ️', bg: greyBg, title: '关于', subtitle: '版本 1.0.0', onTap: () => context.go('/about'), ), const SizedBox(height: 16), // 退出登录 SizedBox( width: double.infinity, child: OutlinedButton( onPressed: () { context.read().add(const LogoutRequested()); context.go('/login'); }, style: OutlinedButton.styleFrom(foregroundColor: colorScheme.error), child: const Text('退出登录'), ), ), const SizedBox(height: DesignTokens.spacing24), ], ), ); } String _roleLabel(UserRoleType? role) { if (role == null) return '未选择角色'; return switch (role.code) { 'teacher' => '老师', 'student' => '学生', 'parent' => '家长', 'independent' => '独立用户', _ => '用户', }; } } /// 统计项 class _StatItem extends StatelessWidget { const _StatItem({required this.label, required this.value, required this.valueColor}); final String label; final String value; final Color valueColor; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Expanded( child: Column( children: [ Text(value, style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: valueColor, )), const SizedBox(height: 4), Text(label, style: theme.textTheme.labelSmall?.copyWith( color: colorScheme(context).onSurface.withValues(alpha: 0.5), )), ], ), ); } static ColorScheme colorScheme(BuildContext context) => Theme.of(context).colorScheme; } /// 成就徽章项 class _BadgeItem extends StatelessWidget { const _BadgeItem({ required this.emoji, required this.name, required this.bgColor, required this.locked, }); final String emoji; final String name; final Color bgColor; final bool locked; @override Widget build(BuildContext context) { final theme = Theme.of(context); return SizedBox( width: 80, child: Opacity( opacity: locked ? 0.5 : 1.0, child: Column( children: [ Container( width: 56, height: 56, decoration: BoxDecoration( color: locked ? theme.colorScheme.outlineVariant.withValues(alpha: 0.3) : bgColor, shape: BoxShape.circle, ), alignment: Alignment.center, child: Text(emoji, style: const TextStyle(fontSize: 24)), ), const SizedBox(height: 6), Text(name, style: theme.textTheme.labelSmall?.copyWith(fontSize: 11)), ], ), ), ); } } /// emoji 菜单项 class _EmojiMenuItem extends StatelessWidget { const _EmojiMenuItem({ required this.emoji, required this.bg, required this.title, required this.onTap, this.subtitle, }); final String emoji; final Color bg; final String title; final String? subtitle; final VoidCallback onTap; @override Widget build(BuildContext context) { final theme = Theme.of(context); return ListTile( leading: Container( width: 36, height: 36, decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(10), ), alignment: Alignment.center, child: Text(emoji, style: const TextStyle(fontSize: 18)), ), title: Text(title, style: theme.textTheme.bodyMedium), subtitle: subtitle != null ? Text(subtitle!, style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurface.withValues(alpha: 0.5), )) : null, trailing: const Icon(Icons.chevron_right, size: 20), onTap: onTap, contentPadding: const EdgeInsets.symmetric(horizontal: 4), ); } } /// emoji 开关菜单项 class _EmojiToggleItem extends StatelessWidget { const _EmojiToggleItem({ required this.emoji, required this.bg, required this.title, required this.value, required this.onChanged, required this.activeColor, }); final String emoji; final Color bg; final String title; final bool value; final ValueChanged onChanged; final Color activeColor; @override Widget build(BuildContext context) { final theme = Theme.of(context); return ListTile( leading: Container( width: 36, height: 36, decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(10), ), alignment: Alignment.center, child: Text(emoji, style: const TextStyle(fontSize: 18)), ), title: Text(title, style: theme.textTheme.bodyMedium), trailing: Switch( value: value, onChanged: onChanged, activeColor: activeColor, ), contentPadding: const EdgeInsets.symmetric(horizontal: 4), ); } } /// 动态统计栏 — 从 JournalRepository 加载真实数据 class _LiveStatsBar extends StatefulWidget { const _LiveStatsBar({required this.borderSoft, required this.colorScheme}); final Color borderSoft; final ColorScheme colorScheme; @override State<_LiveStatsBar> createState() => _LiveStatsBarState(); } class _LiveStatsBarState extends State<_LiveStatsBar> { int _totalCount = 0; int _streakDays = 0; int _monthCount = 0; @override void initState() { super.initState(); _loadStats(); } Future _loadStats() async { try { final repo = context.read(); final totalCount = await repo.getJournalCount(); final journals = await repo.getJournals(); if (!mounted) return; final today = DateTime.now(); final monthCount = journals .where((j) => j.date.year == today.year && j.date.month == today.month) .length; // 推算连续天数 final dates = journals.map((j) => j.date).toSet(); var streak = 0; var checkDate = today; while (dates.contains(DateTime(checkDate.year, checkDate.month, checkDate.day))) { streak++; checkDate = checkDate.subtract(const Duration(days: 1)); } setState(() { _totalCount = totalCount; _streakDays = streak; _monthCount = monthCount; }); } catch (e) { debugPrint('ProfilePage._loadStats 失败: $e'); // 保持默认 0 值 } } @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: widget.colorScheme.surface, borderRadius: AppRadius.mdBorder, ), padding: const EdgeInsets.symmetric(vertical: 16), child: Row( children: [ _StatItem(label: '总日记', value: '$_totalCount', valueColor: AppColors.accent), VerticalDivider(width: 1, indent: 4, endIndent: 4, color: widget.borderSoft), _StatItem(label: '连续天数', value: '$_streakDays', valueColor: AppColors.secondary), VerticalDivider(width: 1, indent: 4, endIndent: 4, color: widget.borderSoft), _StatItem(label: '本月日记', value: '$_monthCount', valueColor: widget.colorScheme.onSurface), VerticalDivider(width: 1, indent: 4, endIndent: 4, color: widget.borderSoft), _StatItem(label: '贴纸数', value: '--', valueColor: widget.colorScheme.onSurface), ], ), ); } } /// 成就徽章动态组件 — 从 AchievementBloc 加载真实数据 class _AchievementBadges extends StatefulWidget { const _AchievementBadges({ required this.accentSoft, required this.tertiarySoft, required this.roseSoft, required this.secondarySoft, }); final Color accentSoft; final Color tertiarySoft; final Color roseSoft; final Color secondarySoft; @override State<_AchievementBadges> createState() => _AchievementBadgesState(); } class _AchievementBadgesState extends State<_AchievementBadges> { late final AchievementBloc _bloc; List _achievements = []; @override void initState() { super.initState(); _bloc = AchievementBloc(api: context.read()); _bloc.load(); _bloc.addListener(_onUpdate); } void _onUpdate() { if (mounted) { setState(() { _achievements = _bloc.state.achievements; }); } } @override void dispose() { _bloc.removeListener(_onUpdate); _bloc.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (_achievements.isEmpty) { return const Center(child: Text('暂无成就', style: TextStyle(fontSize: 13))); } final bgColors = [widget.accentSoft, widget.tertiarySoft, widget.roseSoft, widget.secondarySoft]; return ListView.separated( scrollDirection: Axis.horizontal, itemCount: _achievements.length, separatorBuilder: (_, __) => const SizedBox(width: 12), itemBuilder: (context, index) { final a = _achievements[index]; return _BadgeItem( emoji: a.icon ?? '🏆', name: a.name, bgColor: bgColors[index % bgColors.length], locked: !a.isUnlocked, ); }, ); } }