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,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),
),
],
),
),
);
}
}