feat:增加用户信息编辑功能

This commit is contained in:
2026-07-05 14:14:43 +08:00
parent 9d3a3f2e80
commit 780526ddf4
18 changed files with 379 additions and 60 deletions

View File

@@ -10,3 +10,7 @@ Future<User> queryUserApi(int number) {
converter: (data) => User.fromJson(data), converter: (data) => User.fromJson(data),
); );
} }
Future<bool> updateUserApi(int id, User user) {
return httpUtil.put<bool>("/user/$id", data: user);
}

View File

@@ -126,7 +126,6 @@ class Recipe {
bool isShare; bool isShare;
int userId; int userId;
String username; String username;
String avatar;
List<RecipeMaterial> materialList; List<RecipeMaterial> materialList;
List<RecipeStep> stepList; List<RecipeStep> stepList;
List<FoodRecord> recordList; List<FoodRecord> recordList;
@@ -143,7 +142,6 @@ class Recipe {
required this.isShare, required this.isShare,
required this.userId, required this.userId,
required this.username, required this.username,
required this.avatar,
required this.materialList, required this.materialList,
required this.stepList, required this.stepList,
required this.recordList, required this.recordList,
@@ -166,7 +164,6 @@ class Recipe {
isShare: true, isShare: true,
userId: 0, userId: 0,
username: '', username: '',
avatar: '',
materialList: [], materialList: [],
stepList: [], stepList: [],
recordList: [], recordList: [],

View File

@@ -89,7 +89,6 @@ Recipe _$RecipeFromJson(Map<String, dynamic> json) => Recipe(
isShare: json['isShare'] as bool, isShare: json['isShare'] as bool,
userId: (json['userId'] as num).toInt(), userId: (json['userId'] as num).toInt(),
username: json['username'] as String, username: json['username'] as String,
avatar: json['avatar'] as String,
materialList: materialList:
(json['materialList'] as List<dynamic>) (json['materialList'] as List<dynamic>)
.map((e) => RecipeMaterial.fromJson(e as Map<String, dynamic>)) .map((e) => RecipeMaterial.fromJson(e as Map<String, dynamic>))
@@ -125,7 +124,6 @@ Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{
'isShare': instance.isShare, 'isShare': instance.isShare,
'userId': instance.userId, 'userId': instance.userId,
'username': instance.username, 'username': instance.username,
'avatar': instance.avatar,
'materialList': instance.materialList, 'materialList': instance.materialList,
'stepList': instance.stepList, 'stepList': instance.stepList,
'recordList': instance.recordList, 'recordList': instance.recordList,

View File

@@ -384,7 +384,7 @@ class FoodProvider with ChangeNotifier {
summaryStats = statsResult; summaryStats = statsResult;
recordStats = recordStatsResult; recordStats = recordStatsResult;
categoryStats = categoryStatsResult; categoryStats = categoryStatsResult;
rankStats = rankStatsResult; rankStats = rankStatsResult.take(15).toList();
if (recordStats.isNotEmpty) { if (recordStats.isNotEmpty) {
double sumValue = recordStats.fold( double sumValue = recordStats.fold(

View File

@@ -7,8 +7,9 @@ import 'package:food_hub_app/models/session.dart';
class UserProvider with ChangeNotifier { class UserProvider with ChangeNotifier {
late User currentUser = User.getEmpty(); late User currentUser = User.getEmpty();
bool isLoading = false; bool isLoading = false;
String error = ''; String? error = '';
List<Moment> momentList = []; List<Moment> momentList = [];
late User userFormItem;
Future<void> refreshUser(int id) async { Future<void> refreshUser(int id) async {
if (isLoading) return; if (isLoading) return;
@@ -48,4 +49,56 @@ class UserProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
} }
void initUserForm(User user) {
userFormItem = user;
notifyListeners();
}
void updateUserFormItem({
String? username,
int? gender,
String? phoneNumber,
String? email,
DateTime? birthDate,
String? avatar,
List<String>? area,
String? address,
String? job,
List<String>? tags,
String? description
}) {
if (username != null) userFormItem.username = username;
if (gender != null) userFormItem.gender = gender;
if (phoneNumber != null) userFormItem.phoneNumber = phoneNumber;
if (email != null) userFormItem.email = email;
if (birthDate != null) userFormItem.birthDate = birthDate;
if (avatar != null) userFormItem.avatar = avatar;
if (area != null) userFormItem.area = area;
if (address != null) userFormItem.address = address;
if (job != null) userFormItem.job = job;
if (tags != null) userFormItem.tags = tags;
if (description != null) userFormItem.description = description;
notifyListeners();
}
Future<bool> handleUser() async {
if (isLoading) return true;
try {
isLoading = true;
error = null;
notifyListeners();
await updateUserApi(userFormItem.id!, userFormItem);
return true;
} catch (e) {
error = '处理数据失败: $e';
debugPrint('处理数据失败: $e');
return false;
} finally {
isLoading = false;
notifyListeners();
}
}
} }

View File

@@ -38,12 +38,21 @@ class _LoginPage extends State<LoginPage> {
Future<void> _loadRememberState() async { Future<void> _loadRememberState() async {
bool? isRemember = SPUtil.getBool('isRemember'); bool? isRemember = SPUtil.getBool('isRemember');
if (isRemember) { if (isRemember == true) {
setState(() { final username = SPUtil.getString('username');
_isRemember = true; final password = SPUtil.getString('password');
_username = SPUtil.getString('username');
_password = SPUtil.getString('password'); if (username.isNotEmpty && password.isNotEmpty) {
}); setState(() {
_isRemember = true;
_username = username;
_password = password;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
loginClick();
});
}
} }
} }

View File

@@ -80,6 +80,7 @@ class _MomentPageState extends State<MomentPage> {
Widget _buildMomentList(FoodProvider provider) { Widget _buildMomentList(FoodProvider provider) {
return ListView.separated( return ListView.separated(
padding: const EdgeInsets.only(bottom: 80),
controller: _scrollController, controller: _scrollController,
itemCount: provider.momentList.length, itemCount: provider.momentList.length,
separatorBuilder: (context, index) => SizedBox(height: 8), separatorBuilder: (context, index) => SizedBox(height: 8),

View File

@@ -10,8 +10,6 @@ import 'package:food_hub_app/provider/food_provider.dart';
import 'package:food_hub_app/utils/minio_utils.dart'; import 'package:food_hub_app/utils/minio_utils.dart';
import 'package:food_hub_app/widgets/common/form.dart'; import 'package:food_hub_app/widgets/common/form.dart';
import 'package:food_hub_app/widgets/common/image.dart'; import 'package:food_hub_app/widgets/common/image.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
import 'package:liquid_glass_widgets/widgets/containers/glass_card.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class MomentForm extends StatefulWidget { class MomentForm extends StatefulWidget {
@@ -215,14 +213,12 @@ class _MomentFormState extends State<MomentForm> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final provider = context.watch<FoodProvider>(); final provider = context.watch<FoodProvider>();
return SingleChildScrollView( return AnimatedPadding(
child: Padding( duration: const Duration(milliseconds: 200),
padding: EdgeInsets.all(10), padding: MediaQuery.of(context).viewInsets,
child: GlassCard( child: SingleChildScrollView(
useOwnLayer: true, child: Padding(
settings: LiquidGlassSettings( padding: EdgeInsets.all(16),
glassColor: Theme.of(context).scaffoldBackgroundColor,
),
child: _buildFormBuilder(provider), child: _buildFormBuilder(provider),
), ),
), ),

View File

@@ -33,11 +33,14 @@ class _MomentUserPageState extends State<MomentUserPage> {
} }
return Scaffold( return Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar( appBar: AppBar(
title: Text('我的朋友圈', style: const TextStyle(color: Colors.white)), backgroundColor: Theme.of(context).scaffoldBackgroundColor,
backgroundColor: Theme.of(context).colorScheme.primary, title: Text('我的朋友圈', style: Theme.of(context).textTheme.titleLarge),
centerTitle: true,
automaticallyImplyLeading: false,
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white), icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),
), ),

View File

@@ -4,6 +4,7 @@ import 'package:flutter_common/widget/common_widget.dart';
import 'package:flutter_common/widget/dialog_widget.dart'; import 'package:flutter_common/widget/dialog_widget.dart';
import 'package:food_hub_app/provider/app_provider.dart'; import 'package:food_hub_app/provider/app_provider.dart';
import 'package:food_hub_app/provider/user_provider.dart'; import 'package:food_hub_app/provider/user_provider.dart';
import 'package:food_hub_app/views/profile_form.dart';
import 'package:food_hub_app/widgets/profile/basic_info.dart'; import 'package:food_hub_app/widgets/profile/basic_info.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
import 'package:liquid_glass_widgets/widgets/containers/glass_card.dart'; import 'package:liquid_glass_widgets/widgets/containers/glass_card.dart';
@@ -42,11 +43,26 @@ class ProfilePageState extends State<ProfilePage> {
); );
} }
void _onTapInfo(UserProvider provider) {
provider.initUserForm(provider.currentUser);
showModalBottomSheet(
context: context,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => ProfileForm(),
);
}
void _onTapMoment() { void _onTapMoment() {
Navigator.pushNamed(context, '/momentUser'); Navigator.pushNamed(context, '/momentUser');
} }
Widget _buildFunctionButtons(BuildContext context) { Widget _buildFunctionButtons(BuildContext context) {
final provider = context.watch<UserProvider>();
final colors = Theme.of(context).colorScheme; final colors = Theme.of(context).colorScheme;
return GlassCard( return GlassCard(
@@ -58,10 +74,7 @@ class ProfilePageState extends State<ProfilePage> {
_buildFunctionButton( _buildFunctionButton(
icon: Icons.account_circle, icon: Icons.account_circle,
text: '基本资料', text: '基本资料',
onTap: () { onTap: () => _onTapInfo(provider),
// 跳转到编辑资料页面
// LogUtils.i('点击编辑资料');
},
), ),
const Divider(height: 1, thickness: 0.2), const Divider(height: 1, thickness: 0.2),
_buildFunctionButton( _buildFunctionButton(
@@ -155,8 +168,6 @@ class ProfilePageState extends State<ProfilePage> {
children: [ children: [
if (provider.isLoading) if (provider.isLoading)
buildLoadingIndicator() buildLoadingIndicator()
else if (provider.error.isNotEmpty)
Text(provider.error)
else else
SingleChildScrollView( SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 90), padding: const EdgeInsets.only(bottom: 90),

251
lib/views/profile_form.dart Normal file
View File

@@ -0,0 +1,251 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/dialog_widget.dart';
import 'package:flutter_common/widget/loading_widget.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_input_chips/flutter_input_chips.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:food_hub_app/provider/user_provider.dart';
import 'package:food_hub_app/widgets/common/form.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class ProfileForm extends StatefulWidget {
const ProfileForm({super.key});
@override
State<ProfileForm> createState() => _ProfileFormState();
}
class _ProfileFormState extends State<ProfileForm> {
final _formKey = GlobalKey<FormBuilderState>();
@override
void initState() {
super.initState();
}
Widget _buildFormBuilder(UserProvider provider) {
final double sizeBoxHeight = 8;
return FormBuilder(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildFormLabel(context: context, text: '用户名', isRequired: true),
SizedBox(height: sizeBoxHeight),
FormBuilderTextField(
name: 'username',
initialValue: provider.userFormItem.username,
onChanged: (value) {
provider.updateUserFormItem(username: value);
},
decoration: buildInputDecoration(
context: context,
hintText: '请输入用户名',
),
validator: FormBuilderValidators.required(errorText: '请输入用户名'),
),
SizedBox(height: sizeBoxHeight),
buildFormLabel(context: context, text: '性别', isRequired: true),
SizedBox(height: sizeBoxHeight),
FormBuilderDropdown<int>(
name: 'gender',
initialValue: provider.userFormItem.gender,
decoration: buildInputDecoration(
context: context,
hintText: '请选择性别',
),
items: const [
DropdownMenuItem(value: 1, child: Text('')),
DropdownMenuItem(value: 2, child: Text('')),
],
onChanged: (value) {
provider.updateUserFormItem(gender: value);
},
),
SizedBox(height: sizeBoxHeight),
buildFormLabel(context: context, text: '手机号', isRequired: true),
SizedBox(height: sizeBoxHeight),
FormBuilderTextField(
name: 'phoneNumber',
initialValue: provider.userFormItem.phoneNumber,
keyboardType: TextInputType.phone,
decoration: buildInputDecoration(
context: context,
hintText: '请输入手机号',
),
onChanged: (value) {
provider.updateUserFormItem(phoneNumber: value);
},
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(errorText: '请输入手机号'),
FormBuilderValidators.match(
RegExp(r'^1[3-9]\d{9}$'),
errorText: '请输入正确的11位手机号',
),
]),
),
SizedBox(height: sizeBoxHeight),
buildFormLabel(context: context, text: '邮箱', isRequired: true),
SizedBox(height: sizeBoxHeight),
FormBuilderTextField(
name: 'email',
initialValue: provider.userFormItem.email,
keyboardType: TextInputType.emailAddress,
decoration: buildInputDecoration(
context: context,
hintText: '请输入邮箱',
),
onChanged: (value) {
provider.updateUserFormItem(email: value);
},
validator: FormBuilderValidators.compose([
FormBuilderValidators.email(errorText: '邮箱格式不正确'),
]),
),
SizedBox(height: sizeBoxHeight),
buildFormLabel(context: context, text: '出生日期', isRequired: true),
SizedBox(height: sizeBoxHeight),
FormBuilderDateTimePicker(
name: 'birthDate',
initialValue: provider.userFormItem.birthDate,
inputType: InputType.date,
format: DateFormat('yyyy-MM-dd'),
firstDate: DateTime(1900),
lastDate: DateTime.now(),
decoration: buildInputDecoration(
context: context,
hintText: '请选择出生日期',
),
onChanged: (value) {
provider.updateUserFormItem(birthDate: value);
},
),
SizedBox(height: sizeBoxHeight),
buildFormLabel(context: context, text: '详细地址', isRequired: false),
SizedBox(height: sizeBoxHeight),
FormBuilderTextField(
name: 'address',
initialValue: provider.userFormItem.address,
decoration: buildInputDecoration(
context: context,
hintText: '请输入详细地址',
),
onChanged: (value) {
provider.updateUserFormItem(address: value);
},
),
SizedBox(height: sizeBoxHeight),
buildFormLabel(context: context, text: '职业', isRequired: false),
SizedBox(height: sizeBoxHeight),
FormBuilderTextField(
name: 'job',
initialValue: provider.userFormItem.job,
decoration: buildInputDecoration(
context: context,
hintText: '请输入职业',
),
onChanged: (value) {
provider.updateUserFormItem(job: value);
},
),
SizedBox(height: sizeBoxHeight),
buildFormLabel(context: context, text: '标签', isRequired: false),
SizedBox(height: sizeBoxHeight),
FormBuilderField<List<String>>(
name: 'tags',
initialValue: provider.userFormItem.tags,
validator: FormBuilderValidators.minLength(
1,
errorText: '至少添加一个标签',
),
builder: (FormFieldState<List<String>> field) {
return FlutterInputChips(
padding: EdgeInsets.all(0),
initialValue: field.value ?? [],
onChanged: (value) {
provider.updateUserFormItem(tags: value);
},
inputDecoration: buildInputDecoration(
context: context,
hintText: '请输入标签,多个标签用回车键隔开',
),
);
},
),
SizedBox(height: sizeBoxHeight),
buildFormLabel(context: context, text: '简介', isRequired: false),
SizedBox(height: sizeBoxHeight),
FormBuilderTextField(
name: 'description',
initialValue: provider.userFormItem.description,
decoration: buildInputDecoration(
context: context,
hintText: '请输入简介',
),
onChanged: (value) {
provider.updateUserFormItem(description: value);
},
),
SizedBox(height: sizeBoxHeight),
buildFormButtonGroup(
context: context,
isShowDelete: false,
onConfirm: () => _submitForm(provider),
onDelete: () => {},
),
],
),
);
}
/// 提交表单
void _submitForm(UserProvider provider) async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
LoadingDialog.show(context, message: '提交中');
final result = await provider.handleUser();
if (result == true) {
LoadingDialog.hide(context);
showSuccessTip(context, '更新信息成功');
Future.delayed(Duration(milliseconds: 1500), () {
if (mounted) {
Navigator.pop(context);
}
});
} else {
LoadingDialog.hide(context);
showErrorTip(context, '更新信息失败');
}
}
}
@override
Widget build(BuildContext context) {
final provider = context.watch<UserProvider>();
return AnimatedPadding(
duration: const Duration(milliseconds: 200),
padding: MediaQuery.of(context).viewInsets,
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(16),
child: _buildFormBuilder(provider),
),
),
);
}
}

View File

@@ -99,8 +99,6 @@ class ProfileUserPageState extends State<ProfileUserPage> {
children: [ children: [
if (provider.isLoading) if (provider.isLoading)
buildLoadingIndicator() buildLoadingIndicator()
else if (provider.error.isNotEmpty)
Text(provider.error)
else else
_buildContent(provider), _buildContent(provider),
], ],

View File

@@ -8,8 +8,6 @@ import 'package:food_hub_app/widgets/common/index.dart';
import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:flutter_common/utils/toast_util.dart'; import 'package:flutter_common/utils/toast_util.dart';
import 'package:flutter_common/widget/loading_widget.dart'; import 'package:flutter_common/widget/loading_widget.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
import 'package:liquid_glass_widgets/widgets/containers/glass_card.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class RecipeForm extends StatefulWidget { class RecipeForm extends StatefulWidget {
@@ -551,14 +549,12 @@ class _RecipeFormState extends State<RecipeForm> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final provider = context.watch<FoodProvider>(); final provider = context.watch<FoodProvider>();
return SingleChildScrollView( return AnimatedPadding(
child: Padding( duration: const Duration(milliseconds: 200),
padding: EdgeInsets.all(10), padding: MediaQuery.of(context).viewInsets,
child: GlassCard( child: SingleChildScrollView(
useOwnLayer: true, child: Padding(
settings: LiquidGlassSettings( padding: EdgeInsets.all(16),
glassColor: Theme.of(context).scaffoldBackgroundColor,
),
child: Column( child: Column(
children: [ children: [
_buildStepIndicator(), _buildStepIndicator(),

View File

@@ -88,9 +88,6 @@ class _RecordPageState extends State<RecordPage> {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
constraints: BoxConstraints(
minHeight: 600,
),
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
isScrollControlled: true, isScrollControlled: true,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(

View File

@@ -11,7 +11,6 @@ import 'package:food_hub_app/utils/minio_utils.dart';
import 'package:food_hub_app/widgets/common/form.dart'; import 'package:food_hub_app/widgets/common/form.dart';
import 'package:food_hub_app/widgets/common/image.dart'; import 'package:food_hub_app/widgets/common/image.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -333,14 +332,12 @@ class _RecordFormState extends State<RecordForm> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final provider = context.watch<FoodProvider>(); final provider = context.watch<FoodProvider>();
return SingleChildScrollView( return AnimatedPadding(
child: Padding( duration: const Duration(milliseconds: 200),
padding: EdgeInsets.all(10), padding: MediaQuery.of(context).viewInsets,
child: GlassCard( child: SingleChildScrollView(
useOwnLayer: true, child: Padding(
settings: LiquidGlassSettings( padding: EdgeInsets.all(16),
glassColor: Theme.of(context).scaffoldBackgroundColor,
),
child: _buildFormBuilder(provider), child: _buildFormBuilder(provider),
), ),
), ),

View File

@@ -201,7 +201,7 @@ Widget buildImageUploadButton({
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Container( child: Container(
width: double.infinity, width: double.infinity,
height: 250, height: 180,
decoration: BoxDecoration( decoration: BoxDecoration(
color: colors.surface, color: colors.surface,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),

View File

@@ -421,6 +421,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.1.5" version: "0.1.5"
flutter_input_chips:
dependency: "direct main"
description:
name: flutter_input_chips
sha256: "4b45df0c8b80a23db86850d8f892144dc796f3f6eb13ebd8bcb2d242ec921e22"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:

View File

@@ -1 +1 @@
name: food_hub_app name: food_hub_app