feat: 优化 Agent 运行时与聊天设置体验

This commit is contained in:
qzl
2026-03-16 18:32:09 +08:00
parent 3f79cf0df7
commit 5a34616287
41 changed files with 2603 additions and 1263 deletions
@@ -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,
),
),
],
),
),
);
}
}