feat: 优化 Agent 运行时与聊天设置体验
This commit is contained in:
@@ -2,71 +2,104 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
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';
|
||||
import '../widgets/account_section_card.dart';
|
||||
import '../widgets/account_surface_scaffold.dart';
|
||||
|
||||
class AccountScreen extends StatelessWidget {
|
||||
const AccountScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.surfaceSecondary,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(context),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildMenuCard(context),
|
||||
const SizedBox(height: 24),
|
||||
_buildLogoutButton(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
return AccountSurfaceScaffold(
|
||||
title: '我的账户',
|
||||
subtitle: '管理资料信息与账户安全',
|
||||
onBack: () => context.pop(),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildProfileHero(context),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildMenuCard(context),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildSecurityCard(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 64,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
widgets.BackButton(),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
'我的账户',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate900,
|
||||
Widget _buildProfileHero(BuildContext context) {
|
||||
final authState = context.watch<AuthBloc>().state;
|
||||
final email = authState is AuthAuthenticated ? authState.user.email : '';
|
||||
final identity = email.isEmpty ? '当前登录账户' : email;
|
||||
final badge = email.isEmpty ? 'A' : email.characters.first.toUpperCase();
|
||||
|
||||
return AccountSectionCard(
|
||||
title: '账户概览',
|
||||
description: '查看当前账户状态与基础身份信息',
|
||||
backgroundColor: AppColors.surfaceInfoLight,
|
||||
borderColor: AppColors.borderQuaternary,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
border: Border.all(color: AppColors.borderQuaternary),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
badge,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.blue600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
identity,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
const Text(
|
||||
'账户状态正常,可安全管理资料与密码',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMenuCard(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
),
|
||||
return AccountSectionCard(
|
||||
title: '账户信息',
|
||||
description: '编辑公开资料和登录信息',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildMenuItem(
|
||||
icon: Icons.edit,
|
||||
@@ -84,6 +117,52 @@ class AccountScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSecurityCard(BuildContext context) {
|
||||
return AccountSectionCard(
|
||||
title: '安全与会话',
|
||||
description: '若为公共设备,请及时退出登录',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceSecondary,
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
),
|
||||
child: const Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.shield_outlined,
|
||||
size: 16,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
SizedBox(width: AppSpacing.xs),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'退出后需要重新登录才能继续使用。',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.slate500,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildLogoutButton(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMenuItem({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
@@ -93,15 +172,17 @@ class AccountScreen extends StatelessWidget {
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
height: 56,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
height: AppSpacing.xxl,
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 20, color: AppColors.slate500),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
@@ -126,8 +207,8 @@ class AccountScreen extends StatelessWidget {
|
||||
Widget _buildDivider() {
|
||||
return Container(
|
||||
height: 1,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
color: const Color(0xFFEEF2F7),
|
||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
color: AppColors.borderTertiary,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -138,9 +219,9 @@ class AccountScreen extends StatelessWidget {
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFEE2E2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFFFECACA)),
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
border: Border.all(color: AppColors.feedbackErrorBorder),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
@@ -148,7 +229,7 @@ class AccountScreen extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFFDC2626),
|
||||
color: AppColors.feedbackErrorText,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -179,7 +260,10 @@ class AccountScreen extends StatelessWidget {
|
||||
context.go('/');
|
||||
}
|
||||
},
|
||||
child: const Text('退出', style: TextStyle(color: Color(0xFFDC2626))),
|
||||
child: const Text(
|
||||
'退出',
|
||||
style: TextStyle(color: AppColors.feedbackErrorText),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -5,14 +5,16 @@ 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/banner/app_banner.dart';
|
||||
import '../../../../shared/widgets/fixed_length_code_input.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';
|
||||
import '../widgets/account_section_card.dart';
|
||||
import '../widgets/account_surface_scaffold.dart';
|
||||
|
||||
class ChangePasswordScreen extends StatelessWidget {
|
||||
const ChangePasswordScreen({super.key});
|
||||
@@ -39,19 +41,13 @@ class __ChangePasswordViewState extends State<_ChangePasswordView> {
|
||||
final _confirmPasswordController = TextEditingController();
|
||||
bool _obscurePassword = true;
|
||||
bool _obscureConfirmPassword = true;
|
||||
String _userEmail = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadUserEmail();
|
||||
}
|
||||
|
||||
void _loadUserEmail() {
|
||||
String _resolveUserEmail() {
|
||||
final authState = context.read<AuthBloc>().state;
|
||||
if (authState is AuthAuthenticated) {
|
||||
_userEmail = authState.user.email;
|
||||
return authState.user.email;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -63,8 +59,14 @@ class __ChangePasswordViewState extends State<_ChangePasswordView> {
|
||||
}
|
||||
|
||||
Future<void> _handleSubmit() async {
|
||||
final email = _resolveUserEmail();
|
||||
if (email.isEmpty) {
|
||||
Toast.show(context, '未读取到登录邮箱,请重新登录后重试', type: ToastType.warning);
|
||||
return;
|
||||
}
|
||||
|
||||
final cubit = context.read<ResetPasswordCubit>();
|
||||
cubit.emailChanged(_userEmail);
|
||||
cubit.emailChanged(email);
|
||||
cubit.codeChanged(_codeController.text);
|
||||
cubit.newPasswordChanged(_passwordController.text);
|
||||
cubit.confirmPasswordChanged(_confirmPasswordController.text);
|
||||
@@ -94,281 +96,307 @@ class __ChangePasswordViewState extends State<_ChangePasswordView> {
|
||||
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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: AccountSurfaceScaffold(
|
||||
title: '修改密码',
|
||||
subtitle: '通过邮箱验证码安全更新你的登录密码',
|
||||
onBack: () => context.pop(),
|
||||
body: _buildForm(),
|
||||
footer: BlocBuilder<ResetPasswordCubit, ResetPasswordState>(
|
||||
builder: (context, state) {
|
||||
return _buildSubmitButton(state);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
const SizedBox(height: 16),
|
||||
_buildPasswordInput(state.newPassword.displayError != null),
|
||||
const SizedBox(height: 16),
|
||||
_buildConfirmPasswordInput(
|
||||
_buildEmailSection(state, _resolveUserEmail()),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildPasswordSection(
|
||||
state,
|
||||
state.newPassword.displayError != null,
|
||||
state.confirmPassword.displayError != null,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
_buildSubmitButton(state),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCodeInput(ResetPasswordState state) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'验证码',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.slate700,
|
||||
Widget _buildEmailSection(ResetPasswordState state, String userEmail) {
|
||||
return AccountSectionCard(
|
||||
title: '第 1 步:验证邮箱',
|
||||
description: '先向登录邮箱发送验证码,再进行密码设置',
|
||||
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: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: 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),
|
||||
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,
|
||||
) {
|
||||
return AccountSectionCard(
|
||||
title: '第 2 步:输入验证码并设置新密码',
|
||||
description: '验证码有效后,确认新密码即可完成修改',
|
||||
backgroundColor: state.codeSent
|
||||
? AppColors.white
|
||||
: AppColors.surfaceTertiary,
|
||||
borderColor: state.codeSent
|
||||
? AppColors.borderSecondary
|
||||
: AppColors.borderTertiary,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!state.codeSent)
|
||||
const AppBanner(
|
||||
title: '请先发送验证码',
|
||||
message: '完成邮箱验证后,可继续设置新密码。',
|
||||
type: ToastType.info,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (state.codeSent)
|
||||
AppBanner(
|
||||
title: '验证码已发送',
|
||||
message: state.resendCountdown > 0
|
||||
? '如未收到,可在 ${state.resendCountdown} 秒后重新发送。'
|
||||
: '若未收到邮件,可重新发送验证码。',
|
||||
type: ToastType.info,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
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 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;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
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: [
|
||||
const Text(
|
||||
'确认密码',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.slate700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
TextField(
|
||||
controller: _confirmPasswordController,
|
||||
obscureText: _obscureConfirmPassword,
|
||||
onChanged: (value) {
|
||||
context.read<ResetPasswordCubit>().confirmPasswordChanged(value);
|
||||
},
|
||||
controller: controller,
|
||||
obscureText: isObscured,
|
||||
onChanged: onChanged,
|
||||
decoration: InputDecoration(
|
||||
hintText: '请再次输入新密码',
|
||||
hintText: hintText,
|
||||
errorText: hasError ? ' ' : null,
|
||||
filled: true,
|
||||
fillColor: AppColors.white,
|
||||
fillColor: AppColors.surfaceSecondary,
|
||||
hintStyle: const TextStyle(color: AppColors.slate400),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.lg,
|
||||
vertical: AppSpacing.lg,
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscureConfirmPassword
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility,
|
||||
isObscured ? Icons.visibility_off : Icons.visibility,
|
||||
size: 20,
|
||||
color: AppColors.slate400,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_obscureConfirmPassword = !_obscureConfirmPassword;
|
||||
});
|
||||
},
|
||||
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;
|
||||
final isDisabled = isLoading || !state.canSubmit;
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: AppButton(
|
||||
text: '确认修改',
|
||||
onPressed: isDisabled ? null : _handleSubmit,
|
||||
isLoading: isLoading,
|
||||
),
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (!state.codeSent)
|
||||
const Text(
|
||||
'完成验证码验证后可提交密码修改',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
if (!state.codeSent) const SizedBox(height: AppSpacing.sm),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: AppButton(
|
||||
text: '确认修改',
|
||||
onPressed: isDisabled ? null : _handleSubmit,
|
||||
isLoading: isLoading,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@ import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../shared/widgets/app_button.dart';
|
||||
import '../../../../shared/widgets/app_loading_indicator.dart';
|
||||
import '../../../../shared/widgets/page_header.dart' as widgets;
|
||||
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';
|
||||
import '../widgets/account_section_card.dart';
|
||||
import '../widgets/account_surface_scaffold.dart';
|
||||
|
||||
class EditProfileScreen extends StatefulWidget {
|
||||
const EditProfileScreen({super.key});
|
||||
@@ -118,193 +119,187 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.surfaceSecondary,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
Expanded(
|
||||
child: _isLoading
|
||||
? const Center(child: AppLoadingIndicator(size: 22))
|
||||
: 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
return AccountSurfaceScaffold(
|
||||
title: '编辑资料',
|
||||
subtitle: '完善公开信息,让好友更容易认识你',
|
||||
onBack: () => context.pop(),
|
||||
body: _isLoading
|
||||
? const Center(
|
||||
child: AppLoadingIndicator(variant: AppLoadingVariant.surface),
|
||||
)
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildProfileSummarySection(),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildBasicInfoSection(),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildBioSection(),
|
||||
],
|
||||
),
|
||||
],
|
||||
footer: 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: [
|
||||
widgets.BackButton(onPressed: () => context.pop()),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
'编辑资料',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Widget _buildProfileSummarySection() {
|
||||
final username = _user?.username ?? '未设置用户名';
|
||||
final email = _user?.email;
|
||||
|
||||
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),
|
||||
),
|
||||
return AccountSectionCard(
|
||||
title: '资料概览',
|
||||
description: '展示你的公开身份信息',
|
||||
backgroundColor: AppColors.white,
|
||||
borderColor: AppColors.borderSecondary,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFFEAF1FF), Color(0xFFF8FBFF)],
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [AppColors.blue100, AppColors.surfaceInfoLight],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
border: Border.all(color: AppColors.borderQuaternary),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
size: 28,
|
||||
color: AppColors.blue600,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.lg),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
username,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
email ?? '邮箱未绑定',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
),
|
||||
Widget _buildBasicInfoSection() {
|
||||
return AccountSectionCard(
|
||||
title: '基础信息',
|
||||
description: '用户名会在个人资料和社交场景中展示',
|
||||
backgroundColor: AppColors.white,
|
||||
borderColor: AppColors.borderSecondary,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'用户名',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.slate700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
style: const TextStyle(fontSize: 15, color: AppColors.slate900),
|
||||
decoration: _buildInputDecoration('请输入用户名'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBioSection() {
|
||||
return AccountSectionCard(
|
||||
title: '个人简介',
|
||||
description: '一句话介绍自己,帮助他人快速了解你',
|
||||
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),
|
||||
TextField(
|
||||
controller: _bioController,
|
||||
onChanged: (_) => _onFieldChanged(),
|
||||
maxLines: 4,
|
||||
maxLength: 200,
|
||||
style: const TextStyle(fontSize: 15, color: AppColors.slate900),
|
||||
decoration: _buildInputDecoration(
|
||||
'介绍一下自己吧',
|
||||
).copyWith(contentPadding: const EdgeInsets.all(AppSpacing.lg)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
InputDecoration _buildInputDecoration(String hintText) {
|
||||
return InputDecoration(
|
||||
hintText: hintText,
|
||||
hintStyle: const TextStyle(fontSize: 14, color: AppColors.slate400),
|
||||
filled: true,
|
||||
fillColor: AppColors.surfaceSecondary,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.lg,
|
||||
vertical: AppSpacing.lg,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
borderSide: const BorderSide(color: AppColors.borderTertiary),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
borderSide: const BorderSide(color: AppColors.blue500),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
|
||||
class AccountSectionCard extends StatelessWidget {
|
||||
const AccountSectionCard({
|
||||
super.key,
|
||||
this.title,
|
||||
this.description,
|
||||
required this.child,
|
||||
this.backgroundColor = AppColors.white,
|
||||
this.borderColor = AppColors.borderSecondary,
|
||||
});
|
||||
|
||||
final String? title;
|
||||
final String? description;
|
||||
final Widget child;
|
||||
final Color backgroundColor;
|
||||
final Color borderColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: borderColor),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (title != null) ...[
|
||||
Text(
|
||||
title!,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (description != null) ...[
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
description!,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (title != null || description != null)
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
child,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/page_header.dart' as widgets;
|
||||
|
||||
class AccountSurfaceScaffold extends StatelessWidget {
|
||||
const AccountSurfaceScaffold({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.body,
|
||||
this.footer,
|
||||
this.onBack,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Widget body;
|
||||
final Widget? footer;
|
||||
final VoidCallback? onBack;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.surfaceSecondary,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
widgets.PageHeader(leading: widgets.BackButton(onPressed: onBack)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.xl,
|
||||
AppSpacing.none,
|
||||
AppSpacing.xl,
|
||||
AppSpacing.sm,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
subtitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.xl,
|
||||
AppSpacing.sm,
|
||||
AppSpacing.xl,
|
||||
AppSpacing.xl,
|
||||
),
|
||||
child: body,
|
||||
),
|
||||
),
|
||||
if (footer != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.xl,
|
||||
AppSpacing.none,
|
||||
AppSpacing.xl,
|
||||
AppSpacing.xl,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceInfoLight,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: AppColors.borderTertiary),
|
||||
),
|
||||
child: footer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user