Files
nj/app/lib/features/profile/views/profile_page.dart
iven 8331db63ba feat(app): 设置页 UI + Mood/成就/贴纸 BLoC 接入 API + B7 测试扩展
前端改动:
- 新建设置页面 (主题切换/关于/隐私政策/用户协议/儿童隐私保护)
- SettingsBloc 注册到 MultiRepositoryProvider 全局可访问
- MoodBloc 修复编译错误 + 接入 /diary/stats/mood API
- MoodPage 添加错误状态展示和重试按钮
- AchievementBloc + 页面改造接入 /diary/achievements API
- StickerBloc + 页面改造接入 /diary/sticker-packs API
- TemplateBloc + 页面改造接入 /diary/templates API
- ProfilePage 设置入口改为跳转 /settings
- 添加 /settings 路由

后端改动:
- 扩展 mood_stats_service 测试 (连续天数算法/心情计数/边界场景)
- 新增 class_service 测试 (班级码生成/唯一性/错误映射)
- 新增 achievement_service 测试 (DTO 结构/序列化/map 构建)
- 新增 sticker_service 测试 (DTO 序列化/错误处理)
- 扩展 dto.rs 测试 (achievement/mood_stats/sticker/template/notification)
- 清理 2 个 unused import warning

验证:
- cargo check 0 error 0 warning
- flutter analyze 0 error
2026-06-01 11:19:43 +08:00

172 lines
5.7 KiB
Dart

// 个人中心页面 — 用户信息 + 功能入口 + 设置
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/features/auth/bloc/auth_bloc.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<AuthBloc>().state;
final displayName = authState is Authenticated ? authState.user.displayLabel : '用户';
final role = authState is Authenticated ? authState.user.primaryRoleType : null;
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 用户头像卡片
Card(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(22)),
color: colorScheme.primaryContainer,
child: Padding(
padding: const EdgeInsets.all(24),
child: Row(
children: [
CircleAvatar(
radius: 32,
backgroundColor: colorScheme.primary.withValues(alpha: 0.2),
child: Text(
displayName.characters.first,
style: theme.textTheme.headlineSmall?.copyWith(color: colorScheme.primary),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(displayName, style: theme.textTheme.titleMedium?.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: 20),
// 功能入口
_ProfileMenuItem(
icon: Icons.auto_awesome_outlined,
iconColor: AppColors.accent,
title: '我的成就',
onTap: () => context.go('/achievements'),
),
_ProfileMenuItem(
icon: Icons.emoji_emotions_outlined,
iconColor: AppColors.secondary,
title: '贴纸收藏',
onTap: () => context.go('/stickers'),
),
_ProfileMenuItem(
icon: Icons.dashboard_customize_outlined,
iconColor: AppColors.tertiary,
title: '日记模板',
onTap: () => context.go('/templates'),
),
_ProfileMenuItem(
icon: Icons.groups_outlined,
iconColor: colorScheme.primary,
title: '我的班级',
onTap: () => context.go('/class'),
),
if (role != null && role.name == 'teacher')
_ProfileMenuItem(
icon: Icons.school_outlined,
iconColor: AppColors.accent,
title: '教师管理',
onTap: () => context.go('/teacher'),
),
if (role != null && role.name == 'parent')
_ProfileMenuItem(
icon: Icons.family_restroom_outlined,
iconColor: AppColors.rose,
title: '家长中心',
onTap: () => context.go('/parent'),
),
const Divider(height: 32),
_ProfileMenuItem(
icon: Icons.bar_chart_outlined,
iconColor: colorScheme.primary,
title: '心情统计',
onTap: () => context.go('/mood'),
),
_ProfileMenuItem(
icon: Icons.settings_outlined,
iconColor: colorScheme.onSurface.withValues(alpha: 0.5),
title: '设置',
onTap: () => context.go('/settings'),
),
const SizedBox(height: 16),
// 退出登录
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: () {
context.read<AuthBloc>().add(const LogoutRequested());
context.go('/login');
},
style: OutlinedButton.styleFrom(foregroundColor: colorScheme.error),
child: const Text('退出登录'),
),
),
],
),
);
}
String _roleLabel(dynamic role) {
if (role == null) return '未选择角色';
return switch (role.name) {
'teacher' => '老师',
'student' => '学生',
'parent' => '家长',
'independent' => '独立用户',
_ => '用户',
};
}
}
class _ProfileMenuItem extends StatelessWidget {
const _ProfileMenuItem({
required this.icon,
required this.iconColor,
required this.title,
required this.onTap,
});
final IconData icon;
final Color iconColor;
final String title;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return ListTile(
leading: Icon(icon, color: iconColor),
title: Text(title, style: theme.textTheme.bodyMedium),
trailing: const Icon(Icons.chevron_right, size: 20),
onTap: onTap,
contentPadding: const EdgeInsets.symmetric(horizontal: 4),
);
}
}