diff --git a/apps/lib/core/di/injection.dart b/apps/lib/core/di/injection.dart index 4458909..8b34be6 100644 --- a/apps/lib/core/di/injection.dart +++ b/apps/lib/core/di/injection.dart @@ -11,6 +11,7 @@ import '../../features/auth/data/auth_repository.dart'; import '../../features/auth/data/auth_repository_impl.dart'; import '../../features/auth/presentation/bloc/auth_bloc.dart'; import '../../features/calendar/ui/calendar_state_manager.dart'; +import '../../features/users/data/users_api.dart'; final sl = GetIt.instance; @@ -40,6 +41,9 @@ Future configureDependencies() async { final authApi = AuthApi(apiClient); sl.registerSingleton(authApi); + final usersApi = UsersApi(apiClient); + sl.registerSingleton(usersApi); + final authRepository = AuthRepositoryImpl( api: authApi, tokenStorage: tokenStorage, diff --git a/apps/lib/core/router/app_router.dart b/apps/lib/core/router/app_router.dart index 871e0f2..b41e07b 100644 --- a/apps/lib/core/router/app_router.dart +++ b/apps/lib/core/router/app_router.dart @@ -21,6 +21,8 @@ import '../../features/settings/ui/screens/settings_screen.dart'; import '../../features/settings/ui/screens/features_screen.dart'; import '../../features/settings/ui/screens/memory_screen.dart'; import '../../features/settings/ui/screens/account_screen.dart'; +import '../../features/settings/ui/screens/change_password_screen.dart'; +import '../../features/settings/ui/screens/edit_profile_screen.dart'; final _protectedRoutes = [ '/home', @@ -34,6 +36,8 @@ final _protectedRoutes = [ '/settings/features', '/settings/memory', '/settings/account', + '/change-password', + '/edit-profile', '/messages/invites', ]; @@ -138,6 +142,14 @@ GoRouter createAppRouter(AuthBloc authBloc) { path: '/settings/account', builder: (context, state) => const AccountScreen(), ), + GoRoute( + path: '/change-password', + builder: (context, state) => const ChangePasswordScreen(), + ), + GoRoute( + path: '/edit-profile', + builder: (context, state) => const EditProfileScreen(), + ), ], ); } diff --git a/apps/lib/features/settings/data/services/memory_service.dart b/apps/lib/features/settings/data/services/memory_service.dart new file mode 100644 index 0000000..c36b75f --- /dev/null +++ b/apps/lib/features/settings/data/services/memory_service.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class MemoryItemModel { + final String id; + final IconData icon; + final String title; + final String subtitle; + + MemoryItemModel({ + required this.id, + required this.icon, + required this.title, + required this.subtitle, + }); +} + +class MockMemoryService { + static final MockMemoryService _instance = MockMemoryService._internal(); + factory MockMemoryService() => _instance; + + final List _items = []; + + MockMemoryService._internal(); + + List get items => List.unmodifiable(_items); + + List fetchMemoryItems() { + return items; + } +} + +class MemoryService { + final MockMemoryService _mock = MockMemoryService(); + + List getMemoryItems() { + return _mock.fetchMemoryItems(); + } +} diff --git a/apps/lib/features/settings/ui/screens/account_screen.dart b/apps/lib/features/settings/ui/screens/account_screen.dart index 36c258a..4be0391 100644 --- a/apps/lib/features/settings/ui/screens/account_screen.dart +++ b/apps/lib/features/settings/ui/screens/account_screen.dart @@ -5,6 +5,7 @@ import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/page_header.dart' as widgets; import '../../../auth/presentation/bloc/auth_bloc.dart'; import '../../../auth/presentation/bloc/auth_event.dart'; +import '../../../auth/presentation/bloc/auth_state.dart'; class AccountScreen extends StatelessWidget { const AccountScreen({super.key}); @@ -22,8 +23,6 @@ class AccountScreen extends StatelessWidget { padding: const EdgeInsets.all(20), child: Column( children: [ - _buildAccountInfo(), - const SizedBox(height: 16), _buildMenuCard(context), const SizedBox(height: 24), _buildLogoutButton(context), @@ -60,51 +59,6 @@ class AccountScreen extends StatelessWidget { ); } - Widget _buildAccountInfo() { - return Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: AppColors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.borderSecondary), - ), - child: Row( - children: [ - Container( - width: 56, - height: 56, - decoration: BoxDecoration( - color: AppColors.surfaceInfo, - borderRadius: BorderRadius.circular(28), - ), - child: const Icon(Icons.person, size: 28, color: AppColors.blue500), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Qiuzhiliang', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: AppColors.slate900, - ), - ), - const SizedBox(height: 4), - const Text( - 'qiuzhiliang@xunmee.com', - style: TextStyle(fontSize: 14, color: AppColors.slate500), - ), - ], - ), - ), - ], - ), - ); - } - Widget _buildMenuCard(BuildContext context) { return Container( decoration: BoxDecoration( @@ -114,14 +68,16 @@ class AccountScreen extends StatelessWidget { ), child: Column( children: [ - _buildMenuItem(icon: Icons.edit, title: '编辑资料', onTap: () {}), - _buildDivider(), - _buildMenuItem(icon: Icons.lock, title: '修改密码', onTap: () {}), + _buildMenuItem( + icon: Icons.edit, + title: '编辑资料', + onTap: () => context.push('/edit-profile'), + ), _buildDivider(), _buildMenuItem( - icon: Icons.swap_horiz, - title: '切换账户', - onTap: () => _showSwitchAccountDialog(context), + icon: Icons.lock, + title: '修改密码', + onTap: () => context.push('/change-password'), ), ], ), @@ -212,10 +168,16 @@ class AccountScreen extends StatelessWidget { child: const Text('取消'), ), TextButton( - onPressed: () { + onPressed: () async { Navigator.of(dialogContext).pop(); - context.read().add(AuthLoggedOut()); - context.go('/'); + final authBloc = context.read(); + authBloc.add(AuthLoggedOut()); + await authBloc.stream.firstWhere( + (state) => state is AuthUnauthenticated, + ); + if (context.mounted) { + context.go('/'); + } }, child: const Text('退出', style: TextStyle(color: Color(0xFFDC2626))), ), @@ -223,27 +185,4 @@ class AccountScreen extends StatelessWidget { ), ); } - - void _showSwitchAccountDialog(BuildContext context) { - showDialog( - context: context, - builder: (dialogContext) => AlertDialog( - title: const Text('切换账户'), - content: const Text('确定要切换到其他账户吗?'), - actions: [ - TextButton( - onPressed: () => Navigator.of(dialogContext).pop(), - child: const Text('取消'), - ), - TextButton( - onPressed: () { - Navigator.of(dialogContext).pop(); - context.go('/'); - }, - child: const Text('确定'), - ), - ], - ), - ); - } } diff --git a/apps/lib/features/settings/ui/screens/change_password_screen.dart b/apps/lib/features/settings/ui/screens/change_password_screen.dart new file mode 100644 index 0000000..66f2c05 --- /dev/null +++ b/apps/lib/features/settings/ui/screens/change_password_screen.dart @@ -0,0 +1,365 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:formz/formz.dart'; +import 'package:go_router/go_router.dart'; +import '../../../../core/theme/design_tokens.dart'; +import '../../../../core/di/injection.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/toast/toast.dart'; +import '../../../../shared/widgets/toast/toast_type.dart'; +import '../../../../shared/widgets/page_header.dart' as widgets; +import '../../../auth/presentation/bloc/auth_bloc.dart'; +import '../../../auth/presentation/bloc/auth_state.dart'; +import '../../../../features/auth/presentation/cubits/reset_password_cubit.dart'; +import '../../../../features/auth/data/auth_repository.dart'; + +class ChangePasswordScreen extends StatelessWidget { + const ChangePasswordScreen({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => ResetPasswordCubit(sl()), + child: const _ChangePasswordView(), + ); + } +} + +class _ChangePasswordView extends StatefulWidget { + const _ChangePasswordView(); + + @override + State<_ChangePasswordView> createState() => __ChangePasswordViewState(); +} + +class __ChangePasswordViewState extends State<_ChangePasswordView> { + final _codeController = TextEditingController(); + final _passwordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); + bool _obscurePassword = true; + bool _obscureConfirmPassword = true; + String _userEmail = ''; + + @override + void initState() { + super.initState(); + _loadUserEmail(); + } + + void _loadUserEmail() { + final authState = context.read().state; + if (authState is AuthAuthenticated) { + _userEmail = authState.user.email; + } + } + + @override + void dispose() { + _codeController.dispose(); + _passwordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + Future _handleSubmit() async { + final cubit = context.read(); + cubit.emailChanged(_userEmail); + cubit.codeChanged(_codeController.text); + cubit.newPasswordChanged(_passwordController.text); + cubit.confirmPasswordChanged(_confirmPasswordController.text); + + await cubit.submit(); + } + + @override + Widget build(BuildContext context) { + return BlocListener( + listenWhen: (previous, current) => + previous.status != current.status || + previous.errorMessage != current.errorMessage || + previous.codeSent != current.codeSent, + listener: (context, state) { + if (state.status == FormzSubmissionStatus.success && state.isSuccess) { + Toast.show(context, '密码修改成功', type: ToastType.success); + context.pop(); + } else if (state.status == FormzSubmissionStatus.success && + state.codeSent && + state.errorMessage == 'CODE_SENT_SUCCESS') { + Toast.show(context, '验证码已发送到您的邮箱', type: ToastType.success); + } else if (state.status == FormzSubmissionStatus.failure && + state.errorMessage != null && + state.errorMessage != '' && + state.errorMessage != 'CODE_SENT_SUCCESS') { + Toast.show(context, state.errorMessage!, type: ToastType.error); + } + }, + child: Scaffold( + backgroundColor: AppColors.surfaceSecondary, + body: SafeArea( + child: Column( + children: [ + SizedBox( + height: 64, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + const widgets.BackButton(), + const SizedBox(width: 12), + const Text( + '修改密码', + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w600, + color: AppColors.slate900, + ), + ), + ], + ), + ), + ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildEmailDisplay(), + const SizedBox(height: 24), + _buildForm(), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildEmailDisplay() { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderSecondary), + ), + child: Row( + children: [ + const Icon(Icons.email_outlined, size: 20, color: AppColors.slate500), + const SizedBox(width: 12), + Text( + _userEmail, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: AppColors.slate900, + ), + ), + ], + ), + ); + } + + Widget _buildForm() { + return BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildCodeInput(state.code.displayError != null, state), + const SizedBox(height: 16), + _buildPasswordInput(state.newPassword.displayError != null), + const SizedBox(height: 16), + _buildConfirmPasswordInput( + state.confirmPassword.displayError != null, + ), + const SizedBox(height: 32), + _buildSubmitButton(state), + ], + ); + }, + ); + } + + Widget _buildCodeInput(bool hasError, ResetPasswordState state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '验证码', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.slate700, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: TextField( + controller: _codeController, + keyboardType: TextInputType.number, + onChanged: (value) { + context.read().codeChanged(value); + }, + decoration: InputDecoration( + hintText: '请输入 6 位验证码', + errorText: hasError ? ' ' : null, + filled: true, + fillColor: AppColors.white, + ), + ), + ), + const SizedBox(width: 12), + SizedBox( + height: 48, + child: TextButton( + onPressed: + state.resendCountdown > 0 || + state.status == FormzSubmissionStatus.inProgress + ? null + : () { + if (state.codeSent) { + context.read().resendCode(); + } else { + context.read().emailChanged( + _userEmail, + ); + context.read().sendCode(); + } + }, + style: TextButton.styleFrom( + backgroundColor: state.codeSent + ? AppColors.background + : AppColors.primary, + foregroundColor: state.codeSent + ? AppColors.primary + : AppColors.primaryForeground, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadius.sm), + ), + padding: const EdgeInsets.symmetric(horizontal: 16), + ), + child: Text( + state.resendCountdown > 0 + ? '${state.resendCountdown}秒' + : (state.codeSent ? '重新发送' : '发送验证码'), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ], + ); + } + + Widget _buildPasswordInput(bool hasError) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '新密码', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.slate700, + ), + ), + const SizedBox(height: 8), + TextField( + controller: _passwordController, + obscureText: _obscurePassword, + onChanged: (value) { + context.read().newPasswordChanged(value); + }, + decoration: InputDecoration( + hintText: '请输入新密码(至少 6 位)', + errorText: hasError ? ' ' : null, + filled: true, + fillColor: AppColors.white, + suffixIcon: IconButton( + icon: Icon( + _obscurePassword ? Icons.visibility_off : Icons.visibility, + size: 20, + color: AppColors.slate400, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + ), + ), + ], + ); + } + + Widget _buildConfirmPasswordInput(bool hasError) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '确认密码', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.slate700, + ), + ), + const SizedBox(height: 8), + TextField( + controller: _confirmPasswordController, + obscureText: _obscureConfirmPassword, + onChanged: (value) { + context.read().confirmPasswordChanged(value); + }, + decoration: InputDecoration( + hintText: '请再次输入新密码', + errorText: hasError ? ' ' : null, + filled: true, + fillColor: AppColors.white, + suffixIcon: IconButton( + icon: Icon( + _obscureConfirmPassword + ? Icons.visibility_off + : Icons.visibility, + size: 20, + color: AppColors.slate400, + ), + onPressed: () { + setState(() { + _obscureConfirmPassword = !_obscureConfirmPassword; + }); + }, + ), + ), + ), + ], + ); + } + + Widget _buildSubmitButton(ResetPasswordState state) { + final isLoading = state.status == FormzSubmissionStatus.inProgress; + final isDisabled = isLoading || !state.codeSent; + + return SizedBox( + width: double.infinity, + height: 52, + child: AppButton( + text: '确认修改', + onPressed: isDisabled ? null : _handleSubmit, + isLoading: isLoading, + ), + ); + } +} diff --git a/apps/lib/features/settings/ui/screens/edit_profile_screen.dart b/apps/lib/features/settings/ui/screens/edit_profile_screen.dart new file mode 100644 index 0000000..e5f9ab6 --- /dev/null +++ b/apps/lib/features/settings/ui/screens/edit_profile_screen.dart @@ -0,0 +1,324 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import '../../../../core/theme/design_tokens.dart'; +import '../../../../core/di/injection.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/toast/toast.dart'; +import '../../../../shared/widgets/toast/toast_type.dart'; +import '../../../users/data/models/user_response.dart'; +import '../../../users/data/users_api.dart'; + +class EditProfileScreen extends StatefulWidget { + const EditProfileScreen({super.key}); + + @override + State createState() => _EditProfileScreenState(); +} + +class _EditProfileScreenState extends State { + final _usernameController = TextEditingController(); + final _bioController = TextEditingController(); + final _usersApi = sl(); + + UserResponse? _user; + bool _isLoading = true; + bool _isSaving = false; + bool _hasChanges = false; + + @override + void initState() { + super.initState(); + _loadUser(); + } + + Future _loadUser() async { + try { + final user = await _usersApi.getMe(); + if (mounted) { + setState(() { + _user = user; + _usernameController.text = user.username; + _bioController.text = user.bio ?? ''; + _isLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _isLoading = false; + }); + Toast.show(context, '加载用户信息失败', type: ToastType.error); + } + } + } + + void _onFieldChanged() { + if (_user == null) return; + final usernameChanged = _usernameController.text != _user!.username; + final bioChanged = _bioController.text != (_user!.bio ?? ''); + + if ((usernameChanged || bioChanged) != _hasChanges) { + setState(() { + _hasChanges = usernameChanged || bioChanged; + }); + } + } + + Future _saveProfile() async { + if (!_hasChanges || _user == null) return; + + final newUsername = _usernameController.text.trim(); + final newBio = _bioController.text.trim(); + + if (newUsername.isEmpty) { + Toast.show(context, '用户名不能为空', type: ToastType.warning); + return; + } + if (newUsername.length < 3 || newUsername.length > 30) { + Toast.show(context, '用户名需要3-30个字符', type: ToastType.warning); + return; + } + + setState(() { + _isSaving = true; + }); + + try { + final request = UserUpdateRequest( + username: newUsername, + bio: newBio.isEmpty ? null : newBio, + ); + await _usersApi.updateMe(request); + + if (mounted) { + Toast.show(context, '保存成功', type: ToastType.success); + context.pop(true); + } + } catch (e) { + if (mounted) { + Toast.show(context, '保存失败,请重试', type: ToastType.error); + } + } finally { + if (mounted) { + setState(() { + _isSaving = false; + }); + } + } + } + + @override + void dispose() { + _usernameController.dispose(); + _bioController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.surfaceSecondary, + body: SafeArea( + child: Column( + children: [ + _buildHeader(), + Expanded( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + _buildAvatarSection(), + const SizedBox(height: 24), + _buildFormSection(), + const SizedBox(height: 32), + SizedBox( + width: double.infinity, + height: 52, + child: AppButton( + text: '保存修改', + onPressed: _hasChanges && !_isSaving + ? _saveProfile + : null, + isLoading: _isSaving, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildHeader() { + return SizedBox( + height: 64, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + GestureDetector( + onTap: () => context.pop(), + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: const Color(0xFFF8FAFF), + borderRadius: BorderRadius.circular(18), + border: Border.all(color: const Color(0xFFDEE7F6)), + ), + child: const Icon( + Icons.chevron_left, + size: 18, + color: Color(0xFF334155), + ), + ), + ), + const SizedBox(width: 12), + const Text( + '编辑资料', + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w600, + color: AppColors.slate900, + ), + ), + ], + ), + ), + ); + } + + Widget _buildAvatarSection() { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.borderSecondary), + ), + child: Column( + children: [ + Container( + width: 72, + height: 72, + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFFEAF1FF), Color(0xFFF8FBFF)], + ), + borderRadius: BorderRadius.circular(36), + border: Border.all(color: const Color(0xFFD9E5FA)), + ), + child: const Icon(Icons.person, size: 36, color: AppColors.blue500), + ), + const SizedBox(height: 12), + Text( + '点击更换头像', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: AppColors.slate500, + ), + ), + ], + ), + ); + } + + Widget _buildFormSection() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.borderSecondary), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '用户名', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.slate700, + ), + ), + const SizedBox(height: 8), + TextField( + controller: _usernameController, + onChanged: (_) => _onFieldChanged(), + decoration: InputDecoration( + hintText: '请输入用户名', + filled: true, + fillColor: AppColors.surfaceSecondary, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide( + color: AppColors.blue500, + width: 1, + ), + ), + ), + ), + const SizedBox(height: 20), + const Text( + '个人简介', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.slate700, + ), + ), + const SizedBox(height: 8), + TextField( + controller: _bioController, + onChanged: (_) => _onFieldChanged(), + maxLines: 4, + maxLength: 200, + decoration: InputDecoration( + hintText: '介绍一下自己吧', + filled: true, + fillColor: AppColors.surfaceSecondary, + contentPadding: const EdgeInsets.all(16), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide( + color: AppColors.blue500, + width: 1, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/apps/lib/features/settings/ui/screens/memory_screen.dart b/apps/lib/features/settings/ui/screens/memory_screen.dart index 96233c1..de1846e 100644 --- a/apps/lib/features/settings/ui/screens/memory_screen.dart +++ b/apps/lib/features/settings/ui/screens/memory_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/page_header.dart' as widgets; +import '../../data/services/memory_service.dart'; class MemoryScreen extends StatefulWidget { const MemoryScreen({super.key}); @@ -11,12 +12,14 @@ class MemoryScreen extends StatefulWidget { class _MemoryScreenState extends State { bool _memoryEnabled = true; + final MemoryService _memoryService = MemoryService(); + late List _memoryItems; - final List _memoryItems = [ - MemoryItem(icon: Icons.language, title: '语言偏好', subtitle: '沟通时偏好使用中文'), - MemoryItem(icon: Icons.schedule, title: '工作时间', subtitle: '工作日 9:00-18:00'), - MemoryItem(icon: Icons.meeting_room, title: '会议习惯', subtitle: '偏好下午安排会议'), - ]; + @override + void initState() { + super.initState(); + _memoryItems = _memoryService.getMemoryItems(); + } @override Widget build(BuildContext context) { @@ -33,10 +36,12 @@ class _MemoryScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildToggleCard(), - const SizedBox(height: 14), - _buildListTitle(), - const SizedBox(height: 8), - _buildMemoryList(), + if (_memoryItems.isNotEmpty) ...[ + const SizedBox(height: 14), + _buildListTitle(), + const SizedBox(height: 8), + _buildMemoryList(), + ], const SizedBox(height: 20), _buildManageButton(), ], @@ -122,7 +127,7 @@ class _MemoryScreenState extends State { ); } - Widget _buildMemoryItem(MemoryItem item) { + Widget _buildMemoryItem(MemoryItemModel item) { return GestureDetector( onTap: () {}, child: Container( @@ -232,11 +237,3 @@ class _MemoryScreenState extends State { ); } } - -class MemoryItem { - final IconData icon; - final String title; - final String subtitle; - - MemoryItem({required this.icon, required this.title, required this.subtitle}); -} diff --git a/apps/lib/features/settings/ui/screens/settings_screen.dart b/apps/lib/features/settings/ui/screens/settings_screen.dart index 7b9737f..d7062ec 100644 --- a/apps/lib/features/settings/ui/screens/settings_screen.dart +++ b/apps/lib/features/settings/ui/screens/settings_screen.dart @@ -1,11 +1,47 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import '../../../../core/di/injection.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/page_header.dart' as widgets; +import '../../../users/data/models/user_response.dart'; +import '../../../users/data/users_api.dart'; -class SettingsScreen extends StatelessWidget { +class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); + @override + State createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + UserResponse? _user; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadUser(); + } + + Future _loadUser() async { + try { + final usersApi = sl(); + final user = await usersApi.getMe(); + if (mounted) { + setState(() { + _user = user; + _isLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -38,6 +74,22 @@ class SettingsScreen extends StatelessWidget { } Widget _buildProfileHero() { + if (_isLoading) { + return Container( + width: double.infinity, + height: 100, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(22), + ), + child: const Center(child: CircularProgressIndicator()), + ); + } + + final username = _user?.username ?? '未设置'; + final email = _user?.email ?? '未设置'; + return Container( width: double.infinity, padding: const EdgeInsets.all(20), @@ -83,9 +135,9 @@ class SettingsScreen extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - 'Qiuzhiliang', - style: TextStyle( + Text( + username, + style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w600, color: AppColors.slate900, @@ -113,9 +165,9 @@ class SettingsScreen extends StatelessWidget { ], ), const SizedBox(height: 4), - const Text( - 'qiuzhiliang@xunmee.com', - style: TextStyle( + Text( + email, + style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: AppColors.slate500, @@ -252,7 +304,7 @@ class SettingsScreen extends StatelessWidget { ), const SizedBox(height: 8), const Text( - '已用积分 320 / 1000', + '∞ / ∞', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, @@ -262,10 +314,10 @@ class SettingsScreen extends StatelessWidget { const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(999), - child: const LinearProgressIndicator( - value: 0.32, - backgroundColor: Color(0xFFE8EEF8), - valueColor: AlwaysStoppedAnimation(AppColors.blue400), + child: LinearProgressIndicator( + value: 0, + backgroundColor: const Color(0xFFE8EEF8), + valueColor: const AlwaysStoppedAnimation(AppColors.blue400), minHeight: 8, ), ), @@ -273,24 +325,21 @@ class SettingsScreen extends StatelessWidget { ), ), const SizedBox(width: 12), - GestureDetector( - onTap: () {}, - child: Container( - width: 72, - height: 32, - decoration: BoxDecoration( - color: AppColors.surfaceInfo, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.borderQuaternary), - ), - child: const Center( - child: Text( - '升级', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: AppColors.blue600, - ), + Container( + width: 72, + height: 32, + decoration: BoxDecoration( + color: const Color(0xFFE2E8F0), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFCBD5E1)), + ), + child: const Center( + child: Text( + '升级', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Color(0xFF94A3B8), ), ), ), @@ -309,16 +358,9 @@ class SettingsScreen extends StatelessWidget { ), child: Column( children: [ - _buildMenuItem( - icon: Icons.calendar_today, - title: '日历', - trailing: 'Toki', - onTap: () {}, - ), - _buildDivider(), _buildMenuItem( icon: Icons.notifications, - title: '日程通知', + title: '提醒设置', onTap: () {}, ), _buildDivider(), diff --git a/apps/lib/features/users/data/models/user_response.dart b/apps/lib/features/users/data/models/user_response.dart index fb23cc0..081de5b 100644 --- a/apps/lib/features/users/data/models/user_response.dart +++ b/apps/lib/features/users/data/models/user_response.dart @@ -1,12 +1,14 @@ class UserResponse { final String id; final String username; + final String? email; final String? avatarUrl; final String? bio; const UserResponse({ required this.id, required this.username, + this.email, this.avatarUrl, this.bio, }); @@ -15,6 +17,7 @@ class UserResponse { return UserResponse( id: json['id'] as String, username: json['username'] as String, + email: json['email'] as String?, avatarUrl: json['avatar_url'] as String?, bio: json['bio'] as String?, );