feat:重构模块

This commit is contained in:
2026-06-27 17:12:38 +08:00
parent a5e788eee6
commit a340ba424e
31 changed files with 919 additions and 623 deletions

View File

@@ -1,15 +1,13 @@
/// 应用信息
class AppConfig {
// 网络配置
// http://192.168.1.3:8100
// http://14.103.235.151:81/food-service
// static const String baseApiUrl = "http://14.103.235.151:81/food-service";
// static const String baseApiUrl = "http://192.168.1.3:8100";
static const String baseApiUrl = "https://cxx0822.iepose.cn/food-api";
// static const String baseApiUrl = "http://192.168.1.4:8083";
// static const String baseApiUrl = "http://192.168.1.103:8083";
static const String rustfsIp = '14.103.235.151';
static const String rustfsFileUrl = 'http://14.103.235.151:9100';
static const String baseApiUrl = "https://cxx0822.s.3q.hair/food-api";
static const String rustfsIp = '43.248.188.28';
static const int rustfsPort = 23125;
static const String rustfsAccessKey = 'sZuAg1WCo5i4kD936Tqh';
static const String rustfsSecretKey = 'g2fEyk6bslGrHepBvjhu0OLNF4qCRziPJIx1KAS8';
static const String rustfsFileUrl = 'http://$rustfsIp:$rustfsPort';
static const String bucketName = 'food';
static const String imageBaseUrl = '$rustfsFileUrl/$bucketName/';
// static const String imageBaseUrl = '$baseApiUrl/';

View File

@@ -1,37 +0,0 @@
import 'package:flutter/material.dart';
import 'menu.dart';
class AppNavbar extends StatefulWidget {
final int currentPageIndex;
final Function(int) onTapNavbarItem;
const AppNavbar({
super.key,
required this.currentPageIndex,
required this.onTapNavbarItem,
});
@override
State<StatefulWidget> createState() => AppNavbarState();
}
class AppNavbarState extends State<AppNavbar> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return BottomNavigationBar(
currentIndex: widget.currentPageIndex,
onTap: widget.onTapNavbarItem,
items: bottomNavItems,
type: BottomNavigationBarType.fixed,
selectedItemColor: colors.primary,
unselectedItemColor: colors.onSurface.withAlpha(150),
showSelectedLabels: true,
showUnselectedLabels: true,
backgroundColor: colors.surface,
elevation: 8,
);
}
}

View File

@@ -1,40 +0,0 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/models/layout.dart';
import 'package:food_hub_app/views/moment.dart';
import 'package:food_hub_app/views/profile.dart';
import 'package:food_hub_app/views/record.dart';
import 'package:food_hub_app/views/stats.dart';
final List<PageInfo> pages = [
PageInfo(
label: "记录",
icon: Icons.home_outlined,
activeIcon: Icons.home,
page: RecordPage(),
),
PageInfo(
label: "统计",
icon: Icons.pie_chart_outline,
activeIcon: Icons.pie_chart,
page: StatsPage(),
),
PageInfo(
label: "朋友圈",
icon: Icons.group_outlined,
activeIcon: Icons.group,
page: MomentPage(),
),
PageInfo(
label: "我的",
icon: Icons.account_circle_outlined,
activeIcon: Icons.account_circle,
page: ProfilePage(),
),
];
final List<BottomNavigationBarItem> bottomNavItems =
pages.map((page) {
return BottomNavigationBarItem(icon: Icon(page.icon), label: page.label);
}).toList();
final List<Widget> tabPages = pages.map((item) => item.page).toList();

View File

@@ -16,24 +16,30 @@ import 'package:food_hub_app/views/recipe_form.dart';
import 'package:food_hub_app/views/record_form.dart';
import 'package:food_hub_app/views/recipe_detail.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:liquid_glass_widgets/liquid_glass_setup.dart';
import 'package:provider/provider.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
FormBuilderLocalizations.delegate.load(const Locale('zh', 'CN'));
await SPUtil.init();
await initLogger();
// await initLogger();
// 预编译 shader防止首帧白闪
await LiquidGlassWidgets.initialize();
// wrap() 安装无障碍桥接、全局主题、自适应质量
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => FoodProvider()),
ChangeNotifierProvider(create: (context) => ThemeProvider()),
ChangeNotifierProvider(create: (context) => UserProvider()),
ChangeNotifierProvider(create: (context) => AppProvider()),
],
child: MyApp(),
LiquidGlassWidgets.wrap(
child: MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => FoodProvider()),
ChangeNotifierProvider(create: (context) => ThemeProvider()),
ChangeNotifierProvider(create: (context) => UserProvider()),
ChangeNotifierProvider(create: (context) => AppProvider()),
],
child: const MyApp(),
),
),
);
}
@@ -45,10 +51,52 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final themeProvider = context.watch<ThemeProvider>();
final brightness = MediaQuery.of(context).platformBrightness;
themeProvider.isDarkMode = brightness == Brightness.dark;
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: themeProvider.currentThemeData,
theme: ThemeData(
scaffoldBackgroundColor: const Color(0xFFFAF6F0),
colorScheme: ColorScheme(
brightness: Brightness.light,
primary: const Color(0xFFE89F71),
secondary: const Color(0xFFE89F71),
surface: const Color(0xFFFFFFFF),
error: const Color(0xFFE76F51),
onPrimary: Colors.white,
onSecondary: Colors.white,
onSurface: const Color(0xFF3A332C),
onError: Colors.white,
),
textTheme: TextTheme(
titleLarge: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: const Color(0xFF3A332C),
),
titleMedium: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: const Color(0xFF3A332C),
),
bodyLarge: TextStyle(
fontSize: 15,
color: const Color(0xFF3A332C),
),
bodyMedium: TextStyle(
fontSize: 14,
color: const Color(0xFF8C8379),
),
bodySmall: TextStyle(
fontSize: 12,
color: const Color(0xFF8C8379),
),
),
fontFamily: 'CustomFont',
useMaterial3: true,
),
supportedLocales: const [
Locale('en', 'US'), // 英语
Locale('zh', 'CN'), // 中文
@@ -71,13 +119,13 @@ class MyApp extends StatelessWidget {
home: LoginPage(),
routes: {
'/home': (context) => HomePage(),
'/recordForm': (context) => RecordFormPage(),
'/recordForm': (context) => RecordForm(),
'/recipeForm': (context) => RecipeFormPage(),
'/recipeDetail': (context) => RecipeDetailPage(),
'/momentForm': (context) => MomentFormPage(),
'/momentForm': (context) => MomentForm(),
'/moment': (context) => MomentPage(),
'/momentUser': (context) => MomentUserPage(),
'/ProfileUser': (context) => ProfileUserPage()
'/ProfileUser': (context) => ProfileUserPage(),
},
);
}

