From ed0ceb29d3b246349fcbfce21a0622898e71370b Mon Sep 17 00:00:00 2001 From: Cxx0822 <1556464090@qq.com> Date: Tue, 25 Nov 2025 19:16:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E6=9C=8B=E5=8F=8B=E6=9C=8B=E5=8F=8B=E5=9C=88=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/src/main/AndroidManifest.xml | 3 + lib/main.dart | 8 +- lib/provider/app_provider.dart | 8 ++ lib/provider/user_provider.dart | 23 +++++ lib/views/moment_user.dart | 18 +--- lib/views/profile.dart | 125 ++++++----------------- lib/views/profile_user.dart | 117 +++++++++++++++++++++ lib/widgets/moment/card.dart | 73 ++++++++----- lib/widgets/moment/list.dart | 21 ++++ lib/widgets/profile/basic_info.dart | 113 ++++++++++++++++++++ 10 files changed, 374 insertions(+), 135 deletions(-) create mode 100644 lib/provider/app_provider.dart create mode 100644 lib/views/profile_user.dart create mode 100644 lib/widgets/moment/list.dart create mode 100644 lib/widgets/profile/basic_info.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 95d7db5..8c2e7f2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,7 @@ + + + FoodProvider()), ChangeNotifierProvider(create: (context) => ThemeProvider()), ChangeNotifierProvider(create: (context) => UserProvider()), + ChangeNotifierProvider(create: (context) => AppProvider()), ], child: MyApp(), ), @@ -69,7 +74,8 @@ class MyApp extends StatelessWidget { '/recipeDetail': (context) => RecipeDetailPage(), '/momentForm': (context) => MomentFormPage(), '/moment': (context) => MomentPage(), - '/momentUser': (context) => MomentUserPage() + '/momentUser': (context) => MomentUserPage(), + '/ProfileUser': (context) => ProfileUserPage() }, ); } diff --git a/lib/provider/app_provider.dart b/lib/provider/app_provider.dart new file mode 100644 index 0000000..70d72d7 --- /dev/null +++ b/lib/provider/app_provider.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; + +class AppProvider with ChangeNotifier { + String appVersion = '1.0.0'; + String buildNumber = '1'; + + String get fullVersion => '$appVersion+$buildNumber'; +} diff --git a/lib/provider/user_provider.dart b/lib/provider/user_provider.dart index 9529aa8..ff4e29c 100644 --- a/lib/provider/user_provider.dart +++ b/lib/provider/user_provider.dart @@ -1,11 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:food_hub_app/apis/moment.dart'; import 'package:food_hub_app/apis/user.dart'; +import 'package:food_hub_app/models/moment.dart'; import 'package:food_hub_app/models/session.dart'; class UserProvider with ChangeNotifier { late User currentUser = User.getEmpty(); bool isLoading = false; String error = ''; + List momentList = []; Future refreshUser(int id) async { if (isLoading) return; @@ -25,4 +28,24 @@ class UserProvider with ChangeNotifier { notifyListeners(); } } + + Future queryMomentByUserId(int userId) async { + if (isLoading) return; + + try { + isLoading = true; + error = ''; + notifyListeners(); + + final result = await queryMomentListByUserIdApi(userId); + momentList = result; + notifyListeners(); + } catch (e) { + error = '加载数据失败: $e'; + debugPrint('加载数据失败: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } } diff --git a/lib/views/moment_user.dart b/lib/views/moment_user.dart index e5da6f8..d813db0 100644 --- a/lib/views/moment_user.dart +++ b/lib/views/moment_user.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_common/utils/sp_utils.dart'; import 'package:flutter_common/widget/common_widget.dart'; import 'package:food_hub_app/provider/food_provider.dart'; -import 'package:food_hub_app/widgets/moment/card.dart'; +import 'package:food_hub_app/widgets/moment/list.dart'; import 'package:provider/provider.dart'; class MomentUserPage extends StatefulWidget { @@ -23,16 +23,6 @@ class _MomentUserPageState extends State { }); } - Widget _buildMomentList(FoodProvider provider) { - return ListView.separated( - itemCount: provider.momentList.length, - separatorBuilder: (context, index) => SizedBox(height: 8), - itemBuilder: (context, index) { - return MomentCard(moment: provider.momentList[index], isUser: true); - }, - ); - } - @override Widget build(BuildContext context) { final provider = context.watch(); @@ -51,13 +41,15 @@ class _MomentUserPageState extends State { onPressed: () => Navigator.pop(context), ), ), - backgroundColor: const Color(0xFFF5F5F5), body: provider.isLoading ? buildLoadingIndicator() : Padding( padding: const EdgeInsets.all(10), - child: CommonCard(child: _buildMomentList(provider)), + child: MomentList( + momentList: provider.momentList, + isUser: true, + ), ), ); } diff --git a/lib/views/profile.dart b/lib/views/profile.dart index 83c8798..87fb3b0 100644 --- a/lib/views/profile.dart +++ b/lib/views/profile.dart @@ -1,14 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:flutter_common/utils/date_utils.dart'; import 'package:flutter_common/utils/sp_utils.dart'; import 'package:flutter_common/widget/common_widget.dart'; import 'package:flutter_common/widget/dialog_widget.dart'; -import 'package:food_hub_app/config/app_config.dart'; -import 'package:food_hub_app/models/session.dart'; +import 'package:food_hub_app/provider/app_provider.dart'; import 'package:food_hub_app/provider/user_provider.dart'; -import 'package:food_hub_app/widgets/common/index.dart'; +import 'package:food_hub_app/widgets/profile/basic_info.dart'; import 'package:provider/provider.dart'; -import 'package:tdesign_flutter/tdesign_flutter.dart'; class ProfilePage extends StatefulWidget { const ProfilePage({super.key}); @@ -28,37 +25,6 @@ class ProfilePageState extends State { }); } - Widget _buildAvatar(User user) { - if (user.avatar!.isEmpty) { - return TDAvatar( - size: TDAvatarSize.large, - type: TDAvatarType.customText, - shape: TDAvatarShape.circle, - backgroundColor: Theme.of(context).colorScheme.primary, - text: user.username.isNotEmpty ? user.username[0] : '?', - ); - } else { - return TDAvatar( - size: TDAvatarSize.large, - type: TDAvatarType.normal, - fit: BoxFit.contain, - avatarUrl: '${AppConfig.imageBaseUrl}/${user.avatar}', - ); - } - } - - Widget _buildInfoItem({required IconData icon, required String text}) { - final colors = Theme.of(context).colorScheme; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 16, color: colors.primary), - const SizedBox(width: 6), - Text(text), - ], - ); - } - Widget _buildFunctionButton({ required IconData icon, required String text, @@ -74,59 +40,6 @@ class ProfilePageState extends State { ); } - Widget _buildBasicInfo(User user) { - return CommonCard( - child: Column( - children: [ - _buildAvatar(user), - const SizedBox(height: 8), - Text( - user.username, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - if (user.tags != null && user.tags!.isNotEmpty) - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: - user.tags!.map((tag) => buildTag(context, tag)).toList(), - ), - if (user.tags != null && user.tags!.isNotEmpty) - const SizedBox(height: 8), - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - if (user.area != null && user.area!.length >= 2) - _buildInfoItem( - icon: Icons.location_on, - text: '${user.area![0]}-${user.area![1]}', - ), - if (user.birthDate != null) - _buildInfoItem( - icon: Icons.cake, - text: formatDate(user.birthDate!), - ), - if (user.job != null && user.job!.isNotEmpty) - _buildInfoItem(icon: Icons.work, text: user.job!), - if (user.phoneNumber != null && user.phoneNumber!.isNotEmpty) - _buildInfoItem( - icon: Icons.phone_android, - text: user.phoneNumber!, - ), - if (user.email != null && user.email!.isNotEmpty) - _buildInfoItem(icon: Icons.email, text: user.email!), - ], - ), - if (user.description != null && user.description!.isNotEmpty) - const SizedBox(height: 8), - if (user.description != null && user.description!.isNotEmpty) - Text(user.description!), - ], - ), - ); - } - void _onTapMoment() { Navigator.pushNamed(context, '/momentUser'); } @@ -168,25 +81,51 @@ class ProfilePageState extends State { }, ), const Divider(height: 1), + _buildFunctionButton(icon: Icons.message, text: '反馈意见', onTap: () {}), + const Divider(height: 1), + _buildFunctionButton(icon: Icons.refresh, text: '检查更新', onTap: () {}), + const Divider(height: 1), _buildFunctionButton( icon: Icons.exit_to_app, text: '退出登录', - onTap: () { - showConfirmDialog(context, '确认要退出吗?'); - }, + onTap: _onTapLogout, ), ], ), ); } + void _onTapLogout() async { + final result = await showConfirmDialog(context, '确认要退出吗?'); + if (result == true) { + SPUtil.remove('token'); + Navigator.pop(context); + Navigator.pushNamed(context, '/login'); + } + } + + Widget _buildVersionInfo() { + final provider = context.read(); + + return Text( + '版本 v${provider.fullVersion}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.primary, + ), + ); + } + Widget _buildContent(UserProvider provider) { final user = provider.currentUser; return Column( children: [ - SizedBox(width: double.infinity, child: _buildBasicInfo(user)), + SizedBox(width: double.infinity, child: ProfileInfo(user: user)), const SizedBox(height: 16), _buildFunctionButtons(), + const SizedBox(height: 8), + _buildVersionInfo(), ], ); } diff --git a/lib/views/profile_user.dart b/lib/views/profile_user.dart new file mode 100644 index 0000000..b9afdc3 --- /dev/null +++ b/lib/views/profile_user.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_common/utils/sp_utils.dart'; +import 'package:flutter_common/widget/common_widget.dart'; +import 'package:flutter_common/widget/dialog_widget.dart'; +import 'package:food_hub_app/models/session.dart'; +import 'package:food_hub_app/provider/app_provider.dart'; +import 'package:food_hub_app/provider/user_provider.dart'; +import 'package:food_hub_app/widgets/common/index.dart'; +import 'package:food_hub_app/widgets/moment/list.dart'; +import 'package:food_hub_app/widgets/profile/basic_info.dart'; +import 'package:food_hub_app/widgets/recipe/calendar.dart'; +import 'package:food_hub_app/widgets/recipe/list.dart'; +import 'package:food_hub_app/widgets/recipe/timeline.dart'; +import 'package:provider/provider.dart'; + +enum ProfileTab { + moment('朋友圈'), + recipe('菜谱'); + + final String label; + + const ProfileTab(this.label); +} + +class ProfileUserPage extends StatefulWidget { + const ProfileUserPage({super.key}); + + @override + State createState() => ProfileUserPageState(); +} + +class ProfileUserPageState extends State { + late ProfileTab currentTab = ProfileTab.moment; + + void _onTabChange(ProfileTab tab) { + setState(() { + currentTab = tab; + }); + + // provider.onRecordTabChange(); + } + + Widget _buildTabs({ + required BuildContext context, + required ProfileTab currentTab, + required ValueChanged onTabChanged, + }) { + return Container( + padding: EdgeInsets.all(8), + child: buildToggleSwitch( + context: context, + currentTab: currentTab, + tabValues: ProfileTab.values, + labels: ProfileTab.values.map((e) => e.label).toList(), + onTabChanged: onTabChanged, + ), + ); + } + + Widget _buildContent(UserProvider provider) { + return Column( + children: [ + SizedBox( + width: double.infinity, + child: ProfileInfo(user: provider.currentUser), + ), + _buildTabs( + context: context, + currentTab: currentTab, + onTabChanged: (tab) => _onTabChange(tab), + ), + Expanded( + child: IndexedStack( + index: currentTab.index, + children: [ + MomentList(momentList: provider.momentList, isUser: false), + Text('个人菜谱'), + ], + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + final provider = context.watch(); + final colors = Theme.of(context).colorScheme; + + return Scaffold( + appBar: AppBar( + title: Text( + '${provider.currentUser.username}的个人信息', + style: const TextStyle(color: Colors.white), + ), + backgroundColor: colors.primary, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + ), + body: Padding( + padding: const EdgeInsets.all(10), + child: Stack( + children: [ + if (provider.isLoading) + buildLoadingIndicator() + else if (provider.error.isNotEmpty) + Text(provider.error) + else + _buildContent(provider), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/moment/card.dart b/lib/widgets/moment/card.dart index e2188f4..123bd5a 100644 --- a/lib/widgets/moment/card.dart +++ b/lib/widgets/moment/card.dart @@ -1,25 +1,32 @@ import 'package:flutter/material.dart'; +import 'package:flutter_common/flutter_common.dart'; import 'package:flutter_common/widget/common_widget.dart'; import 'package:food_hub_app/config/app_config.dart'; import 'package:food_hub_app/models/moment.dart'; import 'package:food_hub_app/provider/food_provider.dart'; +import 'package:food_hub_app/provider/user_provider.dart'; import 'package:food_hub_app/widgets/common/image.dart'; import 'package:provider/provider.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; -class MomentCard extends StatelessWidget { +class MomentCard extends StatefulWidget { final Moment moment; final bool isUser; const MomentCard({super.key, required this.moment, required this.isUser}); + @override + State createState() => MomentCardState(); +} + +class MomentCardState extends State { @override Widget build(BuildContext context) { return CommonCard( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildAvatar(context), + InkWell(onTap: () => _onTapUser(context), child: _buildAvatar()), const SizedBox(width: 12), Expanded( child: Column( @@ -28,15 +35,15 @@ class MomentCard extends StatelessWidget { _buildNickname(), const SizedBox(height: 5), _buildContent(), - if (moment.imageList.isNotEmpty) ...[ + if (widget.moment.imageList.isNotEmpty) ...[ const SizedBox(height: 8), - _buildPostImages(context), + _buildPostImages(), ], const SizedBox(height: 10), _buildTime(), const SizedBox(height: 5), - _buildActionButtons(context), - if (moment.commentList!.isNotEmpty) ...[ + _buildActionButtons(), + if (widget.moment.commentList!.isNotEmpty) ...[ const SizedBox(height: 8), _buildCommentList(), ], @@ -48,22 +55,35 @@ class MomentCard extends StatelessWidget { ); } + void _onTapUser(BuildContext context) async { + final provider = context.read(); + + if (widget.moment.userId != SPUtil.getInt('userId')) { + await provider.refreshUser(widget.moment.userId!); + await provider.queryMomentByUserId(widget.moment.userId!); + + if (mounted) { + Navigator.pushNamed(context, '/ProfileUser'); + } + } + } + // 构建头像组件 - Widget _buildAvatar(BuildContext context) { - if (moment.avatar!.isEmpty) { + Widget _buildAvatar() { + if (widget.moment.avatar!.isEmpty) { return TDAvatar( size: TDAvatarSize.medium, type: TDAvatarType.customText, shape: TDAvatarShape.circle, backgroundColor: Theme.of(context).colorScheme.primary, - text: moment.username?[0], + text: widget.moment.username?[0], ); } else { return TDAvatar( size: TDAvatarSize.medium, type: TDAvatarType.normal, fit: BoxFit.contain, - avatarUrl: '${AppConfig.imageBaseUrl}/${moment.avatar}', + avatarUrl: '${AppConfig.imageBaseUrl}/${widget.moment.avatar}', ); } } @@ -71,7 +91,7 @@ class MomentCard extends StatelessWidget { // 构建昵称组件 Widget _buildNickname() { return Text( - moment.username ?? "", + widget.moment.username ?? "", style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ); } @@ -79,14 +99,14 @@ class MomentCard extends StatelessWidget { // 构建内容组件 Widget _buildContent() { return Text( - moment.content, + widget.moment.content, style: const TextStyle(fontSize: 15, height: 1.3), ); } // 构建朋友圈图片列表(网格布局) - Widget _buildPostImages(BuildContext context) { - final imageCount = moment.imageList.length; + Widget _buildPostImages() { + final imageCount = widget.moment.imageList.length; // 计算网格列数 int crossAxisCount; @@ -106,7 +126,7 @@ class MomentCard extends StatelessWidget { // 生成图片URL列表(用于预览时切换) final List imageUrls = - moment.imageList.map((path) => path).toList(); + widget.moment.imageList.map((path) => path).toList(); return GridView.count( shrinkWrap: true, @@ -124,33 +144,30 @@ class MomentCard extends StatelessWidget { // 构建时间组件 Widget _buildTime() { return Text( - moment.date ?? "", + widget.moment.date ?? "", style: TextStyle(fontSize: 12, color: Colors.grey[500]), ); } - void _onTapEdit(BuildContext context) { + void _onTapEdit() { final provider = Provider.of(context, listen: false); provider.isEditing = true; - provider.momentFormItem = moment; + provider.momentFormItem = widget.moment; Navigator.pushNamed(context, '/momentForm'); } // 构建点赞和评论按钮 - Widget _buildActionButtons(BuildContext context) { + Widget _buildActionButtons() { return Row( children: [ - if (isUser) + if (widget.isUser) SizedBox( width: 42, height: 36, child: Stack( alignment: Alignment.bottomLeft, children: [ - InkWell( - onTap: () => _onTapEdit(context), - child: Icon(Icons.edit), - ), + InkWell(onTap: () => _onTapEdit(), child: Icon(Icons.edit)), ], ), ), @@ -166,7 +183,7 @@ class MomentCard extends StatelessWidget { bottom: 15, child: TDBadge( TDBadgeType.message, - count: (moment.likeList?.length ?? 0).toString(), + count: (widget.moment.likeList?.length ?? 0).toString(), ), ), ], @@ -184,7 +201,7 @@ class MomentCard extends StatelessWidget { bottom: 15, child: TDBadge( TDBadgeType.message, - count: (moment.commentList?.length ?? 0).toString(), + count: (widget.moment.commentList?.length ?? 0).toString(), ), ), ], @@ -236,10 +253,10 @@ class MomentCard extends StatelessWidget { child: ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - itemCount: moment.commentList!.length, + itemCount: widget.moment.commentList!.length, separatorBuilder: (context, index) => SizedBox(height: 8), itemBuilder: (context, index) { - return _buildCommentItem(moment.commentList![index]); + return _buildCommentItem(widget.moment.commentList![index]); }, ), ); diff --git a/lib/widgets/moment/list.dart b/lib/widgets/moment/list.dart new file mode 100644 index 0000000..8a8d737 --- /dev/null +++ b/lib/widgets/moment/list.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:food_hub_app/models/moment.dart'; +import 'package:food_hub_app/widgets/moment/card.dart'; + +class MomentList extends StatelessWidget { + final List momentList; + final bool isUser; + + const MomentList({super.key, required this.momentList, required this.isUser}); + + @override + Widget build(BuildContext context) { + return ListView.separated( + itemCount: momentList.length, + separatorBuilder: (context, index) => SizedBox(height: 8), + itemBuilder: (context, index) { + return MomentCard(moment: momentList[index], isUser: isUser); + }, + ); + } +} diff --git a/lib/widgets/profile/basic_info.dart b/lib/widgets/profile/basic_info.dart new file mode 100644 index 0000000..efcf4e3 --- /dev/null +++ b/lib/widgets/profile/basic_info.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_common/utils/date_utils.dart'; +import 'package:flutter_common/widget/common_widget.dart'; +import 'package:food_hub_app/config/app_config.dart'; +import 'package:food_hub_app/models/session.dart'; +import 'package:food_hub_app/widgets/common/index.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; + +class ProfileInfo extends StatelessWidget { + final User user; + + const ProfileInfo({super.key, required this.user}); + + Widget _buildAvatar(BuildContext context, User user) { + if (user.avatar == null) { + return TDAvatar( + size: TDAvatarSize.large, + type: TDAvatarType.customText, + shape: TDAvatarShape.circle, + backgroundColor: Theme.of(context).colorScheme.primary, + text: user.username.isNotEmpty ? user.username[0] : '?', + ); + } else { + return TDAvatar( + size: TDAvatarSize.large, + type: TDAvatarType.normal, + fit: BoxFit.contain, + avatarUrl: '${AppConfig.imageBaseUrl}/${user.avatar}', + ); + } + } + + Widget _buildInfoItem({ + required BuildContext context, + required IconData icon, + required String text, + }) { + final colors = Theme.of(context).colorScheme; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 16, color: colors.primary), + const SizedBox(width: 6), + Text(text), + ], + ); + } + + @override + Widget build(BuildContext context) { + return CommonCard( + child: Column( + children: [ + _buildAvatar(context, user), + const SizedBox(height: 8), + Text( + user.username, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + if (user.tags != null && user.tags!.isNotEmpty) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: + user.tags!.map((tag) => buildTag(context, tag)).toList(), + ), + if (user.tags != null && user.tags!.isNotEmpty) + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + if (user.area != null && user.area!.length >= 2) + _buildInfoItem( + context: context, + icon: Icons.location_on, + text: '${user.area![0]}-${user.area![1]}', + ), + if (user.birthDate != null) + _buildInfoItem( + context: context, + icon: Icons.cake, + text: formatDate(user.birthDate!), + ), + if (user.job != null && user.job!.isNotEmpty) + _buildInfoItem( + context: context, + icon: Icons.work, + text: user.job!, + ), + if (user.phoneNumber != null && user.phoneNumber!.isNotEmpty) + _buildInfoItem( + context: context, + icon: Icons.phone_android, + text: user.phoneNumber!, + ), + if (user.email != null && user.email!.isNotEmpty) + _buildInfoItem( + context: context, + icon: Icons.email, + text: user.email!, + ), + ], + ), + if (user.description != null && user.description!.isNotEmpty) + const SizedBox(height: 8), + if (user.description != null && user.description!.isNotEmpty) + Text(user.description!), + ], + ), + ); + } +}