feat(apps): update UI screens and shared components
- Update home screen with new composer and interactions - Update settings screens with new profile flow - Update calendar share dialog - Update contacts screen - Add new shared widgets: confirm_sheet, phone_prefix_selector - Add new formatters: phone_display_formatter - Update tests for modified components
This commit is contained in:
@@ -49,12 +49,6 @@ class AccountScreen extends StatelessWidget {
|
||||
onTap: () => context.push('/edit-profile'),
|
||||
),
|
||||
_buildDivider(),
|
||||
_buildMenuItem(
|
||||
icon: Icons.lock,
|
||||
title: '修改密码',
|
||||
onTap: () => context.push('/change-password'),
|
||||
),
|
||||
_buildDivider(),
|
||||
_buildMenuItem(
|
||||
icon: Icons.logout,
|
||||
title: '退出登录',
|
||||
|
||||
@@ -1,368 +0,0 @@
|
||||
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/fixed_length_code_input.dart';
|
||||
import '../../../../shared/widgets/toast/toast.dart';
|
||||
import '../../../../shared/widgets/toast/toast_type.dart';
|
||||
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';
|
||||
import '../widgets/account_section_card.dart';
|
||||
import '../widgets/settings_page_scaffold.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 _resolveUserEmail() {
|
||||
final authState = context.read<AuthBloc>().state;
|
||||
if (authState is AuthAuthenticated) {
|
||||
return authState.user.email;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_codeController.dispose();
|
||||
_passwordController.dispose();
|
||||
_confirmPasswordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _handleSubmit() async {
|
||||
final email = _resolveUserEmail();
|
||||
if (email.isEmpty) {
|
||||
Toast.show(context, '未读取到登录邮箱,请重新登录后重试', type: ToastType.warning);
|
||||
return;
|
||||
}
|
||||
|
||||
final cubit = context.read<ResetPasswordCubit>();
|
||||
cubit.emailChanged(email);
|
||||
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: SettingsPageScaffold(
|
||||
title: '修改密码',
|
||||
onBack: () => context.pop(),
|
||||
body: _buildForm(),
|
||||
footer: BlocBuilder<ResetPasswordCubit, ResetPasswordState>(
|
||||
builder: (context, state) {
|
||||
return _buildSubmitButton(state);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildForm() {
|
||||
return BlocBuilder<ResetPasswordCubit, ResetPasswordState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildEmailSection(state, _resolveUserEmail()),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildPasswordSection(
|
||||
state,
|
||||
state.newPassword.displayError != null,
|
||||
state.confirmPassword.displayError != null,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmailSection(ResetPasswordState state, String userEmail) {
|
||||
return AccountSectionCard(
|
||||
title: '发送验证码',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.md,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceInfoLight,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
border: Border.all(color: AppColors.borderTertiary),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.email_outlined,
|
||||
size: 20,
|
||||
color: AppColors.blue600,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(
|
||||
child: Text(
|
||||
userEmail.isEmpty ? '未读取到登录邮箱' : userEmail,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
AppButton(
|
||||
text: state.resendCountdown > 0
|
||||
? '${state.resendCountdown} 秒后可重发'
|
||||
: (state.codeSent ? '重新发送验证码' : '发送验证码'),
|
||||
onPressed:
|
||||
state.resendCountdown > 0 ||
|
||||
state.status == FormzSubmissionStatus.inProgress
|
||||
? null
|
||||
: () {
|
||||
if (userEmail.isEmpty) {
|
||||
Toast.show(
|
||||
context,
|
||||
'未读取到登录邮箱,请重新登录后重试',
|
||||
type: ToastType.warning,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (state.codeSent) {
|
||||
context.read<ResetPasswordCubit>().resendCode();
|
||||
} else {
|
||||
context.read<ResetPasswordCubit>().emailChanged(
|
||||
userEmail,
|
||||
);
|
||||
context.read<ResetPasswordCubit>().sendCode();
|
||||
}
|
||||
},
|
||||
isOutlined: state.codeSent,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPasswordSection(
|
||||
ResetPasswordState state,
|
||||
bool passwordHasError,
|
||||
bool confirmHasError,
|
||||
) {
|
||||
if (!state.codeSent) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return AccountSectionCard(
|
||||
title: '设置新密码',
|
||||
backgroundColor: AppColors.white,
|
||||
borderColor: AppColors.borderSecondary,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'验证码',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.slate700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
FixedLengthCodeInput(
|
||||
controller: _codeController,
|
||||
length: 6,
|
||||
semanticLabel: '修改密码验证码输入框',
|
||||
keyboardType: TextInputType.number,
|
||||
allowedCharacters: const {
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
},
|
||||
onChanged: (value) {
|
||||
context.read<ResetPasswordCubit>().codeChanged(value);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildPasswordInput(passwordHasError),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildConfirmPasswordInput(confirmHasError),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPasswordInput(bool hasError) {
|
||||
return _buildPasswordField(
|
||||
label: '新密码',
|
||||
controller: _passwordController,
|
||||
hintText: '请输入新密码(至少 6 位)',
|
||||
hasError: hasError,
|
||||
isObscured: _obscurePassword,
|
||||
onToggleVisibility: () =>
|
||||
setState(() => _obscurePassword = !_obscurePassword),
|
||||
onChanged: (value) =>
|
||||
context.read<ResetPasswordCubit>().newPasswordChanged(value),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConfirmPasswordInput(bool hasError) {
|
||||
return _buildPasswordField(
|
||||
label: '确认密码',
|
||||
controller: _confirmPasswordController,
|
||||
hintText: '请再次输入新密码',
|
||||
hasError: hasError,
|
||||
isObscured: _obscureConfirmPassword,
|
||||
onToggleVisibility: () =>
|
||||
setState(() => _obscureConfirmPassword = !_obscureConfirmPassword),
|
||||
onChanged: (value) =>
|
||||
context.read<ResetPasswordCubit>().confirmPasswordChanged(value),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPasswordField({
|
||||
required String label,
|
||||
required TextEditingController controller,
|
||||
required String hintText,
|
||||
required bool hasError,
|
||||
required bool isObscured,
|
||||
required VoidCallback onToggleVisibility,
|
||||
required ValueChanged<String> onChanged,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.slate700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
TextField(
|
||||
controller: controller,
|
||||
obscureText: isObscured,
|
||||
onChanged: onChanged,
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
errorText: hasError ? ' ' : null,
|
||||
filled: true,
|
||||
fillColor: AppColors.surfaceSecondary,
|
||||
hintStyle: const TextStyle(color: AppColors.slate400),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.lg,
|
||||
vertical: AppSpacing.lg,
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
isObscured ? Icons.visibility_off : Icons.visibility,
|
||||
size: 20,
|
||||
color: AppColors.slate400,
|
||||
),
|
||||
onPressed: onToggleVisibility,
|
||||
),
|
||||
border: _inputBorder,
|
||||
enabledBorder: _enabledBorder,
|
||||
focusedBorder: _focusedBorder,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
static final _inputBorder = OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
borderSide: BorderSide.none,
|
||||
);
|
||||
|
||||
static final _enabledBorder = OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
borderSide: const BorderSide(color: AppColors.borderTertiary),
|
||||
);
|
||||
|
||||
static final _focusedBorder = OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
borderSide: const BorderSide(color: AppColors.blue500),
|
||||
);
|
||||
|
||||
Widget _buildSubmitButton(ResetPasswordState state) {
|
||||
final isLoading = state.status == FormzSubmissionStatus.inProgress;
|
||||
final isDisabled = isLoading || !state.codeSent || !state.canSubmit;
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: AppButton(
|
||||
text: '确认修改',
|
||||
onPressed: isDisabled ? null : _handleSubmit,
|
||||
isLoading: isLoading,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -122,6 +122,8 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
return SettingsPageScaffold(
|
||||
title: '编辑资料',
|
||||
onBack: () => context.pop(),
|
||||
resizeOnKeyboard: false,
|
||||
maintainBottomViewPadding: true,
|
||||
body: _isLoading
|
||||
? const Center(
|
||||
child: AppLoadingIndicator(variant: AppLoadingVariant.surface),
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:social_app/core/theme/design_tokens.dart';
|
||||
import 'package:social_app/shared/widgets/app_loading_indicator.dart';
|
||||
import 'package:social_app/shared/widgets/toast/toast.dart';
|
||||
import 'package:social_app/shared/widgets/toast/toast_type.dart';
|
||||
import 'package:social_app/shared/utils/phone_display_formatter.dart';
|
||||
import 'package:social_app/features/friends/data/friends_api.dart';
|
||||
import 'package:social_app/features/settings/data/settings_api.dart';
|
||||
import 'package:social_app/features/users/data/models/user_response.dart';
|
||||
@@ -98,7 +99,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
}
|
||||
|
||||
final username = _user?.username ?? '未设置';
|
||||
final email = _user?.email ?? '未设置';
|
||||
final phone = _user?.phone == null
|
||||
? '未设置'
|
||||
: formatPhoneForDisplay(_user?.phone);
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
@@ -195,7 +198,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
email,
|
||||
phone,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
|
||||
Reference in New Issue
Block a user