feat: 设置页面增强,支持用户信息展示和密码修改
This commit is contained in:
@@ -11,6 +11,7 @@ import '../../features/auth/data/auth_repository.dart';
|
|||||||
import '../../features/auth/data/auth_repository_impl.dart';
|
import '../../features/auth/data/auth_repository_impl.dart';
|
||||||
import '../../features/auth/presentation/bloc/auth_bloc.dart';
|
import '../../features/auth/presentation/bloc/auth_bloc.dart';
|
||||||
import '../../features/calendar/ui/calendar_state_manager.dart';
|
import '../../features/calendar/ui/calendar_state_manager.dart';
|
||||||
|
import '../../features/users/data/users_api.dart';
|
||||||
|
|
||||||
final sl = GetIt.instance;
|
final sl = GetIt.instance;
|
||||||
|
|
||||||
@@ -40,6 +41,9 @@ Future<void> configureDependencies() async {
|
|||||||
final authApi = AuthApi(apiClient);
|
final authApi = AuthApi(apiClient);
|
||||||
sl.registerSingleton<AuthApi>(authApi);
|
sl.registerSingleton<AuthApi>(authApi);
|
||||||
|
|
||||||
|
final usersApi = UsersApi(apiClient);
|
||||||
|
sl.registerSingleton<UsersApi>(usersApi);
|
||||||
|
|
||||||
final authRepository = AuthRepositoryImpl(
|
final authRepository = AuthRepositoryImpl(
|
||||||
api: authApi,
|
api: authApi,
|
||||||
tokenStorage: tokenStorage,
|
tokenStorage: tokenStorage,
|
||||||
|
|||||||
@@ -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/features_screen.dart';
|
||||||
import '../../features/settings/ui/screens/memory_screen.dart';
|
import '../../features/settings/ui/screens/memory_screen.dart';
|
||||||
import '../../features/settings/ui/screens/account_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 = [
|
final _protectedRoutes = [
|
||||||
'/home',
|
'/home',
|
||||||
@@ -34,6 +36,8 @@ final _protectedRoutes = [
|
|||||||
'/settings/features',
|
'/settings/features',
|
||||||
'/settings/memory',
|
'/settings/memory',
|
||||||
'/settings/account',
|
'/settings/account',
|
||||||
|
'/change-password',
|
||||||
|
'/edit-profile',
|
||||||
'/messages/invites',
|
'/messages/invites',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -138,6 +142,14 @@ GoRouter createAppRouter(AuthBloc authBloc) {
|
|||||||
path: '/settings/account',
|
path: '/settings/account',
|
||||||
builder: (context, state) => const AccountScreen(),
|
builder: (context, state) => const AccountScreen(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/change-password',
|
||||||
|
builder: (context, state) => const ChangePasswordScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/edit-profile',
|
||||||
|
builder: (context, state) => const EditProfileScreen(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<MemoryItemModel> _items = [];
|
||||||
|
|
||||||
|
MockMemoryService._internal();
|
||||||
|
|
||||||
|
List<MemoryItemModel> get items => List.unmodifiable(_items);
|
||||||
|
|
||||||
|
List<MemoryItemModel> fetchMemoryItems() {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MemoryService {
|
||||||
|
final MockMemoryService _mock = MockMemoryService();
|
||||||
|
|
||||||
|
List<MemoryItemModel> getMemoryItems() {
|
||||||
|
return _mock.fetchMemoryItems();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import '../../../../core/theme/design_tokens.dart';
|
|||||||
import '../../../../shared/widgets/page_header.dart' as widgets;
|
import '../../../../shared/widgets/page_header.dart' as widgets;
|
||||||
import '../../../auth/presentation/bloc/auth_bloc.dart';
|
import '../../../auth/presentation/bloc/auth_bloc.dart';
|
||||||
import '../../../auth/presentation/bloc/auth_event.dart';
|
import '../../../auth/presentation/bloc/auth_event.dart';
|
||||||
|
import '../../../auth/presentation/bloc/auth_state.dart';
|
||||||
|
|
||||||
class AccountScreen extends StatelessWidget {
|
class AccountScreen extends StatelessWidget {
|
||||||
const AccountScreen({super.key});
|
const AccountScreen({super.key});
|
||||||
@@ -22,8 +23,6 @@ class AccountScreen extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildAccountInfo(),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_buildMenuCard(context),
|
_buildMenuCard(context),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_buildLogoutButton(context),
|
_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) {
|
Widget _buildMenuCard(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -114,14 +68,16 @@ class AccountScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildMenuItem(icon: Icons.edit, title: '编辑资料', onTap: () {}),
|
_buildMenuItem(
|
||||||
_buildDivider(),
|
icon: Icons.edit,
|
||||||
_buildMenuItem(icon: Icons.lock, title: '修改密码', onTap: () {}),
|
title: '编辑资料',
|
||||||
|
onTap: () => context.push('/edit-profile'),
|
||||||
|
),
|
||||||
_buildDivider(),
|
_buildDivider(),
|
||||||
_buildMenuItem(
|
_buildMenuItem(
|
||||||
icon: Icons.swap_horiz,
|
icon: Icons.lock,
|
||||||
title: '切换账户',
|
title: '修改密码',
|
||||||
onTap: () => _showSwitchAccountDialog(context),
|
onTap: () => context.push('/change-password'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -212,10 +168,16 @@ class AccountScreen extends StatelessWidget {
|
|||||||
child: const Text('取消'),
|
child: const Text('取消'),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
Navigator.of(dialogContext).pop();
|
Navigator.of(dialogContext).pop();
|
||||||
context.read<AuthBloc>().add(AuthLoggedOut());
|
final authBloc = context.read<AuthBloc>();
|
||||||
|
authBloc.add(AuthLoggedOut());
|
||||||
|
await authBloc.stream.firstWhere(
|
||||||
|
(state) => state is AuthUnauthenticated,
|
||||||
|
);
|
||||||
|
if (context.mounted) {
|
||||||
context.go('/');
|
context.go('/');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: const Text('退出', style: TextStyle(color: Color(0xFFDC2626))),
|
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('确定'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<AuthRepository>()),
|
||||||
|
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<AuthBloc>().state;
|
||||||
|
if (authState is AuthAuthenticated) {
|
||||||
|
_userEmail = authState.user.email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_codeController.dispose();
|
||||||
|
_passwordController.dispose();
|
||||||
|
_confirmPasswordController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleSubmit() async {
|
||||||
|
final cubit = context.read<ResetPasswordCubit>();
|
||||||
|
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<ResetPasswordCubit, ResetPasswordState>(
|
||||||
|
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<ResetPasswordCubit, ResetPasswordState>(
|
||||||
|
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<ResetPasswordCubit>().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<ResetPasswordCubit>().resendCode();
|
||||||
|
} else {
|
||||||
|
context.read<ResetPasswordCubit>().emailChanged(
|
||||||
|
_userEmail,
|
||||||
|
);
|
||||||
|
context.read<ResetPasswordCubit>().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<ResetPasswordCubit>().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<ResetPasswordCubit>().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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<EditProfileScreen> createState() => _EditProfileScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||||
|
final _usernameController = TextEditingController();
|
||||||
|
final _bioController = TextEditingController();
|
||||||
|
final _usersApi = sl<UsersApi>();
|
||||||
|
|
||||||
|
UserResponse? _user;
|
||||||
|
bool _isLoading = true;
|
||||||
|
bool _isSaving = false;
|
||||||
|
bool _hasChanges = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _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<void> _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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../../../../core/theme/design_tokens.dart';
|
import '../../../../core/theme/design_tokens.dart';
|
||||||
import '../../../../shared/widgets/page_header.dart' as widgets;
|
import '../../../../shared/widgets/page_header.dart' as widgets;
|
||||||
|
import '../../data/services/memory_service.dart';
|
||||||
|
|
||||||
class MemoryScreen extends StatefulWidget {
|
class MemoryScreen extends StatefulWidget {
|
||||||
const MemoryScreen({super.key});
|
const MemoryScreen({super.key});
|
||||||
@@ -11,12 +12,14 @@ class MemoryScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _MemoryScreenState extends State<MemoryScreen> {
|
class _MemoryScreenState extends State<MemoryScreen> {
|
||||||
bool _memoryEnabled = true;
|
bool _memoryEnabled = true;
|
||||||
|
final MemoryService _memoryService = MemoryService();
|
||||||
|
late List<MemoryItemModel> _memoryItems;
|
||||||
|
|
||||||
final List<MemoryItem> _memoryItems = [
|
@override
|
||||||
MemoryItem(icon: Icons.language, title: '语言偏好', subtitle: '沟通时偏好使用中文'),
|
void initState() {
|
||||||
MemoryItem(icon: Icons.schedule, title: '工作时间', subtitle: '工作日 9:00-18:00'),
|
super.initState();
|
||||||
MemoryItem(icon: Icons.meeting_room, title: '会议习惯', subtitle: '偏好下午安排会议'),
|
_memoryItems = _memoryService.getMemoryItems();
|
||||||
];
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -33,10 +36,12 @@ class _MemoryScreenState extends State<MemoryScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildToggleCard(),
|
_buildToggleCard(),
|
||||||
|
if (_memoryItems.isNotEmpty) ...[
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
_buildListTitle(),
|
_buildListTitle(),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildMemoryList(),
|
_buildMemoryList(),
|
||||||
|
],
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildManageButton(),
|
_buildManageButton(),
|
||||||
],
|
],
|
||||||
@@ -122,7 +127,7 @@ class _MemoryScreenState extends State<MemoryScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMemoryItem(MemoryItem item) {
|
Widget _buildMemoryItem(MemoryItemModel item) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -232,11 +237,3 @@ class _MemoryScreenState extends State<MemoryScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MemoryItem {
|
|
||||||
final IconData icon;
|
|
||||||
final String title;
|
|
||||||
final String subtitle;
|
|
||||||
|
|
||||||
MemoryItem({required this.icon, required this.title, required this.subtitle});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,47 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import '../../../../core/di/injection.dart';
|
||||||
import '../../../../core/theme/design_tokens.dart';
|
import '../../../../core/theme/design_tokens.dart';
|
||||||
import '../../../../shared/widgets/page_header.dart' as widgets;
|
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});
|
const SettingsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
|
UserResponse? _user;
|
||||||
|
bool _isLoading = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadUser() async {
|
||||||
|
try {
|
||||||
|
final usersApi = sl<UsersApi>();
|
||||||
|
final user = await usersApi.getMe();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_user = user;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -38,6 +74,22 @@ class SettingsScreen extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildProfileHero() {
|
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(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
@@ -83,9 +135,9 @@ class SettingsScreen extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'Qiuzhiliang',
|
username,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: AppColors.slate900,
|
color: AppColors.slate900,
|
||||||
@@ -113,9 +165,9 @@ class SettingsScreen extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
const Text(
|
Text(
|
||||||
'qiuzhiliang@xunmee.com',
|
email,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: AppColors.slate500,
|
color: AppColors.slate500,
|
||||||
@@ -252,7 +304,7 @@ class SettingsScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Text(
|
const Text(
|
||||||
'已用积分 320 / 1000',
|
'∞ / ∞',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
@@ -262,10 +314,10 @@ class SettingsScreen extends StatelessWidget {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(999),
|
borderRadius: BorderRadius.circular(999),
|
||||||
child: const LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: 0.32,
|
value: 0,
|
||||||
backgroundColor: Color(0xFFE8EEF8),
|
backgroundColor: const Color(0xFFE8EEF8),
|
||||||
valueColor: AlwaysStoppedAnimation(AppColors.blue400),
|
valueColor: const AlwaysStoppedAnimation(AppColors.blue400),
|
||||||
minHeight: 8,
|
minHeight: 8,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -273,15 +325,13 @@ class SettingsScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
GestureDetector(
|
Container(
|
||||||
onTap: () {},
|
|
||||||
child: Container(
|
|
||||||
width: 72,
|
width: 72,
|
||||||
height: 32,
|
height: 32,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.surfaceInfo,
|
color: const Color(0xFFE2E8F0),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(color: AppColors.borderQuaternary),
|
border: Border.all(color: const Color(0xFFCBD5E1)),
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: const Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -289,8 +339,7 @@ class SettingsScreen extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: AppColors.blue600,
|
color: Color(0xFF94A3B8),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -309,16 +358,9 @@ class SettingsScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildMenuItem(
|
|
||||||
icon: Icons.calendar_today,
|
|
||||||
title: '日历',
|
|
||||||
trailing: 'Toki',
|
|
||||||
onTap: () {},
|
|
||||||
),
|
|
||||||
_buildDivider(),
|
|
||||||
_buildMenuItem(
|
_buildMenuItem(
|
||||||
icon: Icons.notifications,
|
icon: Icons.notifications,
|
||||||
title: '日程通知',
|
title: '提醒设置',
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
),
|
),
|
||||||
_buildDivider(),
|
_buildDivider(),
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
class UserResponse {
|
class UserResponse {
|
||||||
final String id;
|
final String id;
|
||||||
final String username;
|
final String username;
|
||||||
|
final String? email;
|
||||||
final String? avatarUrl;
|
final String? avatarUrl;
|
||||||
final String? bio;
|
final String? bio;
|
||||||
|
|
||||||
const UserResponse({
|
const UserResponse({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.username,
|
required this.username,
|
||||||
|
this.email,
|
||||||
this.avatarUrl,
|
this.avatarUrl,
|
||||||
this.bio,
|
this.bio,
|
||||||
});
|
});
|
||||||
@@ -15,6 +17,7 @@ class UserResponse {
|
|||||||
return UserResponse(
|
return UserResponse(
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
username: json['username'] as String,
|
username: json['username'] as String,
|
||||||
|
email: json['email'] as String?,
|
||||||
avatarUrl: json['avatar_url'] as String?,
|
avatarUrl: json['avatar_url'] as String?,
|
||||||
bio: json['bio'] as String?,
|
bio: json['bio'] as String?,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user