View File

@@ -0,0 +1,43 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter_common/utils/file_utils.dart';
import 'package:food_hub_app/config/app_config.dart';
import 'package:minio/io.dart';
import 'package:minio/minio.dart';
class MinIOHelper {
static final MinIOHelper _instance = MinIOHelper._internal();
factory MinIOHelper() => _instance;
final String ip = AppConfig.rustfsIp;
MinIOHelper._internal() {
_minio = Minio(
endPoint: AppConfig.rustfsIp,
port: AppConfig.rustfsPort,
accessKey: AppConfig.rustfsAccessKey,
secretKey: AppConfig.rustfsSecretKey,
useSSL: false,
);
}
late Minio _minio;
Future<String> uploadFile({
required PlatformFile file,
required String bucketName,
Function(double)? onProgress,
}) async {
try {
String hashName = await generateMD5HashName(file.path!);
String fileName = '$hashName${getFileExtension(file.name)}';
await _minio.fPutObject(bucketName, fileName, file.path!);
return fileName;
} catch (e) {
throw Exception('文件上传失败: $e');
}
}
}

View File

@@ -1,8 +1,14 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/layout/app_actions.dart';
import 'package:food_hub_app/layout/app_drawer.dart';
import 'package:food_hub_app/layout/app_navbar.dart';
import 'package:food_hub_app/layout/menu.dart';
import 'package:food_hub_app/views/profile.dart';
import 'package:food_hub_app/views/record.dart';
import 'package:food_hub_app/views/stats.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
import 'package:liquid_glass_widgets/widgets/shared/glass_page.dart';
import 'package:liquid_glass_widgets/widgets/surfaces/glass_app_bar.dart';
import 'package:liquid_glass_widgets/widgets/surfaces/glass_bottom_bar.dart';
import 'package:liquid_glass_widgets/widgets/surfaces/glass_scaffold.dart';
import 'moment.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@@ -14,38 +20,52 @@ class HomePage extends StatefulWidget {
class _HomePage extends State<HomePage> {
int _currentIndex = 0;
final _pages = [
const RecordPage(),
const StatsPage(),
const MomentPage(),
const ProfilePage(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('食光集', style: TextStyle(color: Colors.white)),
backgroundColor: Theme.of(context).colorScheme.primary,
leading: Builder(
builder: (context) {
return IconButton(
icon: const Icon(Icons.menu),
color: Colors.white,
onPressed: () => Scaffold.of(context).openDrawer(),
);
},
final colors = Theme.of(context).colorScheme;
return GlassScaffold(
statusBarStyle: GlassStatusBarStyle.auto,
bottomBar: GlassBottomBar(
settings: LiquidGlassSettings(
glassColor: colors.surface
),
actions: [AppActions(pageIndex: _currentIndex)],
),
// backgroundColor: Color(0xFFF5F5F5),
drawer: AppDrawer(),
body: Padding(
padding: EdgeInsets.all(10),
child: tabPages[_currentIndex],
),
bottomNavigationBar: AppNavbar(
currentPageIndex: _currentIndex,
onTapNavbarItem: (index) {
setState(() {
_currentIndex = index;
});
},
selectedIndex: _currentIndex,
onTabSelected: (i) => setState(() => _currentIndex = i),
indicatorColor: colors.primary,
selectedIconColor: colors.surface,
unselectedIconColor: Color(0xFF000000),
tabs: const [
GlassBottomBarTab(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: '记录',
),
GlassBottomBarTab(
icon: Icon(Icons.pie_chart_outline),
activeIcon: Icon(Icons.pie_chart),
label: '统计',
),
GlassBottomBarTab(
icon: Icon(Icons.group_outlined),
activeIcon: Icon(Icons.group),
label: '朋友圈',
),
GlassBottomBarTab(
icon: Icon(Icons.account_circle_outlined),
activeIcon: Icon(Icons.account_circle),
label: '我的',
),
],
),
body: _pages[_currentIndex],
);
}
}

View File

@@ -83,7 +83,7 @@ class _LoginPage extends State<LoginPage> {
handleRememberState();
handleTokenState(session.saToken.tokenValue);
handleUserInfo(session.userInfo);
LoadingDialog.hide(context);
Navigator.pushNamed(context, '/home');
} catch (e) {
@@ -104,12 +104,12 @@ class _LoginPage extends State<LoginPage> {
key: _formKey,
child: Column(
children: [
Expanded(child: buildLoginForm()),
Expanded(child: buildLoginForm(context)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 50),
child: Column(
children: [
buildOtherLoginText(),
buildOtherLoginText(context),
const SizedBox(height: 15),
buildOtherMethod(context),
const SizedBox(height: 20),
@@ -131,29 +131,29 @@ class _LoginPage extends State<LoginPage> {
);
}
Widget buildLoginForm() {
Widget buildLoginForm(BuildContext context) {
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 20),
children: [
const SizedBox(height: kToolbarHeight),
buildTitle(),
const SizedBox(height: 60),
buildUsernameTextField(),
buildUsernameTextField(context),
const SizedBox(height: 20),
buildPasswordTextField(),
buildPasswordTextField(context),
const SizedBox(height: 10),
buildRememberPasswordCheckbox(),
buildRememberPasswordCheckbox(context),
const SizedBox(height: 20),
buildLoginButton(),
buildLoginButton(context),
],
);
}
Widget buildUsernameTextField() {
Widget buildUsernameTextField(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
formLabelText(labelText: "账号:"),
Text("账号:", style: Theme.of(context).textTheme.bodyLarge),
const SizedBox(height: 10),
FormBuilderTextField(
name: 'username',
@@ -171,11 +171,11 @@ class _LoginPage extends State<LoginPage> {
);
}
Widget buildPasswordTextField() {
Widget buildPasswordTextField(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
formLabelText(labelText: "密码:"),
Text("密码:", style: Theme.of(context).textTheme.bodyLarge),
const SizedBox(height: 10),
FormBuilderTextField(
name: 'password',
@@ -205,7 +205,7 @@ class _LoginPage extends State<LoginPage> {
);
}
Widget buildRememberPasswordCheckbox() {
Widget buildRememberPasswordCheckbox(BuildContext context) {
return Row(
children: [
Checkbox(
@@ -218,12 +218,12 @@ class _LoginPage extends State<LoginPage> {
});
},
),
const Text('记住密码', style: TextStyle(color: Colors.grey, fontSize: 14)),
Text('记住密码', style: Theme.of(context).textTheme.bodyMedium),
],
);
}
Widget buildLoginButton() {
Widget buildLoginButton(BuildContext context) {
return SizedBox(
height: 45,
width: double.infinity,
@@ -243,23 +243,20 @@ class _LoginPage extends State<LoginPage> {
);
}
Widget buildOtherLoginText() {
Widget buildOtherLoginText(BuildContext context) {
return Row(
children: [
Expanded(child: Divider(color: Colors.grey[300], thickness: 1)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'其他方式登录',
style: TextStyle(color: Colors.grey, fontSize: 14),
),
child: Text('其他方式登录', style: Theme.of(context).textTheme.bodyMedium),
),
Expanded(child: Divider(color: Colors.grey[300], thickness: 1)),
],
);
}
Widget buildOtherMethod(context) {
Widget buildOtherMethod(BuildContext context) {
return OverflowBar(
alignment: MainAxisAlignment.center,
children:
@@ -292,16 +289,19 @@ class _LoginPage extends State<LoginPage> {
);
}
Widget buildRegisterText(context) {
Widget buildRegisterText(BuildContext context) {
return Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('没有账号?', style: TextStyle(fontSize: 18)),
Text('没有账号?', style: Theme.of(context).textTheme.bodyMedium),
GestureDetector(
child: const Text(
child: Text(
'点击注册',
style: TextStyle(fontSize: 18, color: Colors.green),
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.primary,
),
),
onTap: () {
print("点击注册");

View File

@@ -59,8 +59,8 @@ class _MomentPageState extends State<MomentPage> {
}
void _onScroll() {
// 当滚动距离超过300时显示返回顶部按钮
if (_scrollController.offset > 300) {
// 当滚动距离超过时显示返回顶部按钮
if (_scrollController.offset > 100) {
if (!_showScrollToTop) {
setState(() {
_showScrollToTop = true;
@@ -99,26 +99,38 @@ class _MomentPageState extends State<MomentPage> {
}
// 有数据时显示列表
return Stack(
children: [
buildEasyRefresh(
freshController: _freshController,
onRefresh: _onRefresh,
onLoad: _onLoad,
body: Stack(
children: [
if (provider.isLoading)
buildLoadingIndicator()
else
_buildMomentList(provider),
],
),
),
return Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
title: Text('朋友圈', style: Theme.of(context).textTheme.titleLarge),
centerTitle: true,
automaticallyImplyLeading: false,
),
body: Padding(
padding: EdgeInsetsGeometry.all(10),
child: Stack(
children: [
buildEasyRefresh(
freshController: _freshController,
onRefresh: _onRefresh,
onLoad: _onLoad,
body: Stack(
children: [
if (provider.isLoading)
buildLoadingIndicator()
else
_buildMomentList(provider),
],
),
),
// 返回顶部按钮
if (_showScrollToTop)
buildScrollToTop(context: context, scrollToTop: _scrollToTop),
],
// 返回顶部按钮
if (_showScrollToTop)
buildScrollToTop(context: context, scrollToTop: _scrollToTop),
],
),
),
);
}
}

View File

@@ -1,26 +1,27 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/utils/log_utils.dart';
import 'package:flutter_common/utils/minio_utils.dart';
import 'package:flutter_common/utils/toast_util.dart';
import 'package:flutter_common/widget/common_widget.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:food_hub_app/config/app_config.dart';
import 'package:food_hub_app/provider/food_provider.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/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';
class MomentFormPage extends StatefulWidget {
const MomentFormPage({super.key});
class MomentForm extends StatefulWidget {
const MomentForm({super.key});
@override
State<MomentFormPage> createState() => _MomentFormPageState();
State<MomentForm> createState() => _MomentFormState();
}
class _MomentFormPageState extends State<MomentFormPage> {
class _MomentFormState extends State<MomentForm> {
final _formKey = GlobalKey<FormBuilderState>();
final _focusNode = FocusNode();
static const String _contentField = 'content';
@@ -34,8 +35,8 @@ class _MomentFormPageState extends State<MomentFormPage> {
name: _contentField,
initialValue: provider.momentFormItem.content,
focusNode: _focusNode,
maxLines: 5,
minLines: 3,
maxLines: 8,
minLines: 5,
maxLength: maxContentLength,
buildCounter: (
context, {
@@ -75,10 +76,10 @@ class _MomentFormPageState extends State<MomentFormPage> {
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 4/3,
childAspectRatio: 4 / 3,
),
itemCount: totalItems,
itemBuilder: (context, index) {
@@ -111,7 +112,11 @@ class _MomentFormPageState extends State<MomentFormPage> {
_buildContentField(provider),
const SizedBox(height: 8),
buildFormLabel(context: context, text: '上传图片最多9张', isRequired: false),
buildFormLabel(
context: context,
text: '上传图片最多9张',
isRequired: false,
),
const SizedBox(height: 8),
_buildImageField(provider),
const SizedBox(height: 8),
@@ -146,24 +151,27 @@ class _MomentFormPageState extends State<MomentFormPage> {
if (result != null) {
// 限制选择数量
final filesToUpload = result.files.take(remainingCount).toList();
LoadingDialog.show(context, message: '上传中');
try {
final newImageUrls = <String>[];
// 逐个上传图片
for (final file in filesToUpload) {
final fileName = await MinIOHelper().uploadFile(
bucketName: AppConfig.bucketName,
file: file,
);
logger.i('图片名称:$fileName');
newImageUrls.add(fileName);
}
final newImageUrls = <String>[];
// 逐个上传图片
for (final file in filesToUpload) {
final fileName = await MinIOHelper().uploadFile(
bucketName: AppConfig.bucketName,
file: file,
);
logger.i('图片名称:$fileName');
newImageUrls.add(fileName);
// 更新图片列表
provider.addMomentFormImage(newImageUrls);
} catch (e) {
ToastUtil.error(e.toString());
} finally {
LoadingDialog.hide(context);
}
// 更新图片列表
LoadingDialog.hide(context);
provider.addMomentFormImage(newImageUrls);
}
}
@@ -173,7 +181,7 @@ class _MomentFormPageState extends State<MomentFormPage> {
}
/// 提交表单
void _submitForm(FoodProvider provider) async{
void _submitForm(FoodProvider provider) async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
LoadingDialog.show(context, message: '提交中');
final result = await provider.handleMoment();
@@ -205,24 +213,17 @@ class _MomentFormPageState extends State<MomentFormPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final provider = context.watch<FoodProvider>();
final String title = provider.isEditing ? '编辑朋友圈' : '新增朋友圈';
return Scaffold(
appBar: AppBar(
title: Text(title, 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: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(10),
child: CommonCard(child: _buildFormBuilder(provider)),
return SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(10),
child: GlassCard(
useOwnLayer: true,
settings: LiquidGlassSettings(
glassColor: Theme.of(context).scaffoldBackgroundColor,
),
child: _buildFormBuilder(provider),
),
),
);

View File

@@ -5,6 +5,8 @@ import 'package:flutter_common/widget/dialog_widget.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/profile/basic_info.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';
class ProfilePage extends StatefulWidget {
@@ -44,8 +46,13 @@ class ProfilePageState extends State<ProfilePage> {
Navigator.pushNamed(context, '/momentUser');
}
Widget _buildFunctionButtons() {
return CommonCard(
Widget _buildFunctionButtons(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return GlassCard(
padding: EdgeInsetsGeometry.symmetric(vertical: 0, horizontal: 10),
useOwnLayer: true,
settings: LiquidGlassSettings(glassColor: colors.surface),
child: Column(
children: [
_buildFunctionButton(
@@ -56,13 +63,13 @@ class ProfilePageState extends State<ProfilePage> {
// LogUtils.i('点击编辑资料');
},
),
const Divider(height: 1),
const Divider(height: 1, thickness: 0.2),
_buildFunctionButton(
icon: Icons.group_outlined,
text: '朋友圈',
onTap: _onTapMoment,
),
const Divider(height: 1),
const Divider(height: 1, thickness: 0.2),
_buildFunctionButton(
icon: Icons.favorite,
text: '我的收藏',
@@ -71,7 +78,7 @@ class ProfilePageState extends State<ProfilePage> {
// LogUtils.i('点击浏览历史');
},
),
const Divider(height: 1),
const Divider(height: 1, thickness: 0.2),
_buildFunctionButton(
icon: Icons.lock,
text: '更改密码',
@@ -80,11 +87,11 @@ class ProfilePageState extends State<ProfilePage> {
// LogUtils.i('点击帮助与反馈');
},
),
const Divider(height: 1),
const Divider(height: 1, thickness: 0.2),
_buildFunctionButton(icon: Icons.message, text: '反馈意见', onTap: () {}),
const Divider(height: 1),
const Divider(height: 1, thickness: 0.2),
_buildFunctionButton(icon: Icons.refresh, text: '检查更新', onTap: () {}),
const Divider(height: 1),
const Divider(height: 1, thickness: 0.2),
_buildFunctionButton(
icon: Icons.exit_to_app,
text: '退出登录',
@@ -117,13 +124,13 @@ class ProfilePageState extends State<ProfilePage> {
);
}
Widget _buildContent(UserProvider provider) {
Widget _buildContent(UserProvider provider, BuildContext context) {
final user = provider.currentUser;
return Column(
children: [
SizedBox(width: double.infinity, child: ProfileInfo(user: user)),
const SizedBox(height: 16),
_buildFunctionButtons(),
_buildFunctionButtons(context),
const SizedBox(height: 8),
_buildVersionInfo(),
],
@@ -134,15 +141,27 @@ class ProfilePageState extends State<ProfilePage> {
Widget build(BuildContext context) {
final provider = context.watch<UserProvider>();
return Stack(
children: [
if (provider.isLoading)
buildLoadingIndicator()
else if (provider.error.isNotEmpty)
Text(provider.error)
else
_buildContent(provider),
],
return Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
title: Text('我的', style: Theme.of(context).textTheme.titleLarge),
centerTitle: true,
automaticallyImplyLeading: false,
),
body: Padding(
padding: EdgeInsetsGeometry.all(10),
child: Stack(
children: [
if (provider.isLoading)
buildLoadingIndicator()
else if (provider.error.isNotEmpty)
Text(provider.error)
else
SingleChildScrollView(child: _buildContent(provider, context)),
],
),
),
);
}
}

View File

@@ -1,5 +1,10 @@
import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:food_hub_app/provider/food_provider.dart';
import 'package:food_hub_app/views/moment_form.dart';
import 'package:food_hub_app/views/record_form.dart';
import 'package:food_hub_app/widgets/common/easy_refresh.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:food_hub_app/widgets/recipe/calendar.dart';
import 'package:food_hub_app/widgets/recipe/list.dart';
@@ -24,12 +29,68 @@ class RecordPage extends StatefulWidget {
}
class _RecordPageState extends State<RecordPage> {
final EasyRefreshController _freshController = EasyRefreshController(
controlFinishRefresh: true,
controlFinishLoad: true,
);
final ScrollController _scrollController = ScrollController();
bool _showScrollToTop = false;
final List<Widget> _tabPages = [
RecipeList(),
RecipeCalendar(),
RecipeTimeline(),
];
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_scrollController.removeListener(_onScroll);
_freshController.dispose();
_scrollController.dispose();
super.dispose();
}
// 下拉刷新
Future<void> _onRefresh() async {
_freshController.finishRefresh();
}
// 上拉加载
Future<void> _onLoad() async {
final provider = context.read<FoodProvider>();
if (provider.hasMore) {
await provider.refreshFoodStats();
_freshController.finishLoad(IndicatorResult.success);
} else {
_freshController.finishLoad(IndicatorResult.noMore);
}
}
void _onScroll() {
// 当滚动距离超过时显示返回顶部按钮
if (_scrollController.offset > 100) {
if (!_showScrollToTop) {
setState(() {
_showScrollToTop = true;
});
}
} else {
if (_showScrollToTop) {
setState(() {
_showScrollToTop = false;
});
}
}
}
// 滚动到顶部
void _scrollToTop() => scrollToTopAnimateTo(_scrollController);
Widget _buildTabs({
required BuildContext context,
required RecordTab currentTab,
@@ -52,24 +113,135 @@ class _RecordPageState extends State<RecordPage> {
provider.onRecordTabChange();
}
void _handleAddRecord(FoodProvider provider) {
provider.resetRecordForm();
provider.isEditing = false;
Navigator.pop(context);
showModalBottomSheet(
context: context,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => RecordForm(),
);
}
void _handleAddMoment(FoodProvider provider) {
provider.resetMomentForm();
provider.isEditing = false;
Navigator.pop(context);
showModalBottomSheet(
context: context,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => MomentForm(),
);
}
void _onPressAdd(FoodProvider provider) {
showModalBottomSheet(
context: context,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder:
(context) => Padding(
padding: EdgeInsetsGeometry.symmetric(horizontal: 0, vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
Text('请选择操作', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 10),
ListTile(
leading: Icon(
Icons.book,
color: Theme.of(context).colorScheme.primary,
),
title: Text(
'新增菜谱',
style: Theme.of(context).textTheme.bodyLarge,
),
onTap: () => {},
),
ListTile(
leading: Icon(
Icons.note_add,
color: Theme.of(context).colorScheme.primary,
),
title: Text(
'新增记录',
style: Theme.of(context).textTheme.bodyLarge,
),
onTap: () => _handleAddRecord(provider),
),
ListTile(
leading: Icon(
Icons.group,
color: Theme.of(context).colorScheme.primary,
),
title: Text(
'发布朋友圈',
style: Theme.of(context).textTheme.bodyLarge,
),
onTap: () => _handleAddMoment(provider),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
final provider = context.watch<FoodProvider>();
return Column(
children: [
_buildTabs(
context: context,
currentTab: provider.currentRecordTab,
onTabChanged: (tab) => _onTabChange(tab, provider),
return Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
elevation: 0,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
title: Text('记录', style: Theme.of(context).textTheme.titleLarge),
centerTitle: true,
leading: Builder(
builder:
(context) => IconButton(
icon: Icon(Icons.menu, color: Theme.of(context).primaryColor),
onPressed: () => {},
),
),
Expanded(
child: IndexedStack(
index: provider.currentRecordTab.index,
children: _tabPages,
actions: [
IconButton(
icon: Icon(Icons.search, color: Theme.of(context).primaryColor),
onPressed: () {},
),
IconButton(
icon: Icon(Icons.add, color: Theme.of(context).primaryColor),
onPressed: () => _onPressAdd(provider),
),
],
),
body: Padding(
padding: const EdgeInsets.all(10),
child: Column(
children: [
_buildTabs(
context: context,
currentTab: provider.currentRecordTab,
onTabChanged: (tab) => _onTabChange(tab, provider),
),
Expanded(child: _tabPages[provider.currentRecordTab.index]),
],
),
],
),
);
}
}

View File

@@ -1,28 +1,28 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/utils/minio_utils.dart';
import 'package:flutter_common/utils/toast_util.dart';
import 'package:flutter_common/widget/common_widget.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:food_hub_app/apis/recipe.dart';
import 'package:food_hub_app/config/app_config.dart';
import 'package:food_hub_app/provider/food_provider.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/image.dart';
import 'package:intl/intl.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
import 'package:provider/provider.dart';
class RecordFormPage extends StatefulWidget {
const RecordFormPage({super.key});
class RecordForm extends StatefulWidget {
const RecordForm({super.key});
@override
State<RecordFormPage> createState() => _RecordFormPageState();
State<RecordForm> createState() => _RecordFormState();
}
class _RecordFormPageState extends State<RecordFormPage> {
class _RecordFormState extends State<RecordForm> {
final _formKey = GlobalKey<FormBuilderState>();
static const String _nameField = 'name';
static const String _dateField = 'date';
@@ -49,19 +49,19 @@ class _RecordFormPageState extends State<RecordFormPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildFormLabel(context: context, text: '菜谱名称', isRequired: true),
const SizedBox(height: 8),
const SizedBox(height: 12),
_buildRecipeField(provider),
const SizedBox(height: 8),
const SizedBox(height: 12),
buildFormLabel(context: context, text: '完成时间', isRequired: true),
const SizedBox(height: 8),
const SizedBox(height: 12),
_buildDateField(provider),
const SizedBox(height: 8),
const SizedBox(height: 12),
buildFormLabel(context: context, text: '上传图片', isRequired: true),
const SizedBox(height: 8),
const SizedBox(height: 12),
_buildImageField(provider),
const SizedBox(height: 8),
const SizedBox(height: 12),
buildFormButtonGroup(
context: context,
@@ -254,13 +254,18 @@ class _RecordFormPageState extends State<RecordFormPage> {
PlatformFile file = result.files.first;
LoadingDialog.show(context, message: '上传中');
final fileName = await MinIOHelper().uploadFile(
bucketName: AppConfig.bucketName,
file: file,
);
LoadingDialog.hide(context);
provider.updateRecordFormItem(imageUrl: fileName);
try {
final fileName = await MinIOHelper().uploadFile(
bucketName: AppConfig.bucketName,
file: file,
);
ToastUtil.success('上传成功');
provider.updateRecordFormItem(imageUrl: fileName);
} catch (e) {
ToastUtil.error(e.toString());
} finally {
LoadingDialog.hide(context);
}
}
}
@@ -326,24 +331,17 @@ class _RecordFormPageState extends State<RecordFormPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final provider = context.watch<FoodProvider>();
final String title = provider.isEditing ? '编辑记录' : '新增记录';
return Scaffold(
appBar: AppBar(
title: Text(title, style: TextStyle(color: Colors.white)),
backgroundColor: colors.primary,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
),
body: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(10),
child: CommonCard(child: _buildFormBuilder(provider)),
return SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(10),
child: GlassCard(
useOwnLayer: true,
settings: LiquidGlassSettings(
glassColor: Theme.of(context).scaffoldBackgroundColor,
),
child: _buildFormBuilder(provider),
),
),
);

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_common/widget/chart.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:food_hub_app/provider/food_provider.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
import 'package:provider/provider.dart';
class StatsPage extends StatefulWidget {
@@ -24,7 +25,9 @@ class _StatsPage extends State<StatsPage> {
});
}
Widget _buildSummaryStats(FoodProvider provider) {
Widget _buildSummaryStats(BuildContext context, FoodProvider provider) {
final colors = Theme.of(context).colorScheme;
return GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
@@ -34,42 +37,62 @@ class _StatsPage extends State<StatsPage> {
mainAxisSpacing: 8,
childAspectRatio: 2,
children: [
StatsCard(
icon: Icons.restaurant_menu,
title: '菜谱总数',
value: provider.summaryStats.recipeCount.toString(),
unit: '',
GlassCard(
useOwnLayer: true,
settings: LiquidGlassSettings(glassColor: colors.surface),
child: StatsCard(
icon: Icons.restaurant_menu,
title: '菜谱总数',
value: provider.summaryStats.recipeCount.toString(),
unit: '',
),
),
StatsCard(
icon: Icons.grid_view_rounded,
title: '菜谱类别',
value: provider.summaryStats.categoryCount.toString(),
unit: '',
GlassCard(
useOwnLayer: true,
settings: LiquidGlassSettings(glassColor: colors.surface),
child: StatsCard(
icon: Icons.grid_view_rounded,
title: '菜谱类别',
value: provider.summaryStats.categoryCount.toString(),
unit: '',
),
),
StatsCard(
icon: Icons.flag,
title: '做菜次数',
value: provider.summaryStats.workCount.toString(),
unit: '',
GlassCard(
useOwnLayer: true,
settings: LiquidGlassSettings(glassColor: colors.surface),
child: StatsCard(
icon: Icons.flag,
title: '做菜次数',
value: provider.summaryStats.workCount.toString(),
unit: '',
),
),
StatsCard(
icon: Icons.show_chart,
title: '平均次数',
value: provider.averageRecordCount.toStringAsFixed(2),
unit: '次/月',
GlassCard(
useOwnLayer: true,
settings: LiquidGlassSettings(glassColor: colors.surface),
child: StatsCard(
icon: Icons.show_chart,
title: '平均次数',
value: provider.averageRecordCount.toStringAsFixed(2),
unit: '次/月',
),
),
],
);
}
Widget _buildContent(FoodProvider provider) {
Widget _buildContent(BuildContext context, FoodProvider provider) {
final colors = Theme.of(context).colorScheme;
return Column(
children: [
_buildSummaryStats(provider),
_buildSummaryStats(context, provider),
SizedBox(height: 8),
SizedBox(
height: chartHeight,
child: CommonCard(
child: GlassCard(
useOwnLayer: true,
settings: LiquidGlassSettings(glassColor: colors.surface),
child: LineChart(
title: '记录统计',
xAxisName: '日期',
@@ -82,7 +105,9 @@ class _StatsPage extends State<StatsPage> {
SizedBox(height: 8),
SizedBox(
height: chartHeight,
child: CommonCard(
child: GlassCard(
useOwnLayer: true,
settings: LiquidGlassSettings(glassColor: colors.surface),
child: PieChart(
title: '菜谱统计',
unit: '',
@@ -93,7 +118,9 @@ class _StatsPage extends State<StatsPage> {
SizedBox(height: 8),
SizedBox(
height: chartHeight,
child: CommonCard(
child: GlassCard(
useOwnLayer: true,
settings: LiquidGlassSettings(glassColor: colors.surface),
child: RankChart(title: '排行榜', data: provider.rankStats, unit: ''),
),
),
@@ -105,13 +132,28 @@ class _StatsPage extends State<StatsPage> {
Widget build(BuildContext context) {
final provider = context.watch<FoodProvider>();
return Stack(
children: [
if (provider.isLoading)
buildLoadingIndicator()
else
SingleChildScrollView(child: _buildContent(provider)),
],
return Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
title: Text('统计', style: Theme.of(context).textTheme.titleLarge),
centerTitle: true,
automaticallyImplyLeading: false,
),
body: Padding(
padding: EdgeInsetsGeometry.all(10),
child: Stack(
children: [
if (provider.isLoading)
buildLoadingIndicator()
else
SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 90),
child: _buildContent(context, provider),
),
],
),
),
);
}
}

View File

@@ -43,15 +43,17 @@ Widget buildScrollToTop({
required BuildContext context,
required VoidCallback scrollToTop,
}) {
final colors = Theme.of(context).colorScheme;
return Positioned(
right: 0,
bottom: 20,
bottom: 80,
child: FloatingActionButton(
onPressed: scrollToTop,
backgroundColor: Theme.of(context).colorScheme.primary,
backgroundColor: colors.primary,
elevation: 2,
mini: true,
child: const Icon(Icons.arrow_upward, color: Colors.white),
child: Icon(Icons.arrow_upward, color: colors.surface),
),
);
}

View File

@@ -12,26 +12,26 @@ InputDecoration buildInputDecoration({
return InputDecoration(
hintText: hintText,
hintStyle: TextStyle(color: colors.onSurface.withAlpha(100)),
hintStyle: Theme.of(context).textTheme.bodyMedium,
filled: true,
fillColor: colors.surface,
border: OutlineInputBorder(
borderSide: const BorderSide(color: Color(0xFFE5E7EB)),
borderRadius: BorderRadius.circular(8),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(10),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: colors.primary, width: 2),
borderRadius: BorderRadius.circular(8),
borderRadius: BorderRadius.circular(10),
),
errorBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.red, width: 2),
borderRadius: BorderRadius.circular(8),
borderRadius: BorderRadius.circular(10),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 14),
errorStyle: const TextStyle(fontSize: 12, height: 1, color: Colors.red),
prefixIcon:
prefixIcon != null
? Icon(prefixIcon, size: 20, color: Color(0xFF86909C))
? Icon(prefixIcon, size: 20, color: colors.primary)
: null,
prefixIconConstraints: const BoxConstraints(minWidth: 40),
isDense: true,

View File

@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:food_hub_app/config/app_config.dart';
import 'package:photo_view/photo_view.dart';
@@ -53,6 +54,8 @@ class _ImagePreviewPageState extends State<ImagePreviewPage> {
@override
Widget build(BuildContext context) {
final index = '${_currentIndex + 1}/${widget.images.length}';
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
@@ -63,10 +66,7 @@ class _ImagePreviewPageState extends State<ImagePreviewPage> {
onPressed: () => Navigator.pop(context),
),
// 显示实时更新的页码(当前索引+1 / 总数量)
title: Text(
'${_currentIndex + 1}/${widget.images.length}',
style: const TextStyle(color: Colors.white),
),
title: Text(index, style: const TextStyle(color: Colors.white)),
centerTitle: true,
),
body: PhotoViewGallery(
@@ -95,21 +95,6 @@ class CommonImage extends StatelessWidget {
const CommonImage({super.key, required this.imageUrls, required this.index});
Widget _buildImageLoadingIndicator(ImageChunkEvent? progress) {
final value =
progress?.expectedTotalBytes != null
? progress!.cumulativeBytesLoaded / progress.expectedTotalBytes!
: null;
return Center(
child: SizedBox(
width: 30,
height: 30,
child: CircularProgressIndicator(value: value),
),
);
}
Widget _buildErrorImage() {
return Container(
color: Colors.grey[200],
@@ -140,14 +125,16 @@ class CommonImage extends StatelessWidget {
onTap: () {
_navigateToImagePreview(context, imageUrls, index);
},
child: Image.network(
'${AppConfig.imageBaseUrl}${imageUrls[index]}',
child: CachedNetworkImage(
imageUrl: '${AppConfig.imageBaseUrl}${imageUrls[index]}',
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return _buildImageLoadingIndicator(loadingProgress);
},
errorBuilder: (context, error, stackTrace) => _buildErrorImage(),
progressIndicatorBuilder:
(context, url, downloadProgress) => CircularProgressIndicator(
strokeWidth: 1.5,
padding: EdgeInsetsGeometry.all(20),
value: downloadProgress.progress,
),
errorWidget: (context, error, stackTrace) => _buildErrorImage(),
),
),
),
@@ -213,19 +200,19 @@ Widget buildImageUploadButton({
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
width: 96,
height: 96,
width: double.infinity,
height: 250,
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.black),
border: Border.all(color: colors.surface),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.add, color: colors.onSurface.withAlpha(100)),
Icon(Icons.camera_alt, color: colors.onSurface.withAlpha(100)),
SizedBox(height: 6),
Text('添加图片', style: TextStyle(color: colors.onSurface.withAlpha(100))),
Text('点击上传美食图片', style: Theme.of(context).textTheme.bodyMedium),
],
),
),

View File

@@ -43,8 +43,8 @@ Widget buildToggleSwitch<T extends Enum>({
totalSwitches: tabValues.length,
labels: labels,
activeBgColor: [colors.primary],
activeFgColor: Colors.white,
inactiveBgColor: colors.surfaceContainer,
activeFgColor: colors.surface,
inactiveBgColor: colors.surface,
inactiveFgColor: colors.onSurface,
cornerRadius: 12.0,
customTextStyles: [TextStyle(fontSize: 12, fontWeight: FontWeight.w500)],
@@ -85,10 +85,10 @@ Widget buildTag(BuildContext context, String title) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: colors.primary.withAlpha(60),
color: colors.primary,
borderRadius: BorderRadius.circular(10),
),
child: Text(title, style: TextStyle(color: colors.primary)),
child: Text(title, style: TextStyle(color: colors.surface)),
);
}
@@ -135,24 +135,10 @@ Widget buildInfoButton({
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
elevation: 0,
),
child: Text(text, style: TextStyle(color: Colors.white)),
child: Text(text),
);
}
/// 取消按钮样式
ButtonStyle cancelButtonStyle() {
return ButtonStyle(
backgroundColor: WidgetStateProperty.all(Colors.grey),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
}
Text buttonText({required String text}) {
return Text(text, style: TextStyle(color: Colors.white));
}
/// 图片错误显示
Widget errorImageContainer(double height) {
return Container(

View File

@@ -7,7 +7,6 @@ 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 StatefulWidget {
final Moment moment;
@@ -26,21 +25,24 @@ class MomentCardState extends State<MomentCard> {
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(onTap: () => _onTapUser(context), child: _buildAvatar()),
InkWell(
onTap: () => _onTapUser(context),
child: _buildAvatar(context),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildNickname(),
_buildNickname(context),
const SizedBox(height: 5),
_buildContent(),
_buildContent(context),
if (widget.moment.imageList.isNotEmpty) ...[
const SizedBox(height: 8),
_buildPostImages(),
],
const SizedBox(height: 10),
_buildTime(),
_buildTime(context),
const SizedBox(height: 5),
_buildActionButtons(),
if (widget.moment.commentList!.isNotEmpty) ...[
@@ -69,38 +71,35 @@ class MomentCardState extends State<MomentCard> {
}
// 构建头像组件
Widget _buildAvatar() {
Widget _buildAvatar(BuildContext context) {
if (widget.moment.avatar!.isEmpty) {
return TDAvatar(
size: TDAvatarSize.medium,
type: TDAvatarType.customText,
shape: TDAvatarShape.circle,
return CircleAvatar(
radius: 18,
backgroundColor: Theme.of(context).colorScheme.primary,
text: widget.moment.username?[0],
foregroundColor: Theme.of(context).colorScheme.surface,
child: Text(widget.moment.username![0], style: TextStyle(fontSize: 14)),
);
} else {
return TDAvatar(
size: TDAvatarSize.medium,
type: TDAvatarType.normal,
fit: BoxFit.contain,
avatarUrl: '${AppConfig.imageBaseUrl}/${widget.moment.avatar}',
return CircleAvatar(
radius: 18,
backgroundImage: NetworkImage('${AppConfig.imageBaseUrl}/${widget.moment.avatar}')
);
}
}
// 构建昵称组件
Widget _buildNickname() {
Widget _buildNickname(BuildContext context) {
return Text(
widget.moment.username ?? "",
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
style: Theme.of(context).textTheme.titleMedium,
);
}
// 构建内容组件
Widget _buildContent() {
Widget _buildContent(BuildContext context) {
return Text(
widget.moment.content,
style: const TextStyle(fontSize: 15, height: 1.3),
style: Theme.of(context).textTheme.bodyMedium,
);
}
@@ -142,10 +141,10 @@ class MomentCardState extends State<MomentCard> {
}
// 构建时间组件
Widget _buildTime() {
Widget _buildTime(BuildContext context) {
return Text(
widget.moment.date ?? "",
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
style: Theme.of(context).textTheme.bodySmall,
);
}
@@ -178,14 +177,14 @@ class MomentCardState extends State<MomentCard> {
alignment: Alignment.bottomLeft,
children: [
Icon(Icons.thumb_up_alt_outlined),
Positioned(
left: 20,
bottom: 15,
child: TDBadge(
TDBadgeType.message,
count: (widget.moment.likeList?.length ?? 0).toString(),
),
),
// Positioned(
// left: 20,
// bottom: 15,
// child: TDBadge(
// TDBadgeType.message,
// count: (widget.moment.likeList?.length ?? 0).toString(),
// ),
// ),
],
),
),
@@ -196,14 +195,14 @@ class MomentCardState extends State<MomentCard> {
alignment: Alignment.bottomLeft,
children: [
Icon(Icons.comment_outlined),
Positioned(
left: 20,
bottom: 15,
child: TDBadge(
TDBadgeType.message,
count: (widget.moment.commentList?.length ?? 0).toString(),
),
),
// Positioned(
// left: 20,
// bottom: 15,
// child: TDBadge(
// TDBadgeType.message,
// count: (widget.moment.commentList?.length ?? 0).toString(),
// ),
// ),
],
),
),

View File

@@ -4,7 +4,8 @@ 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';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
import 'package:liquid_glass_widgets/widgets/containers/glass_card.dart';
class ProfileInfo extends StatelessWidget {
final User user;
@@ -13,19 +14,21 @@ class ProfileInfo extends StatelessWidget {
Widget _buildAvatar(BuildContext context, User user) {
if (user.avatar == null) {
return TDAvatar(
size: TDAvatarSize.large,
type: TDAvatarType.customText,
shape: TDAvatarShape.circle,
return CircleAvatar(
radius: 24,
backgroundColor: Theme.of(context).colorScheme.primary,
text: user.username.isNotEmpty ? user.username[0] : '?',
foregroundColor: Theme.of(context).colorScheme.surface,
child: Text(
user.username.isNotEmpty ? user.username[0] : '?',
style: TextStyle(fontSize: 14),
),
);
} else {
return TDAvatar(
size: TDAvatarSize.large,
type: TDAvatarType.normal,
fit: BoxFit.contain,
avatarUrl: '${AppConfig.imageBaseUrl}/${user.avatar}',
return CircleAvatar(
radius: 24,
backgroundImage: NetworkImage(
'${AppConfig.imageBaseUrl}/${user.avatar}',
),
);
}
}
@@ -41,22 +44,24 @@ class ProfileInfo extends StatelessWidget {
children: [
Icon(icon, size: 16, color: colors.primary),
const SizedBox(width: 6),
Text(text),
Text(text, style: Theme.of(context).textTheme.bodyMedium),
],
);
}
@override
Widget build(BuildContext context) {
return CommonCard(
final colors = Theme.of(context).colorScheme;
return GlassCard(
padding: EdgeInsetsGeometry.all(10),
useOwnLayer: true,
settings: LiquidGlassSettings(glassColor: colors.surface),
child: Column(
children: [
_buildAvatar(context, user),
const SizedBox(height: 8),
Text(
user.username,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Text(user.username, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
if (user.tags != null && user.tags!.isNotEmpty)
Row(

View File

@@ -3,6 +3,7 @@ import 'package:flutter_common/widget/common_widget.dart';
import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/provider/food_provider.dart';
import 'package:food_hub_app/utils/index.dart';
import 'package:food_hub_app/views/record_form.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:provider/provider.dart';
@@ -62,19 +63,16 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
Text(formatDateTime(provider.selectedDay, 'MM月dd日 EEEE')),
SizedBox(height: 4),
if (provider.selectRecordList.isEmpty)
buildEmptyData()
SizedBox(height: 100, child: buildEmptyData())
else
Expanded(
child: ListView.separated(
itemCount: provider.selectRecordList.length,
separatorBuilder: (context, index) => SizedBox(height: 8),
itemBuilder: (context, index) {
return _buildRecordItem(
context,
provider.selectRecordList[index],
);
},
),
Column(
children:
provider.selectRecordList.map((record) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _buildRecordItem(context, record),
);
}).toList(),
),
],
);
@@ -112,7 +110,15 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
provider.initRecordForm(record);
provider.isEditing = true;
Navigator.pushNamed(context, "/recordForm");
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => RecordForm(),
);
}
Widget _buildRecordItem(BuildContext context, FoodRecord record) {
@@ -195,12 +201,15 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
}
Widget _buildContent(FoodProvider provider) {
return Column(
children: [
CommonCard(child: _recipeCalendar(provider)),
SizedBox(height: 8),
Expanded(child: CommonCard(child: _dailyItem(context, provider))),
],
return SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 80),
child: Column(
children: [
CommonCard(child: _recipeCalendar(provider)),
const SizedBox(height: 8),
CommonCard(child: _dailyItem(context, provider)),
],
),
);
}
@@ -208,13 +217,10 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
Widget build(BuildContext context) {
final provider = context.watch<FoodProvider>();
return Stack(
children: [
if (provider.isLoading)
buildLoadingIndicator()
else
_buildContent(provider),
],
);
if (provider.isLoading) {
return buildLoadingIndicator();
}
return _buildContent(provider);
}
}

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/widgets/common/image.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
class RecipeCard extends StatelessWidget {
final RecipeSummary recipe;
@@ -17,14 +18,22 @@ class RecipeCard extends StatelessWidget {
final List<String> imageUrls =
recipe.recordList.reversed.map((record) => record.imageUrl).toList();
return CommonCard(
final colors = Theme.of(context).colorScheme;
return GlassCard(
padding: EdgeInsetsGeometry.all(10),
useOwnLayer: true,
settings: LiquidGlassSettings(glassColor: colors.surface),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CommonImage(imageUrls: imageUrls, index: 0),
SizedBox(height: 8),
_buildRecipeContent(context),
InkWell(
onTap: () => navigatorToRecipeDetail(context),
child: _buildRecipeContent(context),
),
],
),
);
@@ -32,6 +41,7 @@ class RecipeCard extends StatelessWidget {
Widget _buildRecipeContent(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -42,17 +52,10 @@ class RecipeCard extends StatelessWidget {
children: [
Text(
recipe.name,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
style: textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
InkWell(
onTap: () => navigatorToRecipeDetail(context),
child: Icon(
Icons.arrow_circle_right_sharp,
color: colors.secondary,
),
),
],
),
SizedBox(height: 5),
@@ -62,19 +65,13 @@ class RecipeCard extends StatelessWidget {
_buildIconText(
icon: Icons.food_bank,
text: recipe.category,
color: Color(0xFF6A5ACD),
color: colors.primary,
),
const SizedBox(width: 12),
_buildIconText(
icon: Icons.thumb_up_alt_outlined,
text: recipe.likeCount.toString(),
color: Color(0xFFDC143C),
),
const SizedBox(width: 12),
_buildIconText(
icon: Icons.star_outline,
text: recipe.favouriteCount.toString(),
color: Color(0xFF20B2AA),
icon: Icons.note_add,
text: recipe.recordList.length.toString(),
color: colors.primary,
),
],
),

View File

@@ -22,56 +22,10 @@ class _RecipeListState extends State<RecipeList> {
context.read<FoodProvider>().queryCategoryList();
});
}
Widget _buildSelectCategory(FoodProvider provider) {
final colors = Theme.of(context).colorScheme;
return Container(
width: 140,
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(12),
),
padding: EdgeInsets.symmetric(horizontal: 16),
child: DropdownButton<String>(
value: provider.queryCategory,
isExpanded: true,
borderRadius: BorderRadius.circular(12),
dropdownColor: colors.surfaceContainer,
elevation: 6,
underline: Container(),
items:
provider.categoryList.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: colors.primary,
shape: BoxShape.circle,
),
),
SizedBox(width: 8),
Expanded(
child: Text(value, style: TextStyle(fontSize: 14)),
),
],
),
);
}).toList(),
onChanged: (value) {
provider.queryCategory = value!;
provider.refreshRecipeList();
},
),
);
}
Widget _buildRecipeList(FoodProvider provider) {
return GridView.builder(
padding: const EdgeInsets.only(bottom: 80),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
@@ -91,8 +45,6 @@ class _RecipeListState extends State<RecipeList> {
} else {
return Column(
children: [
_buildSelectCategory(provider),
SizedBox(height: 5),
Expanded(child: _buildRecipeList(provider)),
],
);

View File

@@ -28,26 +28,24 @@ class _RecipeTimeline extends State<RecipeTimeline> {
SizedBox(width: 5),
Text(
record.date,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
style: Theme.of(context).textTheme.titleMedium,
),
],
),
SizedBox(height: 6),
Padding(
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 0),
child: CommonCard(
child: Column(
children: [
Text(
record.name,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 5),
CommonImage(imageUrls: [record.imageUrl], index: 0),
],
),
SizedBox(height: 8),
CommonCard(
child: Column(
children: [
Text(
record.name,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 5),
CommonImage(imageUrls: [record.imageUrl], index: 0),
],
),
),
SizedBox(height: 20)
],
);
}
@@ -59,6 +57,7 @@ class _RecipeTimeline extends State<RecipeTimeline> {
final colors = Theme.of(context).colorScheme;
return Timeline.tileBuilder(
padding: const EdgeInsets.only(bottom: 80),
theme: TimelineThemeData(nodePosition: 0, indicatorPosition: 0),
builder: TimelineTileBuilder.connected(
itemCount: recordList.length,