From 63081690130d101479201580b59a53136e704155 Mon Sep 17 00:00:00 2001 From: Cxx0822 <1556464090@qq.com> Date: Sun, 23 Nov 2025 00:02:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=9B=B4=E6=96=B0=E7=BE=8E=E9=A3=9F?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E8=A1=A8=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/apis/moment.dart | 25 +-- lib/apis/recipe.dart | 54 +++--- lib/apis/session.dart | 9 +- lib/apis/stats.dart | 24 ++- lib/config/app_config.dart | 4 +- lib/layout/app_drawer.dart | 2 +- lib/layout/theme_layout.dart | 128 ------------- lib/main.dart | 6 +- lib/models/moment.dart | 30 +-- lib/models/recipe.dart | 4 +- lib/models/recipe.g.dart | 19 +- lib/models/stats.dart | 13 -- lib/models/stats.g.dart | 10 - lib/provider/food_provider.dart | 64 ++++++- lib/provider/theme_provider.dart | 76 -------- lib/utils/http_util.dart | 245 ------------------------- lib/utils/index.dart | 13 +- lib/utils/log_util.dart | 12 -- lib/utils/minio_utils.dart | 54 ------ lib/utils/screenshot_util.dart | 6 +- lib/utils/sp_util.dart | 61 ------- lib/utils/theme_utils.dart | 87 --------- lib/views/login.dart | 5 +- lib/views/moment.dart | 4 +- lib/views/moment_form.dart | 94 +++++----- lib/views/recipe_detail.dart | 3 +- lib/views/record.dart | 26 +-- lib/views/record_form.dart | 131 ++++++------- lib/views/stats.dart | 80 +++----- lib/widgets/common/chart.dart | 253 ------------------------- lib/widgets/common/image.dart | 254 +++++++++++--------------- lib/widgets/common/index.dart | 88 --------- lib/widgets/common/year_selector.dart | 121 ------------ lib/widgets/moment/card.dart | 6 +- lib/widgets/recipe/calendar.dart | 9 +- lib/widgets/recipe/card.dart | 6 +- lib/widgets/recipe/list.dart | 4 +- lib/widgets/recipe/timeline.dart | 21 +-- lib/widgets/stats/card.dart | 5 +- pubspec.lock | 19 +- pubspec.yaml | 8 +- 41 files changed, 465 insertions(+), 1618 deletions(-) delete mode 100644 lib/layout/theme_layout.dart delete mode 100644 lib/provider/theme_provider.dart delete mode 100644 lib/utils/http_util.dart delete mode 100644 lib/utils/log_util.dart delete mode 100644 lib/utils/minio_utils.dart delete mode 100644 lib/utils/sp_util.dart delete mode 100644 lib/utils/theme_utils.dart delete mode 100644 lib/widgets/common/chart.dart delete mode 100644 lib/widgets/common/year_selector.dart diff --git a/lib/apis/moment.dart b/lib/apis/moment.dart index bb97058..e00e139 100644 --- a/lib/apis/moment.dart +++ b/lib/apis/moment.dart @@ -1,28 +1,31 @@ +import 'package:flutter_common/utils/convert_utils.dart'; +import 'package:flutter_common/utils/http_utils.dart'; +import 'package:food_hub_app/config/app_config.dart'; import 'package:food_hub_app/models/moment.dart'; -import 'package:food_hub_app/utils/http_util.dart'; -import 'package:food_hub_app/utils/index.dart'; + +final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl); Future addMomentApi(Moment moment) { - return HttpUtil().post("/moment", data: moment); + return httpUtil.post("/moment", data: moment); } Future updateMomentApi(int id, Moment moment) { - return HttpUtil().put("/moment/$id", data: moment); + return httpUtil.put("/moment/$id", data: moment); } Future deleteMomentApi(int id) { - return HttpUtil().delete("/moment/$id"); + return httpUtil.delete("/moment/$id"); } Future> queryMomentListApi() { - return HttpUtil().get>( + return httpUtil.get>( "/moment", - converter: (data) => convertListResponse(data, Moment.fromJson), + converter: (data) => convertList(data, Moment.fromJson), ); } Future queryMomentByPageApi(int currentPage, int pageSize) { - return HttpUtil().get( + return httpUtil.get( "/moment/page", queryParameters: {"currentPage": currentPage, "pageSize": pageSize}, converter: (data) => PageMoment.fromJson(data), @@ -30,13 +33,13 @@ Future queryMomentByPageApi(int currentPage, int pageSize) { } Future addMomentCommentApi(int id, String content) { - return HttpUtil().post("/moment/$id/comment", data: content); + return httpUtil.post("/moment/$id/comment", data: content); } Future addMomentLikeApi(int id) { - return HttpUtil().post("/food/moment/$id/like"); + return httpUtil.post("/food/moment/$id/like"); } Future deleteMomentLikeApi(int id) { - return HttpUtil().delete("/food/moment/$id/like"); + return httpUtil.delete("/food/moment/$id/like"); } diff --git a/lib/apis/recipe.dart b/lib/apis/recipe.dart index e89b495..3926656 100644 --- a/lib/apis/recipe.dart +++ b/lib/apis/recipe.dart @@ -1,100 +1,102 @@ +import 'package:flutter_common/utils/convert_utils.dart'; +import 'package:flutter_common/utils/http_utils.dart'; +import 'package:food_hub_app/config/app_config.dart'; import 'package:food_hub_app/models/recipe.dart'; -import 'package:food_hub_app/utils/http_util.dart'; import 'package:food_hub_app/utils/index.dart'; +final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl); + Future queryRecipeByIdApi(int id) { - return HttpUtil().get( + return httpUtil.get( "/food/recipe/$id", converter: (data) => RecipeDetail.fromJson(data), ); } Future addRecipeApi(Recipe recipe) { - return HttpUtil().post("/food/recipe", data: recipe); + return httpUtil.post("/food/recipe", data: recipe); } Future updateRecipeApi(int id, Recipe recipe) { - return HttpUtil().put("/food/recipe/$id", data: recipe); + return httpUtil.put("/food/recipe/$id", data: recipe); } Future deleteRecipeApi(int id) { - return HttpUtil().delete("/food/recipe/$id"); + return httpUtil.delete("/food/recipe/$id"); } Future> queryRecipeByUserApi(int id) { - return HttpUtil().get>( + return httpUtil.get>( "/food/recipe/user/$id", - converter: (data) => convertListResponse(data, Recipe.fromJson), + converter: (data) => convertList(data, Recipe.fromJson), ); } Future> queryRecipeUserFavouriteApi() { - return HttpUtil().get>( + return httpUtil.get>( "/food/recipe/user/favourite", - converter: (data) => convertListResponse(data, Recipe.fromJson), + converter: (data) => convertList(data, Recipe.fromJson), ); } Future> queryRecipeApi(RecipeQuery recipeQuery) { - return HttpUtil().get>( + return httpUtil.get>( "/food/recipe", queryParameters: recipeQuery.toJson(), converter: - (data) => - convertListResponse(data, RecipeSummary.fromJson), + (data) => convertList(data, RecipeSummary.fromJson), ); } Future> queryFoodNameListApi() { - return HttpUtil().get>( + return httpUtil.get>( "/food/recipe/name", - converter: (data) => convertListStringResponse(data), + converter: (data) => convertStringList(data), ); } Future addRecordApi(FoodRecord record) { - return HttpUtil().post("/food/record", data: record); + return httpUtil.post("/food/record", data: record); } Future updateRecordApi(int id, FoodRecord record) { - return HttpUtil().put("/food/record/$id", data: record); + return httpUtil.put("/food/record/$id", data: record); } Future deleteRecordApi(int id) { - return HttpUtil().delete("/food/record/$id"); + return httpUtil.delete("/food/record/$id"); } Future addRecipeCommentApi(int id, String content) { - return HttpUtil().post("/food/recipe/$id/comment", data: content); + return httpUtil.post("/food/recipe/$id/comment", data: content); } Future addRecipeLikeApi(int id) { - return HttpUtil().post("/food/recipe/$id/like"); + return httpUtil.post("/food/recipe/$id/like"); } Future deleteRecipeLikeApi(int id) { - return HttpUtil().delete("/food/recipe/$id/like"); + return httpUtil.delete("/food/recipe/$id/like"); } Future addRecipeFavouriteApi(int id) { - return HttpUtil().post("/food/recipe/$id/favourite"); + return httpUtil.post("/food/recipe/$id/favourite"); } Future deleteRecipeFavouriteApi(int id) { - return HttpUtil().delete("/food/recipe/$id/like"); + return httpUtil.delete("/food/recipe/$id/like"); } Future> queryRecordApi(String startDate, String endDate) { - return HttpUtil().get>( + return httpUtil.get>( "/food/record", queryParameters: {"startDate": startDate, "endDate": endDate}, - converter: - (data) => convertListResponse(data, FoodRecord.fromJson), + converter: (data) => convertList(data, FoodRecord.fromJson), ); } Future> queryCategoryApi() { - return HttpUtil().get>( + return httpUtil.get>( "/food/category", converter: (data) => convertListStringResponse(data), ); diff --git a/lib/apis/session.dart b/lib/apis/session.dart index 616dc25..c95ea6d 100644 --- a/lib/apis/session.dart +++ b/lib/apis/session.dart @@ -1,10 +1,13 @@ +import 'package:flutter_common/utils/http_utils.dart'; +import 'package:food_hub_app/config/app_config.dart'; import 'package:food_hub_app/models/session.dart'; -import 'package:food_hub_app/utils/http_util.dart'; + +final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl); Future loginApi(String username, String password) { - return HttpUtil().post( + return httpUtil.post( "/session", queryParameters: {"username": username, "password": password}, converter: (data) => Session.fromJson(data), ); -} \ No newline at end of file +} diff --git a/lib/apis/stats.dart b/lib/apis/stats.dart index 0859288..6c7e6a7 100644 --- a/lib/apis/stats.dart +++ b/lib/apis/stats.dart @@ -1,31 +1,35 @@ +import 'package:flutter_common/models/common_model.dart'; +import 'package:flutter_common/utils/convert_utils.dart'; +import 'package:flutter_common/utils/http_utils.dart'; +import 'package:food_hub_app/config/app_config.dart'; import 'package:food_hub_app/models/stats.dart'; -import 'package:food_hub_app/utils/http_util.dart'; -import 'package:food_hub_app/utils/index.dart'; + +final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl); Future queryStatsApi() { - return HttpUtil().get( + return httpUtil.get( "/food/stats", converter: (data) => SummaryStats.fromJson(data), ); } Future> queryRecordStatsApi() { - return HttpUtil().get>( + return httpUtil.get>( "/food/stats/record", - converter: (data) => convertListResponse(data, ChartData.fromJson), + converter: (data) => convertList(data, ChartData.fromJson), ); } Future> queryCategoryStatsApi() { - return HttpUtil().get>( + return httpUtil.get>( "/food/stats/category", - converter: (data) => convertListResponse(data, ChartData.fromJson), + converter: (data) => convertList(data, ChartData.fromJson), ); } Future> queryRankStatsApi() { - return HttpUtil().get>( + return httpUtil.get>( "/food/stats/rank", - converter: (data) => convertListResponse(data, ChartData.fromJson), + converter: (data) => convertList(data, ChartData.fromJson), ); -} \ No newline at end of file +} diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index cd79309..fc6f97f 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -10,6 +10,6 @@ class AppConfig { static const String rustfsIp = '14.103.235.151'; static const String rustfsFileUrl = 'http://14.103.235.151:9100'; static const String bucketName = 'food'; - // static const String imageBaseUrl = '$rustfsFileUrl/$bucketName/'; - static const String imageBaseUrl = '$baseApiUrl/'; + static const String imageBaseUrl = '$rustfsFileUrl/$bucketName/'; + // static const String imageBaseUrl = '$baseApiUrl/'; } diff --git a/lib/layout/app_drawer.dart b/lib/layout/app_drawer.dart index 6add289..28ba9f2 100644 --- a/lib/layout/app_drawer.dart +++ b/lib/layout/app_drawer.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:food_hub_app/layout/theme_layout.dart'; +import 'package:flutter_common/layout/theme_layout.dart'; class AppDrawer extends StatefulWidget { const AppDrawer({super.key}); diff --git a/lib/layout/theme_layout.dart b/lib/layout/theme_layout.dart deleted file mode 100644 index 0d7f53b..0000000 --- a/lib/layout/theme_layout.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:food_hub_app/provider/theme_provider.dart'; -import 'package:food_hub_app/utils/theme_utils.dart'; -import 'package:provider/provider.dart'; - -class ThemeLayout extends StatelessWidget { - final Widget? child; - - const ThemeLayout({super.key, this.child}); - - Widget _buildThemeColorList(BuildContext context, ThemeProvider provider) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - crossAxisSpacing: 8, - mainAxisSpacing: 8, - childAspectRatio: 1.2, - ), - itemCount: provider.availableThemes.length, - itemBuilder: (context, index) { - final themeColor = provider.availableThemes[index]; - final isSelected = provider.currentTheme == themeColor; - - return _buildThemeColorItem( - themeColor: themeColor, - isSelected: isSelected, - onTap: () => provider.changeTheme(themeColor), - ); - }, - ), - ); - } - - Widget _buildThemeColorItem({ - required ThemeColor themeColor, - required bool isSelected, - required VoidCallback onTap, - }) { - return GestureDetector( - onTap: onTap, - child: Container( - decoration: BoxDecoration( - color: - isSelected - ? themeColor.primaryColor.withAlpha(50) - : Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: isSelected ? themeColor.primaryColor : Colors.grey.shade300, - width: isSelected ? 2 : 1, - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 颜色圆点 - Container( - width: 20, - height: 20, - decoration: BoxDecoration( - color: themeColor.primaryColor, - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha(10), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - ), - const SizedBox(height: 4), - Text( - themeColor.name, - style: TextStyle( - fontSize: 10, - fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, - color: - isSelected ? themeColor.primaryColor : Colors.grey.shade600, - ), - ), - ], - ), - ), - ); - } - - Widget _buildDarkModeSection(BuildContext context, ThemeProvider provider) { - return ListTile( - leading: Icon( - provider.isDarkMode ? Icons.dark_mode : Icons.light_mode, - color: Theme.of(context).colorScheme.primary, - ), - title: const Text('暗黑模式'), - trailing: Switch( - value: provider.isDarkMode, - onChanged: (value) { - provider.toggleDarkMode(value); - }, - ), - onTap: () { - provider.toggleDarkMode(!provider.isDarkMode); - }, - ); - } - - @override - Widget build(BuildContext context) { - final provider = context.watch(); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), - child: Text('主题颜色', style: Theme.of(context).textTheme.titleMedium), - ), - _buildDarkModeSection(context, provider), - _buildThemeColorList(context, provider), - ], - ); - } -} diff --git a/lib/main.dart b/lib/main.dart index 2a09123..2534914 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_common/provider/theme_provider.dart'; +import 'package:flutter_common/utils/sp_utils.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:food_hub_app/provider/food_provider.dart'; -import 'package:food_hub_app/provider/theme_provider.dart'; -import 'package:food_hub_app/utils/sp_util.dart'; import 'package:food_hub_app/views/home.dart'; import 'package:food_hub_app/views/login.dart'; import 'package:food_hub_app/views/moment_form.dart'; @@ -13,8 +13,10 @@ import 'package:provider/provider.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + FormBuilderLocalizations.delegate.load(const Locale('zh', 'CN')); await SPUtil.init(); + runApp( MultiProvider( providers: [ diff --git a/lib/models/moment.dart b/lib/models/moment.dart index b446749..14ccd09 100644 --- a/lib/models/moment.dart +++ b/lib/models/moment.dart @@ -4,13 +4,13 @@ part 'moment.g.dart'; @JsonSerializable() class Comment { - final int id; - final String username; - final String avatar; - final String content; - final String date; + int id; + String username; + String avatar; + String content; + String date; - const Comment({ + Comment({ required this.id, required this.username, required this.avatar, @@ -25,15 +25,15 @@ class Comment { @JsonSerializable() class Moment { - final int? id; - final int? userId; - final String? username; - final String? avatar; - final String content; - final List imageList; - final String? date; - final List? likeList; - final List? commentList; + int? id; + int? userId; + String? username; + String? avatar; + String content; + List imageList; + String? date; + List? likeList; + List? commentList; Moment({ this.id, diff --git a/lib/models/recipe.dart b/lib/models/recipe.dart index 2a0ac4d..d927714 100644 --- a/lib/models/recipe.dart +++ b/lib/models/recipe.dart @@ -83,9 +83,9 @@ class FoodRecord { }); factory FoodRecord.fromJson(Map json) => - _$RecordFromJson(json); + _$FoodRecordFromJson(json); - Map toJson() => _$RecordToJson(this); + Map toJson() => _$FoodRecordToJson(this); static FoodRecord getEmpty() { return FoodRecord( diff --git a/lib/models/recipe.g.dart b/lib/models/recipe.g.dart index 8861150..539c885 100644 --- a/lib/models/recipe.g.dart +++ b/lib/models/recipe.g.dart @@ -51,7 +51,7 @@ Map _$RecipeCommentToJson(RecipeComment instance) => 'date': instance.date, }; -FoodRecord _$RecordFromJson(Map json) => FoodRecord( +FoodRecord _$FoodRecordFromJson(Map json) => FoodRecord( id: (json['id'] as num?)?.toInt(), name: json['name'] as String, category: json['category'] as String, @@ -60,14 +60,15 @@ FoodRecord _$RecordFromJson(Map json) => FoodRecord( imageUrl: json['imageUrl'] as String, ); -Map _$RecordToJson(FoodRecord instance) => { - 'id': instance.id, - 'name': instance.name, - 'category': instance.category, - 'person': instance.person, - 'date': instance.date, - 'imageUrl': instance.imageUrl, -}; +Map _$FoodRecordToJson(FoodRecord instance) => + { + 'id': instance.id, + 'name': instance.name, + 'category': instance.category, + 'person': instance.person, + 'date': instance.date, + 'imageUrl': instance.imageUrl, + }; RecipeQuery _$RecipeQueryFromJson(Map json) => RecipeQuery(category: json['category'] as String); diff --git a/lib/models/stats.dart b/lib/models/stats.dart index 2258b98..a37bb54 100644 --- a/lib/models/stats.dart +++ b/lib/models/stats.dart @@ -19,16 +19,3 @@ class SummaryStats { Map toJson() => _$SummaryStatsToJson(this); } - -@JsonSerializable() -class ChartData { - final String name; - final double value; - - const ChartData({required this.name, required this.value}); - - factory ChartData.fromJson(Map json) => - _$ChartDataFromJson(json); - - Map toJson() => _$ChartDataToJson(this); -} diff --git a/lib/models/stats.g.dart b/lib/models/stats.g.dart index 2821d26..91b4436 100644 --- a/lib/models/stats.g.dart +++ b/lib/models/stats.g.dart @@ -18,13 +18,3 @@ Map _$SummaryStatsToJson(SummaryStats instance) => 'categoryCount': instance.categoryCount, 'workCount': instance.workCount, }; - -ChartData _$ChartDataFromJson(Map json) => ChartData( - name: json['name'] as String, - value: (json['value'] as num).toDouble(), -); - -Map _$ChartDataToJson(ChartData instance) => { - 'name': instance.name, - 'value': instance.value, -}; diff --git a/lib/provider/food_provider.dart b/lib/provider/food_provider.dart index 1ba1025..0df56b8 100644 --- a/lib/provider/food_provider.dart +++ b/lib/provider/food_provider.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_common/models/common_model.dart'; +import 'package:flutter_common/utils/toast_util.dart'; import 'package:food_hub_app/apis/moment.dart'; import 'package:food_hub_app/apis/recipe.dart'; import 'package:food_hub_app/apis/stats.dart'; @@ -7,8 +9,10 @@ import 'package:food_hub_app/models/recipe.dart'; import 'package:food_hub_app/models/stats.dart'; import 'package:food_hub_app/utils/date_util.dart'; import 'package:food_hub_app/utils/index.dart'; +import 'package:food_hub_app/views/record.dart'; class FoodProvider with ChangeNotifier { + late RecordTab currentRecordTab = RecordTab.recipe; late FoodRecord recordFormItem; late Moment momentFormItem; @@ -20,6 +24,7 @@ class FoodProvider with ChangeNotifier { late List categoryList = []; late List recipeSummaryList = []; + int selectYear = DateTime.now().year; DateTime selectedDay = DateTime.now(); DateTime focusedDay = DateTime.now(); @@ -61,6 +66,18 @@ class FoodProvider with ChangeNotifier { notifyListeners(); } + Future onRecordTabChange() async { + switch (currentRecordTab) { + case RecordTab.recipe: + await refreshRecipeList(); + break; + case RecordTab.calendar: + case RecordTab.timeline: + await refreshRecordList(); + break; + } + } + Future queryCategoryList() async { try { error = null; @@ -98,16 +115,31 @@ class FoodProvider with ChangeNotifier { } } - Future refreshRecordList(String? startDate, String? endDate) async { + Future refreshRecordList() async { if (isLoading) return; + if (currentRecordTab == RecordTab.recipe) return; + try { isLoading = true; error = null; notifyListeners(); - startDate ??= getFirstDayOfMonth(focusedDay); - endDate ??= getLastDayOfMonth(focusedDay); + late String startDate; + late String endDate; + + switch (currentRecordTab) { + case RecordTab.calendar: + startDate = getFirstDayOfMonth(focusedDay); + endDate = getLastDayOfMonth(focusedDay); + break; + case RecordTab.timeline: + startDate = '$selectYear-01-01'; + endDate = '$selectYear-12-31'; + break; + case RecordTab.recipe: + break; + } final result = await queryRecordApi(startDate, endDate); @@ -129,6 +161,30 @@ class FoodProvider with ChangeNotifier { } } + Future handleRecord() async { + if (isLoading) return; + + try { + isLoading = true; + error = null; + notifyListeners(); + + if (isEditing) { + await updateRecordApi(recordFormItem.id!, recordFormItem); + ToastUtil.success('更新记录成功'); + } else { + await addRecordApi(recordFormItem); + ToastUtil.success('上传记录成功'); + } + } catch (e) { + error = '处理数据失败: $e'; + debugPrint('处理数据失败: $e'); + } finally { + isLoading = false; + notifyListeners(); + } + } + void refreshSelectRecord() { selectRecordList = recordList @@ -136,7 +192,7 @@ class FoodProvider with ChangeNotifier { .toList(); } - Future refreshBlogStats() async { + Future refreshFoodStats() async { if (isLoading) return; try { diff --git a/lib/provider/theme_provider.dart b/lib/provider/theme_provider.dart deleted file mode 100644 index 8d09789..0000000 --- a/lib/provider/theme_provider.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:food_hub_app/utils/theme_utils.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class ThemeProvider with ChangeNotifier { - static const String _themeKey = 'selected_theme'; - static const String _darkModeKey = 'is_dark_mode'; - - bool isDarkMode = false; - ThemeColor currentTheme = defaultThemes[0]; - - late SharedPreferences _prefs; - bool isInitialized = false; - - ThemeProvider() { - _initPreferences(); - } - - // 初始化 SharedPreferences - Future _initPreferences() async { - _prefs = await SharedPreferences.getInstance(); - _loadPreferences(); - - isInitialized = true; - notifyListeners(); - } - - // 加载存储的设置 - void _loadPreferences() { - isDarkMode = _prefs.getBool(_darkModeKey) ?? false; - - // 加载主题色 - final themeName = _prefs.getString(_themeKey); - if (themeName != null) { - currentTheme = getThemeColor(themeName); - } - } - - // 保存主题色 - Future _saveTheme() async { - await _prefs.setString(_themeKey, currentTheme.name); - } - - // 保存暗黑模式 - Future _saveDarkMode() async { - await _prefs.setBool(_darkModeKey, isDarkMode); - } - - List get availableThemes => defaultThemes; - - // 切换明暗模式 - void toggleDarkMode(bool value) { - isDarkMode = value; - _saveDarkMode(); - notifyListeners(); - } - - // 更改主题色 - void changeTheme(ThemeColor theme) { - currentTheme = theme; - _saveTheme(); - notifyListeners(); - } - - ThemeData get currentThemeData { - return ThemeData( - primarySwatch: currentTheme.materialColor, - colorScheme: ColorScheme.fromSeed( - seedColor: currentTheme.primaryColor, - brightness: isDarkMode ? Brightness.dark : Brightness.light, - ), - useMaterial3: true, - fontFamily: 'CustomFont', - ); - } -} diff --git a/lib/utils/http_util.dart b/lib/utils/http_util.dart deleted file mode 100644 index 36feda0..0000000 --- a/lib/utils/http_util.dart +++ /dev/null @@ -1,245 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:food_hub_app/config/app_config.dart'; -import 'package:food_hub_app/utils/sp_util.dart'; -import 'package:food_hub_app/widgets/common/index.dart'; - -import 'log_util.dart'; - -class HttpUtil { - static final HttpUtil _instance = HttpUtil._internal(); - - factory HttpUtil() => _instance; - - late Dio _dio; - - // 请求头配置 - Map headers = { - 'Content-Type': 'application/json;charset=UTF-8', - 'Accept': 'application/json', - }; - - // 超时时间 - final int timeout = 5; - - HttpUtil._internal() { - // 初始化Dio实例 - BaseOptions options = BaseOptions( - baseUrl: AppConfig.baseApiUrl, - connectTimeout: Duration(seconds: timeout), - receiveTimeout: Duration(seconds: timeout), - headers: headers, - ); - - _dio = Dio(options); - - // 添加请求拦截器 - _dio.interceptors.add( - InterceptorsWrapper( - onRequest: (options, handler) { - logger.d("请求URL: ${options.uri}"); - if (options.data != null) { - logger.d("请求参数: ${options.data}"); - } - - if (SPUtil.getString('token').isNotEmpty) { - options.headers['satoken'] = SPUtil.getString('token'); - } - return handler.next(options); - }, - ), - ); - - // 添加响应拦截器 - _dio.interceptors.add( - InterceptorsWrapper( - onResponse: (response, handler) { - logger.d("响应状态码: ${response.statusCode}"); - // logger.d("响应数据: ${response.data}"); - return handler.next(response); - }, - ), - ); - - // 添加错误拦截器 - _dio.interceptors.add( - InterceptorsWrapper( - onError: (DioException e, handler) { - _handleError(e); - return handler.next(e); - }, - ), - ); - } - - // 基础请求方法(处理所有类型的请求) - Future _request( - String path, { - required String method, - dynamic data, - Map? queryParameters, - T Function(dynamic data)? converter, - }) async { - try { - Response response = await _dio.request( - path, - data: data, - queryParameters: queryParameters, - options: Options(method: method), - ); - - // 处理响应数据 - if (converter != null) { - return converter(response.data); - } - - // 没有转换器时尝试直接返回(可能不安全,建议提供转换器) - return response.data; - } catch (e) { - rethrow; - } - } - - // GET请求 - Future get( - String path, { - Map? queryParameters, - T Function(dynamic data)? converter, - }) => _request( - path, - method: "GET", - queryParameters: queryParameters, - converter: converter, - ); - - // POST请求 - Future post( - String path, { - dynamic data, - Map? queryParameters, - T Function(dynamic data)? converter, - }) => _request( - path, - method: "POST", - data: data, - queryParameters: queryParameters, - converter: converter, - ); - - // PUT请求 - Future put( - String path, { - dynamic data, - Map? queryParameters, - T Function(dynamic data)? converter, - }) => _request( - path, - method: "PUT", - data: data, - queryParameters: queryParameters, - converter: converter, - ); - - // DELETE请求 - Future delete( - String path, { - dynamic data, - Map? queryParameters, - T Function(dynamic data)? converter, - }) => _request( - path, - method: "DELETE", - data: data, - queryParameters: queryParameters, - converter: converter, - ); - - // 文件上传 - Future upload( - String path, - String filePath, { - Map? queryParameters, - }) async { - try { - FormData formData = FormData.fromMap({ - 'file': await MultipartFile.fromFile(filePath), - }); - - Response response = await _dio.post( - path, - data: formData, - queryParameters: queryParameters, - ); - return response.data; - } catch (e) { - _handleError(e); - rethrow; - } - } - - // 错误处理 - void _handleError(dynamic error) { - String errorMessage = '未知错误'; - if (error is DioException) { - switch (error.type) { - case DioExceptionType.connectionTimeout: - errorMessage = '连接超时,请检查网络连接'; - break; - case DioExceptionType.sendTimeout: - errorMessage = '发送超时,请检查网络连接'; - break; - case DioExceptionType.receiveTimeout: - errorMessage = '接收超时,请检查网络连接'; - break; - case DioExceptionType.cancel: - errorMessage = '请求已取消'; - break; - case DioExceptionType.badCertificate: - errorMessage = '证书验证失败'; - break; - case DioExceptionType.badResponse: - // 处理HTTP响应错误(4xx, 5xx) - final statusCode = error.response?.statusCode ?? 0; - final responseData = error.response?.data; - - if (responseData is Map && responseData.containsKey('message')) { - // 服务器返回了自定义错误消息 - errorMessage = responseData['message']?.toString() ?? '未知错误'; - } else { - errorMessage = _getHttpErrorMessage(statusCode); - } - break; - case DioExceptionType.connectionError: - errorMessage = '网络连接错误,请检查网络设置'; - break; - case DioExceptionType.unknown: - if (error.error != null) { - errorMessage = '未知错误: ${error.error.toString()}'; - } - errorMessage = '发生未知错误'; - break; - } - } else { - errorMessage = '非Dio错误: $error'; - } - - print(errorMessage); - showErrorToast(errorMessage); - } - - String _getHttpErrorMessage(int statusCode) { - // 根据HTTP状态码返回对应的错误消息 - switch (statusCode) { - case 400: return '错误请求,请检查参数'; - case 401: return '未授权,请登录'; - case 403: return '禁止访问,权限不足'; - case 404: return '资源不存在'; - case 405: return '方法不允许'; - case 408: return '请求超时'; - case 500: return '服务器内部错误'; - case 502: return '网关错误'; - case 503: return '服务不可用'; - case 504: return '网关超时'; - default: return 'HTTP错误: $statusCode'; - } - } -} diff --git a/lib/utils/index.dart b/lib/utils/index.dart index c460c19..553073c 100644 --- a/lib/utils/index.dart +++ b/lib/utils/index.dart @@ -16,18 +16,7 @@ String formatDateTime(DateTime dateTime, [String format = 'yyyy-MM-dd']) { return Intl.withLocale('zh_CN', () => formatter.format(dateTime)); } -/// 通用列表转换函数 -List convertListResponse( - dynamic data, - T Function(Map) fromJson, -) { - if (data is List) { - return data.map((item) => fromJson(item as Map)).toList(); - } - throw FormatException( - 'Expected a list of items for conversion, but got ${data.runtimeType}', - ); -} + List convertListStringResponse(dynamic data) { if (data is List) { diff --git a/lib/utils/log_util.dart b/lib/utils/log_util.dart deleted file mode 100644 index f571317..0000000 --- a/lib/utils/log_util.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:logger/logger.dart'; - -// 全局日志实例,在整个项目中共享 -final Logger logger = Logger( - printer: PrettyPrinter( - methodCount: 1, - colors: true, - dateTimeFormat: DateTimeFormat.dateAndTime, - ), - // 可选:配置输出到文件(需配合文件操作库) - // output: FileOutput(file: File('logs/app.log')), -); \ No newline at end of file diff --git a/lib/utils/minio_utils.dart b/lib/utils/minio_utils.dart deleted file mode 100644 index 4b81bc9..0000000 --- a/lib/utils/minio_utils.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'dart:io'; -import 'package:crypto/crypto.dart'; -import 'package:file_picker/file_picker.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; - - MinIOHelper._internal() { - _minio = Minio( - endPoint: AppConfig.rustfsIp, - port: 9100, - accessKey: "tHSFfcDW8qpCzKa2Xg6Y", - secretKey: "oq79EeYJ4jdczRp2IHUMCnbKtSw58NgDlG3sOkvX", - useSSL: false, - ); - } - - late Minio _minio; - - Future uploadFile({ - required PlatformFile file, - Function(double)? onProgress, - }) async { - try { - String hashName = await _generateMD5HashName(file.path!); - String fileName = '$hashName${_getFileExtension(file.name)}'; - - await _minio.fPutObject(AppConfig.bucketName, fileName, file.path!); - return fileName; - } catch (e) { - throw Exception('文件上传失败: $e'); - } - } - - String _getFileExtension(String fileName) { - if (fileName.contains('.')) { - return '.${fileName.split('.').last.toLowerCase()}'; - } - return ''; - } - - Future _generateMD5HashName(String filePath) async { - final file = File(filePath); - final bytes = await file.readAsBytes(); - final hash = md5.convert(bytes); - return hash.toString(); - } -} diff --git a/lib/utils/screenshot_util.dart b/lib/utils/screenshot_util.dart index 6adef12..df3389c 100644 --- a/lib/utils/screenshot_util.dart +++ b/lib/utils/screenshot_util.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:food_hub_app/widgets/common/index.dart'; +import 'package:flutter_common/utils/toast_util.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; @@ -115,10 +115,10 @@ class ScreenshotUtil { // 分享后删除临时文件 await imageFile.delete(); } else { - showErrorToast('生成图片失败,请重试'); + ToastUtil.error('生成图片失败,请重试'); } } catch (e) { - showErrorToast('分享失败: $e'); + ToastUtil.error('分享失败: $e'); } } } diff --git a/lib/utils/sp_util.dart b/lib/utils/sp_util.dart deleted file mode 100644 index b255633..0000000 --- a/lib/utils/sp_util.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:shared_preferences/shared_preferences.dart'; - -class SPUtil { - static late SharedPreferences _prefs; - - /// 初始化 - static Future init() async { - _prefs = await SharedPreferences.getInstance(); - } - - /// 存储数据 - static Future set(String key, dynamic value) { - if (value is String) return _prefs.setString(key, value); - if (value is int) return _prefs.setInt(key, value); - if (value is bool) return _prefs.setBool(key, value); - if (value is double) return _prefs.setDouble(key, value); - if (value is List) return _prefs.setStringList(key, value); - throw ArgumentError('Unsupported value type: ${value.runtimeType}'); - } - - /// 获取数据 - static dynamic get(String key, [dynamic defaultValue]) { - if (!_prefs.containsKey(key)) return defaultValue; - return _prefs.get(key) ?? defaultValue; - } - - /// 获取字符串 - static String getString(String key, [String defaultValue = '']) { - return _prefs.getString(key) ?? defaultValue; - } - - /// 获取布尔值 - static bool getBool(String key, [bool defaultValue = false]) { - return _prefs.getBool(key) ?? defaultValue; - } - - /// 获取整数 - static int getInt(String key, [int defaultValue = 0]) { - return _prefs.getInt(key) ?? defaultValue; - } - - /// 获取浮点数 - static double getDouble(String key, [double defaultValue = 0.0]) { - return _prefs.getDouble(key) ?? defaultValue; - } - - /// 获取字符串列表 - static List getStringList(String key, [List defaultValue = const []]) { - return _prefs.getStringList(key) ?? defaultValue; - } - - /// 删除数据 - static Future remove(String key) { - return _prefs.remove(key); - } - - /// 清空所有数据 - static Future clear() { - return _prefs.clear(); - } -} \ No newline at end of file diff --git a/lib/utils/theme_utils.dart b/lib/utils/theme_utils.dart deleted file mode 100644 index 1998144..0000000 --- a/lib/utils/theme_utils.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:flutter/material.dart'; - -class ThemeColor { - final String name; - final Color primaryColor; - final MaterialColor materialColor; - - ThemeColor({ - required this.name, - required this.primaryColor, - required this.materialColor, - }); -} - -final List defaultThemes = [ - ThemeColor( - name: '梦幻紫', - primaryColor: Color(0xFF8B5CF6), - materialColor: MaterialColor(0xFF8B5CF6, { - 50: Color(0xFFF5F3FF), - 100: Color(0xFFEDE9FE), - 200: Color(0xFFDDD6FE), - 300: Color(0xFFC4B5FD), - 400: Color(0xFFA78BFA), - 500: Color(0xFF8B5CF6), - 600: Color(0xFF7C3AED), - 700: Color(0xFF6D28D9), - 800: Color(0xFF5B21B6), - 900: Color(0xFF4C1D95), - }), - ), - ThemeColor( - name: '活力橙', - primaryColor: Color(0xFFF59E0B), - materialColor: MaterialColor(0xFFF59E0B, { - 50: Color(0xFFFFFBEB), - 100: Color(0xFFFEF3C7), - 200: Color(0xFFFDE68A), - 300: Color(0xFFFCD34D), - 400: Color(0xFFFBBF24), - 500: Color(0xFFF59E0B), - 600: Color(0xFFD97706), - 700: Color(0xFFB45309), - 800: Color(0xFF92400E), - 900: Color(0xFF78350F), - }), - ), - ThemeColor( - name: '浪漫粉', - primaryColor: Color(0xFFEC4899), - materialColor: MaterialColor(0xFFEC4899, { - 50: Color(0xFFFDF2F8), - 100: Color(0xFFFCE7F3), - 200: Color(0xFFFBCFE8), - 300: Color(0xFFF9A8D4), - 400: Color(0xFFF472B6), - 500: Color(0xFFEC4899), - 600: Color(0xFFDB2777), - 700: Color(0xFFBE185D), - 800: Color(0xFF9D174D), - 900: Color(0xFF831843), - }), - ), - ThemeColor( - name: '清新青', - primaryColor: Color(0xFF06B6D4), - materialColor: MaterialColor(0xFF06B6D4, { - 50: Color(0xFFF0FDFA), - 100: Color(0xFFCCFBF1), - 200: Color(0xFF99F6E4), - 300: Color(0xFF5EEAD4), - 400: Color(0xFF2DD4BF), - 500: Color(0xFF06B6D4), - 600: Color(0xFF0891B2), - 700: Color(0xFF0E7490), - 800: Color(0xFF155E75), - 900: Color(0xFF164E63), - }), - ), -]; - -ThemeColor getThemeColor(String themeName) { - return defaultThemes.firstWhere( - (theme) => theme.name == themeName, - orElse: () => defaultThemes[0], - ); -} diff --git a/lib/views/login.dart b/lib/views/login.dart index f9f2d74..d8300c1 100644 --- a/lib/views/login.dart +++ b/lib/views/login.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_common/utils/sp_utils.dart'; +import 'package:flutter_common/utils/toast_util.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:food_hub_app/apis/session.dart'; import 'package:food_hub_app/models/session.dart'; -import 'package:food_hub_app/utils/sp_util.dart'; import 'package:food_hub_app/widgets/common/index.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; @@ -69,7 +70,7 @@ class _LoginPage extends State { if ((_formKey.currentState as FormState).validate()) { (_formKey.currentState as FormState).save(); Session session = await loginApi(_username, _password); - showSuccessToast('登录成功'); + ToastUtil.success('登录成功'); handleRememberState(); handleTokenState(session.saToken.tokenValue); diff --git a/lib/views/moment.dart b/lib/views/moment.dart index a9b4897..fb5e307 100644 --- a/lib/views/moment.dart +++ b/lib/views/moment.dart @@ -1,8 +1,8 @@ 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/widgets/common/easy_refresh.dart'; -import 'package:food_hub_app/widgets/common/index.dart'; import 'package:food_hub_app/widgets/moment/card.dart'; import 'package:provider/provider.dart'; @@ -108,7 +108,7 @@ class _MomentPageState extends State { body: Stack( children: [ if (provider.isLoading) - buildLoadingIndicator(context) + buildLoadingIndicator() else _buildMomentList(provider), ], diff --git a/lib/views/moment_form.dart b/lib/views/moment_form.dart index 619096b..81dbca2 100644 --- a/lib/views/moment_form.dart +++ b/lib/views/moment_form.dart @@ -1,11 +1,14 @@ 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_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:food_hub_app/widgets/common/index.dart'; import 'package:provider/provider.dart'; class MomentFormPage extends StatefulWidget { @@ -22,7 +25,9 @@ class _MomentFormPageState extends State { static const int maxContentLength = 200; static const int maxImageCount = 9; // 最大图片数量 - Widget _buildContentField() { + Widget _buildContentField(FoodProvider provider) { + final colors = Theme.of(context).colorScheme; + return FormBuilderTextField( name: _contentField, focusNode: _focusNode, @@ -38,16 +43,13 @@ class _MomentFormPageState extends State { return Text( '$currentLength/$maxContentLength', style: TextStyle( - color: - currentLength > maxLength! - ? Colors.red - : Theme.of(context).colorScheme.primary, + color: currentLength > maxLength! ? Colors.red : colors.primary, ), ); }, onChanged: (value) { setState(() { - // provider.momentFormItem.content = value!; + provider.momentFormItem.content = value!; }); }, decoration: buildInputDecoration(context: context, hintText: '请输入朋友圈内容'), @@ -56,39 +58,41 @@ class _MomentFormPageState extends State { return '请输入朋友圈内容'; } if (value.length > maxContentLength) { - // 修正为使用 maxContentLength - return '内容不能超过${maxContentLength}字'; + return '内容不能超过$maxContentLength字'; } return null; }, ); } - Widget _buildImageGrid(FoodProvider provider) { + Widget _buildImageField(FoodProvider provider) { final imageUrls = provider.momentFormItem.imageList; + final totalItems = + imageUrls.length + (imageUrls.length < maxImageCount ? 1 : 0); - return Wrap( - spacing: 8, - runSpacing: 8, - children: [ - ...imageUrls.asMap().entries.map((entry) { - final index = entry.key; - final imageUrl = entry.value; - return Stack( - children: [ - buildImagePreviewItem( - context: context, - imageUrls: imageUrls, - index: index, - onRemoveImage: () => _removeImage(provider, index), - ) - ], + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + childAspectRatio: 1, + ), + itemCount: totalItems, + itemBuilder: (context, index) { + if (index < imageUrls.length) { + return ImagePreview( + imageUrls: imageUrls, + index: index, + onRemoveImage: () => _removeImage(provider, index), ); - }), - // 上传按钮(如果还有剩余位置) - if (imageUrls.length < maxImageCount) - buildImageUploadButton(onPickImage: () => _pickImages(provider)), - ], + } + // 上传按钮(最后一个) + else { + return buildImageUploadButton(onTap: () => _pickImages(provider)); + } + }, ); } @@ -102,14 +106,14 @@ class _MomentFormPageState extends State { children: [ buildFormLabel('朋友圈内容', required: true), const SizedBox(height: 8), - _buildContentField(), - const SizedBox(height: 20), + _buildContentField(provider), + const SizedBox(height: 8), buildFormLabel('上传图片', required: true), - const SizedBox(height: 10), - _buildImageGrid(provider), - - const SizedBox(height: 20), + const SizedBox(height: 8), + _buildImageField(provider), + const SizedBox(height: 8), + buildFormButtonGroup( context: context, onConfirm: () => _submitForm(provider), @@ -125,7 +129,7 @@ class _MomentFormPageState extends State { final remainingCount = maxImageCount - currentCount; if (remainingCount <= 0) { - showErrorToast('最多只能上传$maxImageCount张图片'); + ToastUtil.error('最多只能上传$maxImageCount张图片'); return; } @@ -147,7 +151,11 @@ class _MomentFormPageState extends State { // 逐个上传图片 for (final file in filesToUpload) { - final fileName = await MinIOHelper().uploadFile(file: file); + final fileName = await MinIOHelper().uploadFile( + bucketName: AppConfig.bucketName, + file: file, + ); + logger.i('图片名称:$fileName'); newImageUrls.add(fileName); } @@ -156,9 +164,9 @@ class _MomentFormPageState extends State { provider.momentFormItem.imageList.addAll(newImageUrls); }); - showSuccessToast('图片上传成功'); + ToastUtil.success('图片上传成功'); } catch (e) { - showErrorToast('图片上传失败'); + ToastUtil.error('图片上传失败'); } } } @@ -199,7 +207,7 @@ class _MomentFormPageState extends State { body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(10), - child: BuildCard(child: _buildFormBuilder()), + child: CommonCard(child: _buildFormBuilder()), ), ), ); diff --git a/lib/views/recipe_detail.dart b/lib/views/recipe_detail.dart index 58b039a..2df5519 100644 --- a/lib/views/recipe_detail.dart +++ b/lib/views/recipe_detail.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_common/widget/common_widget.dart'; import 'package:food_hub_app/apis/recipe.dart'; import 'package:food_hub_app/models/recipe.dart'; import 'package:food_hub_app/utils/screenshot_util.dart'; @@ -226,7 +227,7 @@ class _RecipeDetailState extends State { required String title, required Widget content, }) { - return BuildCard( + return CommonCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/views/record.dart b/lib/views/record.dart index 5218981..caaa3c1 100644 --- a/lib/views/record.dart +++ b/lib/views/record.dart @@ -24,7 +24,6 @@ class RecordPage extends StatefulWidget { } class _RecordPageState extends State { - RecordTab _currentTab = RecordTab.recipe; final List _tabPages = [ RecipeList(), RecipeCalendar(), @@ -49,22 +48,8 @@ class _RecordPageState extends State { } void _onTabChange(RecordTab tab, FoodProvider provider) { - setState(() { - _currentTab = tab; - }); - - switch (tab) { - case RecordTab.recipe: - provider.refreshRecipeList(); - break; - case RecordTab.calendar: - provider.refreshRecordList(null, null); - break; - case RecordTab.timeline: - final year = DateTime.now().year; - provider.refreshRecordList("$year-01-01", "$year-12-31"); - break; - } + provider.currentRecordTab = tab; + provider.onRecordTabChange(); } @override @@ -75,11 +60,14 @@ class _RecordPageState extends State { children: [ _buildTabs( context: context, - currentTab: _currentTab, + currentTab: provider.currentRecordTab, onTabChanged: (tab) => _onTabChange(tab, provider), ), Expanded( - child: IndexedStack(index: _currentTab.index, children: _tabPages), + child: IndexedStack( + index: provider.currentRecordTab.index, + children: _tabPages, + ), ), ], ); diff --git a/lib/views/record_form.dart b/lib/views/record_form.dart index 5b064d9..455a300 100644 --- a/lib/views/record_form.dart +++ b/lib/views/record_form.dart @@ -1,12 +1,14 @@ 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_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:food_hub_app/widgets/common/index.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; @@ -48,52 +50,19 @@ class _RecordFormPageState extends State { children: [ buildFormLabel('菜谱名称', required: true), const SizedBox(height: 8), - _buildRecipeAutocomplete(provider), + _buildRecipeField(provider), + const SizedBox(height: 8), - const SizedBox(height: 10), buildFormLabel('完成时间', required: true), const SizedBox(height: 8), - FormBuilderTextField( - name: _dateField, - readOnly: true, - initialValue: provider.recordFormItem.date, - decoration: buildInputDecoration( - context: context, - hintText: '请选择完成时间', - prefixIcon: const Icon( - Icons.calendar_month, - size: 20, - color: Color(0xFF86909C), - ), - ), - onTap: () => _onSelectDate(provider), - validator: (value) { - if (value == null || value.isEmpty) { - return '请选择完成时间'; - } - return null; - }, - ), - const SizedBox(height: 10), + _buildDateField(provider), + const SizedBox(height: 8), buildFormLabel('上传图片', required: true), - const SizedBox(height: 10), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (provider.recordFormItem.imageUrl.isEmpty) - buildImageUploadButton(onPickImage: () => _pickImage(provider)) - else - buildImagePreviewItem( - context: context, - imageUrls: [provider.recordFormItem.imageUrl], - index: 0, - onRemoveImage: () => _removeImage(provider), - ), - ], - ), + const SizedBox(height: 8), + _buildImageField(provider), + const SizedBox(height: 8), - const SizedBox(height: 10), buildFormButtonGroup( context: context, onConfirm: () => _submitForm(provider), @@ -103,7 +72,7 @@ class _RecordFormPageState extends State { ); } - Widget _buildRecipeAutocomplete(FoodProvider provider) { + Widget _buildRecipeField(FoodProvider provider) { return Autocomplete( initialValue: TextEditingValue(text: provider.recordFormItem.name), optionsBuilder: (TextEditingValue textEditingValue) { @@ -118,9 +87,7 @@ class _RecordFormPageState extends State { }, onSelected: (String value) { - setState(() { - provider.recordFormItem.name = value; - }); + provider.recordFormItem.name = value; }, optionsViewBuilder: ( @@ -149,7 +116,7 @@ class _RecordFormPageState extends State { child: Container( constraints: BoxConstraints( maxHeight: 200, - maxWidth: MediaQuery.of(context).size.width - 32, + maxWidth: MediaQuery.of(context).size.width - 58, ), child: ListView.builder( padding: EdgeInsets.zero, @@ -179,9 +146,7 @@ class _RecordFormPageState extends State { void onClearRecipeName() { controller.clear(); - setState(() { - provider.recordFormItem.name = ''; - }); + provider.recordFormItem.name = ''; } Widget? buildSuffixIcon() { @@ -201,9 +166,7 @@ class _RecordFormPageState extends State { focusNode: focusNode, enabled: !provider.isEditing, onChanged: (value) { - setState(() { - provider.recordFormItem.name = value ?? ''; - }); + provider.recordFormItem.name = value ?? ''; }, decoration: buildInputDecoration( context: context, @@ -225,6 +188,30 @@ class _RecordFormPageState extends State { ); } + Widget _buildDateField(FoodProvider provider) { + return FormBuilderTextField( + name: _dateField, + readOnly: true, + initialValue: provider.recordFormItem.date, + decoration: buildInputDecoration( + context: context, + hintText: '请选择完成时间', + prefixIcon: const Icon( + Icons.calendar_month, + size: 20, + color: Color(0xFF86909C), + ), + ), + onTap: () => _onSelectDate(provider), + validator: (value) { + if (value == null || value.isEmpty) { + return '请选择完成时间'; + } + return null; + }, + ); + } + /// 选择日期 Future _onSelectDate(FoodProvider provider) async { final DateTime? picked = await showDatePicker( @@ -237,12 +224,26 @@ class _RecordFormPageState extends State { if (picked != null) { final String date = DateFormat('yyyy-MM-dd').format(picked); _formKey.currentState?.fields[_dateField]?.didChange(date); - setState(() { - provider.recordFormItem.date = date; - }); + provider.recordFormItem.date = date; } } + Widget _buildImageField(FoodProvider provider) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (provider.recordFormItem.imageUrl.isEmpty) + buildImageUploadButton(onTap: () => _pickImage(provider)) + else + ImagePreview( + imageUrls: [provider.recordFormItem.imageUrl], + index: 0, + onRemoveImage: () => _removeImage(provider), + ), + ], + ); + } + /// 选择图片(限制单张) Future _pickImage(FoodProvider provider) async { FilePickerResult? result = await FilePicker.platform.pickFiles( @@ -253,7 +254,11 @@ class _RecordFormPageState extends State { if (result != null) { PlatformFile file = result.files.first; - final fileName = await MinIOHelper().uploadFile(file: file); + final fileName = await MinIOHelper().uploadFile( + bucketName: AppConfig.bucketName, + file: file, + ); + setState(() { provider.recordFormItem.imageUrl = fileName; }); @@ -268,16 +273,16 @@ class _RecordFormPageState extends State { } /// 提交表单 - void _submitForm(FoodProvider provider) { + void _submitForm(FoodProvider provider) async { if (_formKey.currentState?.saveAndValidate() ?? false) { if (provider.recordFormItem.imageUrl.isEmpty) { - showErrorToast('请上传图片'); + ToastUtil.error('请上传图片'); return; } - print(provider.recordFormItem.name); - print(provider.recordFormItem.date); - print(provider.recordFormItem.imageUrl); + await provider.handleRecord(); + await provider.onRecordTabChange(); + Navigator.pop(context); } } @@ -300,7 +305,7 @@ class _RecordFormPageState extends State { body: SingleChildScrollView( child: Padding( padding: EdgeInsets.all(10), - child: BuildCard(child: _buildFormBuilder()), + child: CommonCard(child: _buildFormBuilder()), ), ), ); diff --git a/lib/views/stats.dart b/lib/views/stats.dart index d53ba72..4f785d8 100644 --- a/lib/views/stats.dart +++ b/lib/views/stats.dart @@ -1,7 +1,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:food_hub_app/widgets/common/chart.dart'; -import 'package:food_hub_app/widgets/common/index.dart'; import 'package:food_hub_app/widgets/stats/card.dart'; import 'package:provider/provider.dart'; @@ -21,7 +21,7 @@ class _StatsPage extends State { // 初始化加载数据 WidgetsBinding.instance.addPostFrameCallback((_) { - context.read().refreshBlogStats(); + context.read().refreshFoodStats(); }); } @@ -67,56 +67,6 @@ class _StatsPage extends State { ); } - Widget _buildRecordStats(FoodProvider provider) { - return Column( - children: [ - buildChartTitle(context: context, title: '记录统计'), - const SizedBox(height: 3), - buildChartDivider(context: context), - const SizedBox(height: 3), - Expanded( - child: lineChart( - context: context, - xAxisName: '日期', - yAxisName: '次数', - unit: '次', - data: provider.recordStats, - ), - ), - ], - ); - } - - Widget _buildCategoryStats(FoodProvider provider) { - return Column( - children: [ - buildChartTitle(context: context, title: '菜谱统计'), - const SizedBox(height: 3), - buildChartDivider(context: context), - const SizedBox(height: 3), - Expanded( - child: pieChart( - context: context, - unit: '个', - data: provider.categoryStats, - ), - ), - ], - ); - } - - Widget _buildRankStats(FoodProvider provider) { - return Column( - children: [ - buildChartTitle(context: context, title: '排行榜'), - const SizedBox(height: 3), - buildChartDivider(context: context), - const SizedBox(height: 3), - Expanded(child: rankChart(data: provider.rankStats, unit: '次')), - ], - ); - } - Widget _buildContent(FoodProvider provider) { return Column( children: [ @@ -124,17 +74,33 @@ class _StatsPage extends State { SizedBox(height: 8), SizedBox( height: chartHeight, - child: BuildCard(child: _buildRecordStats(provider)), + child: CommonCard( + child: LineChart( + title: '记录统计', + xAxisName: '日期', + yAxisName: '次数', + unit: '次', + data: provider.recordStats, + ), + ), ), SizedBox(height: 8), SizedBox( height: chartHeight, - child: BuildCard(child: _buildCategoryStats(provider)), + child: CommonCard( + child: PieChart( + title: '菜谱统计', + unit: '个', + data: provider.categoryStats, + ), + ), ), SizedBox(height: 8), SizedBox( height: chartHeight, - child: BuildCard(child: _buildRankStats(provider)), + child: CommonCard( + child: RankChart(title: '排行榜', data: provider.rankStats, unit: '次'), + ), ), ], ); @@ -147,7 +113,7 @@ class _StatsPage extends State { return Stack( children: [ if (provider.isLoading) - buildLoadingIndicator(context) + buildLoadingIndicator() else SingleChildScrollView(child: _buildContent(provider)), ], diff --git a/lib/widgets/common/chart.dart b/lib/widgets/common/chart.dart deleted file mode 100644 index cd461fc..0000000 --- a/lib/widgets/common/chart.dart +++ /dev/null @@ -1,253 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:food_hub_app/models/stats.dart'; -import 'package:syncfusion_flutter_charts/charts.dart'; - -Widget lineChart({ - required BuildContext context, - required String xAxisName, - required String yAxisName, - required String unit, - required List data, -}) { - return SfCartesianChart( - // 图表标题 - // title: ChartTitle(text: '2023年上半年销售额(万元)'), - - // X轴配置(类别轴) - primaryXAxis: CategoryAxis(majorGridLines: MajorGridLines(width: 0)), - - // Y轴配置(数值轴) - // primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName($unit)')), - - // 启用图例 - // legend: const Legend(isVisible: true), - - // 启用交互提示(点击数据点显示详情) - tooltipBehavior: TooltipBehavior( - enable: true, - format: 'point.x: point.y $unit', - ), - - // 折线图数据系列 - series: [ - LineSeries( - dataSource: data, - // X轴数据映射 - xValueMapper: (ChartData chart, _) => chart.name, - // Y轴数据映射 - yValueMapper: (ChartData chart, _) => chart.value, - // 线条颜色 - color: Theme.of(context).primaryColor, - // 线条宽度 - width: 3, - - // 数据点样式 - markerSettings: const MarkerSettings( - isVisible: true, - color: Colors.white, - shape: DataMarkerType.circle, - height: 6, - width: 6, - ), - - // 折线名称(会显示在图例中) - name: yAxisName, - - // 启用数据标签(直接显示数值) - dataLabelSettings: const DataLabelSettings( - isVisible: true, - color: Colors.white, - opacity: 0, - ), - - // 动画效果 - animationDuration: 2000, // 动画时长(毫秒) - ), - ], - ); -} - -Widget barChart({ - required BuildContext context, - required String xAxisName, - required String yAxisName, - required String unit, - required List data, -}) { - return SfCartesianChart( - // X轴配置(类别轴) - primaryXAxis: CategoryAxis( - majorGridLines: MajorGridLines(width: 0), - title: AxisTitle(text: xAxisName), - ), - - // Y轴配置(数值轴) - primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName($unit)')), - - // 启用交互提示 - tooltipBehavior: TooltipBehavior( - enable: true, - format: 'point.x: point.y $unit', - ), - - // 柱状图数据系列 - series: [ - ColumnSeries( - dataSource: data, - // X轴数据映射 - xValueMapper: (ChartData chart, _) => chart.name, - // Y轴数据映射 - yValueMapper: (ChartData chart, _) => chart.value, - - // 名称 - name: yAxisName, - - // 柱子颜色 - color: Theme.of(context).primaryColor, - - // 柱子宽度(0-1之间,1表示占满类别间隔) - width: 0.6, - - // 柱子边框 - borderWidth: 1, - borderColor: Colors.black12, - - // 数据标签 - dataLabelSettings: const DataLabelSettings( - isVisible: true, - color: Colors.white, - opacity: 0, - alignment: ChartAlignment.center, - ), - - // 动画效果 - animationDuration: 2000, - ), - ], - ); -} - -Widget pieChart({ - required BuildContext context, - required String unit, - required List data, -}) { - // 计算 value 的总和 - double sumValue = data.fold(0.0, (sum, item) => sum + item.value); - - return SfCircularChart( - // 饼图标题 - // title: ChartTitle(text: '菜谱类别占比分布'), - - // 启用图例 - legend: const Legend(isVisible: true, position: LegendPosition.right), - - // 启用交互提示(点击扇区显示详情) - tooltipBehavior: TooltipBehavior( - enable: true, - format: 'point.x: point.y $unit', - ), - - // 饼图系列配置 - series: [ - PieSeries( - dataSource: data, - // 类别映射(饼图扇区名称) - xValueMapper: (ChartData data, _) => data.name, - // 数值映射(扇区大小占比) - yValueMapper: (ChartData data, _) => data.value, - - // 扇区半径(0-1之间,1表示充满容器) - // radius: '50%', - - // 启用扇区分离效果 - explode: true, - // 指定分离的扇区索引(这里分离第一个扇区) - explodeIndex: 0, - // 分离距离 - explodeOffset: '5%', - - dataLabelMapper: (ChartData data, _) { - final percentage = (data.value / sumValue * 100).toStringAsFixed(0); - return '$percentage%'; - }, - - // 数据标签(显示在扇区上的文本) - dataLabelSettings: DataLabelSettings(isVisible: true), - - // 动画效果 - animationDuration: 2000, - ), - ], - ); -} - -Widget buildChartTitle({required BuildContext context, required String title}) { - return Row( - children: [ - Icon(Icons.insert_chart, color: Theme.of(context).colorScheme.primary), - Text(title), - ], - ); -} - -Widget buildChartDivider({required BuildContext context}) { - return Divider( - height: 1, - thickness: 1, - color: Theme.of(context).colorScheme.primary, - indent: 0, - endIndent: 0, - ); -} - -Color _getRankColor(int index) { - switch (index) { - case 0: // 第1名 - return Colors.amber; // 金色 - case 1: // 第2名 - return Colors.grey; // 银色 - case 2: // 第3名 - return Colors.orange[700]!; // 铜色 - default: - return Colors.black; // 普通颜色 - } -} - -Widget rankChart({required String unit, required List data}) { - return ListView.builder( - itemCount: data.length, - itemBuilder: (context, index) { - final item = data[index]; - final rank = index + 1; - return SizedBox( - height: 35, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - width: 25, - alignment: Alignment.center, - child: Text( - '$rank.', - style: TextStyle( - fontWeight: FontWeight.bold, - color: _getRankColor(index), - ), - ), - ), - Text(item.name, style: TextStyle(color: _getRankColor(index))), - ], - ), - Text( - '${item.value.toStringAsFixed(0)} $unit', - style: TextStyle(color: _getRankColor(index)), - ), - ], - ), - ); - }, - ); -} diff --git a/lib/widgets/common/image.dart b/lib/widgets/common/image.dart index 374a3c3..eef8d7f 100644 --- a/lib/widgets/common/image.dart +++ b/lib/widgets/common/image.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:food_hub_app/config/app_config.dart'; import 'package:photo_view/photo_view.dart'; @@ -91,138 +89,123 @@ class _ImagePreviewPageState extends State { } } -Widget buildNetworkImage( - BuildContext context, - List imageUrls, - int index, -) { - return AspectRatio( - aspectRatio: 4 / 3, - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: GestureDetector( - onTap: () { - showFullScreenImage(context, imageUrls, index); - }, - child: Image.network( - '${AppConfig.imageBaseUrl}${imageUrls[index]}', - fit: BoxFit.cover, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return buildImageLoadingIndicator(loadingProgress); - }, - errorBuilder: (context, error, stackTrace) => buildErrorImage(), - ), +class CommonImage extends StatelessWidget { + final List imageUrls; + final int index; + + 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 buildImagePreviewItem({ - required BuildContext context, - required List imageUrls, - required int index, - required VoidCallback onRemoveImage, -}) { - return Container( - decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Stack( - children: [ - buildNetworkImage(context, imageUrls, index), - buildDeleteImage(onRemoveImage: onRemoveImage), - ], - ), - ), - ); -} + Widget _buildErrorImage() { + return Container( + color: Colors.grey[200], + child: const Icon(Icons.image, color: Colors.grey, size: 30), + ); + } -Widget buildImageLoadingIndicator(ImageChunkEvent? loadingProgress) { - return Center( - child: SizedBox( - width: 30, - height: 30, - child: CircularProgressIndicator( - value: - loadingProgress?.expectedTotalBytes != null - ? loadingProgress!.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null, - ), - ), - ); -} - -Widget buildErrorImage() { - return Container( - color: Colors.grey[200], - child: const Icon(Icons.image, color: Colors.grey, size: 30), - ); -} - -Widget _buildPhotoView(ImageProvider imageProvider) { - return PhotoView( - imageProvider: imageProvider, - backgroundDecoration: const BoxDecoration(color: Colors.transparent), - minScale: PhotoViewComputedScale.contained, - maxScale: PhotoViewComputedScale.covered * 2, - initialScale: PhotoViewComputedScale.contained, - loadingBuilder: (context, event) => buildImageLoadingIndicator(event), - errorBuilder: (context, error, stackTrace) => buildErrorImage(), - ); -} - -Widget _buildCloseImage(BuildContext context) { - return Positioned( - top: MediaQuery.of(context).padding.top + 10, - right: 20, - child: IconButton( - icon: Icon(Icons.close, color: Colors.white, size: 30), - onPressed: () => Navigator.of(context).pop(), - ), - ); -} - -void showFullScreenImage( - BuildContext context, - List imageUrls, - int index, -) { - Navigator.push( - context, - MaterialPageRoute( + void _navigateToImagePreview( + BuildContext context, + List imageUrls, + int index, + ) { + final route = MaterialPageRoute( builder: (context) => ImagePreviewPage(images: imageUrls, initialIndex: index), - ), - ); - // Navigator.of(context).push( - // PageRouteBuilder( - // opaque: false, - // pageBuilder: ( - // BuildContext context, - // Animation animation, - // Animation secondaryAnimation, - // ) { - // return Scaffold( - // backgroundColor: Colors.black.withAlpha(200), - // body: Stack( - // children: [ - // // 可缩放图片 - // Positioned.fill(child: _buildPhotoView(imageProvider)), - // // 关闭按钮 - // _buildCloseImage(context), - // ], - // ), - // ); - // }, - // ), - // ); + ); + + Navigator.push(context, route); + } + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 4 / 3, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: GestureDetector( + onTap: () { + _navigateToImagePreview(context, imageUrls, index); + }, + child: Image.network( + '${AppConfig.imageBaseUrl}${imageUrls[index]}', + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return _buildImageLoadingIndicator(loadingProgress); + }, + errorBuilder: (context, error, stackTrace) => _buildErrorImage(), + ), + ), + ), + ); + } } -Widget buildImageUploadButton({required VoidCallback onPickImage}) { +class ImagePreview extends StatelessWidget { + final List imageUrls; + final int index; + final VoidCallback onRemoveImage; + + const ImagePreview({ + super.key, + required this.imageUrls, + required this.index, + required this.onRemoveImage, + }); + + Widget _buildDeleteImage() { + return Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: onRemoveImage, + child: Container( + width: 24, + height: 24, + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + child: const Icon(Icons.close, color: Colors.white, size: 16), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Stack( + children: [ + CommonImage(imageUrls: imageUrls, index: index), + _buildDeleteImage(), + ], + ), + ), + ); + } +} + +Widget buildImageUploadButton({required VoidCallback onTap}) { return InkWell( - onTap: onPickImage, + onTap: onTap, borderRadius: BorderRadius.circular(8), child: Container( width: 96, @@ -243,22 +226,3 @@ Widget buildImageUploadButton({required VoidCallback onPickImage}) { ), ); } - -Widget buildDeleteImage({required VoidCallback onRemoveImage}) { - return Positioned( - top: 0, - right: 0, - child: GestureDetector( - onTap: onRemoveImage, - child: Container( - width: 24, - height: 24, - decoration: const BoxDecoration( - color: Colors.red, - shape: BoxShape.circle, - ), - child: const Icon(Icons.close, color: Colors.white, size: 16), - ), - ), - ); -} diff --git a/lib/widgets/common/index.dart b/lib/widgets/common/index.dart index 8eb8380..d7a9934 100644 --- a/lib/widgets/common/index.dart +++ b/lib/widgets/common/index.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:toggle_switch/toggle_switch.dart'; Widget formLabelText({required String labelText, bool isRequired = false}) { @@ -28,33 +27,6 @@ InputDecoration formInputDecoration({ ); } -class BuildCard extends StatelessWidget { - final Widget? child; - - const BuildCard({super.key, this.child}); - - @override - Widget build(BuildContext context) { - final colors = Theme.of(context).colorScheme; - - return Container( - decoration: BoxDecoration( - color: colors.surfaceContainer, - borderRadius: BorderRadius.circular(12), - // border: Border.all(color: colors.outline.withAlpha(50), width: 1), - ), - padding: const EdgeInsets.all(10), - child: child, - ); - } -} - -Widget buildEmptyData() { - return Center( - child: Text('暂无数据', style: TextStyle(fontSize: 16, color: Colors.grey)), - ); -} - Widget buildToggleSwitch({ required BuildContext context, required T currentTab, @@ -153,63 +125,3 @@ Widget errorImageContainer(double height) { child: const Icon(Icons.image, color: Colors.grey, size: 30), ); } - -/// 成功消息 -void showSuccessToast(String message) { - Fluttertoast.showToast( - msg: message, - toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.TOP, - timeInSecForIosWeb: 1, - backgroundColor: Colors.green, - textColor: Colors.white, - fontSize: 16.0, - ); -} - -/// 错误消息 -void showErrorToast(String message) { - Fluttertoast.showToast( - msg: message, - toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.TOP, - timeInSecForIosWeb: 1, - backgroundColor: Colors.red, - textColor: Colors.white, - fontSize: 16.0, - ); -} - -Widget buildLoadingIndicator(BuildContext context) { - final colors = Theme.of(context).colorScheme; - - return Center( - child: Container( - padding: EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 2)), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(colors.primary), - ), - SizedBox(height: 12), - Text( - '加载中', - style: TextStyle( - fontSize: 14, - color: colors.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ); -} diff --git a/lib/widgets/common/year_selector.dart b/lib/widgets/common/year_selector.dart deleted file mode 100644 index b2f2c95..0000000 --- a/lib/widgets/common/year_selector.dart +++ /dev/null @@ -1,121 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:food_hub_app/widgets/common/index.dart'; - -class YearSelector extends StatefulWidget { - final int initialYear; - final int? minYear; - final int? maxYear; - final Function(int) onYearChanged; - - const YearSelector({ - super.key, - required this.initialYear, - required this.onYearChanged, - this.minYear, - this.maxYear, - }); - - @override - State createState() => _YearSelectorState(); -} - -// SingleTickerProviderStateMixin 动画控制器 -class _YearSelectorState extends State - with SingleTickerProviderStateMixin { - late int _currentYear; - - // 用于动画效果 - late AnimationController _animationController; - late Animation _scaleAnimation; - - @override - void initState() { - super.initState(); - _currentYear = widget.initialYear; - - // 初始化动画控制器 - _animationController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 200), - ); - - // 缩放动画 - _scaleAnimation = Tween(begin: 1.0, end: 1.1).animate( - CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), - ); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - /// 切换到上一年 - void _previousYear() { - if (widget.minYear == null || _currentYear > widget.minYear!) { - _animateYearChange(() { - setState(() { - _currentYear--; - }); - widget.onYearChanged(_currentYear); - }); - } - } - - /// 切换到下一年 - void _nextYear() { - if (widget.maxYear == null || _currentYear < widget.maxYear!) { - _animateYearChange(() { - setState(() { - _currentYear++; - }); - widget.onYearChanged(_currentYear); - }); - } - } - - /// 年份变化时的动画效果 - void _animateYearChange(VoidCallback onComplete) { - _animationController.forward().then((_) { - onComplete(); - _animationController.reverse(); - }); - } - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - circleIconButton( - context: context, - icon: Icons.chevron_left, - isSmall: true, - onPressed: () => _previousYear(), - ), - // 年份显示 - AnimatedBuilder( - animation: _scaleAnimation, - builder: (context, child) { - return Transform.scale(scale: _scaleAnimation.value, child: child); - }, - child: Text( - '$_currentYear', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryColor, - ), - ), - ), - circleIconButton( - context: context, - icon: Icons.chevron_right, - isSmall: true, - onPressed: () => _nextYear(), - ) - ], - ); - } -} diff --git a/lib/widgets/moment/card.dart b/lib/widgets/moment/card.dart index dc3cfe0..061906f 100644 --- a/lib/widgets/moment/card.dart +++ b/lib/widgets/moment/card.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.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/widgets/common/image.dart'; -import 'package:food_hub_app/widgets/common/index.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; class MomentCard extends StatelessWidget { @@ -12,7 +12,7 @@ class MomentCard extends StatelessWidget { @override Widget build(BuildContext context) { - return BuildCard( + return CommonCard( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -113,7 +113,7 @@ class MomentCard extends StatelessWidget { mainAxisSpacing: 4, childAspectRatio: itemAspectRatio, children: List.generate(imageCount, (index) { - return buildNetworkImage(context, imageUrls, index); + return CommonImage(imageUrls: imageUrls, index: index); }), ); } diff --git a/lib/widgets/recipe/calendar.dart b/lib/widgets/recipe/calendar.dart index 03229c1..e31771f 100644 --- a/lib/widgets/recipe/calendar.dart +++ b/lib/widgets/recipe/calendar.dart @@ -1,4 +1,5 @@ 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/provider/food_provider.dart'; import 'package:food_hub_app/utils/index.dart'; @@ -37,7 +38,7 @@ class _RecipeCalendarState extends State { }, onPageChanged: (focusedDay) { provider.focusedDay = focusedDay; - provider.refreshRecordList(null, null); + provider.refreshRecordList(); }, // 自定义日期单元格构建器 calendarBuilders: CalendarBuilders( @@ -191,9 +192,9 @@ class _RecipeCalendarState extends State { Widget _buildContent(FoodProvider provider) { return Column( children: [ - BuildCard(child: _recipeCalendar(provider)), + CommonCard(child: _recipeCalendar(provider)), SizedBox(height: 8), - Expanded(child: BuildCard(child: _dailyItem(context, provider))), + Expanded(child: CommonCard(child: _dailyItem(context, provider))), ], ); } @@ -205,7 +206,7 @@ class _RecipeCalendarState extends State { return Stack( children: [ if (provider.isLoading) - buildLoadingIndicator(context) + buildLoadingIndicator() else _buildContent(provider), ], diff --git a/lib/widgets/recipe/card.dart b/lib/widgets/recipe/card.dart index 4771bef..958af1c 100644 --- a/lib/widgets/recipe/card.dart +++ b/lib/widgets/recipe/card.dart @@ -1,7 +1,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:food_hub_app/widgets/common/index.dart'; class RecipeCard extends StatelessWidget { final RecipeSummary recipe; @@ -17,12 +17,12 @@ class RecipeCard extends StatelessWidget { final List imageUrls = recipe.recordList.reversed.map((record) => record.imageUrl).toList(); - return BuildCard( + return CommonCard( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ - buildNetworkImage(context, imageUrls, 0), + CommonImage(imageUrls: imageUrls, index: 0), SizedBox(height: 8), GestureDetector( onTap: () => navigatorToRecipeDetail(context), diff --git a/lib/widgets/recipe/list.dart b/lib/widgets/recipe/list.dart index f59c728..78df2f5 100644 --- a/lib/widgets/recipe/list.dart +++ b/lib/widgets/recipe/list.dart @@ -1,6 +1,6 @@ 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/widgets/common/index.dart'; import 'package:food_hub_app/widgets/recipe/card.dart'; import 'package:provider/provider.dart'; @@ -107,7 +107,7 @@ class _RecipeListState extends State { return Stack( children: [ if (provider.isLoading) - buildLoadingIndicator(context) + buildLoadingIndicator() else _buildContent(provider), ], diff --git a/lib/widgets/recipe/timeline.dart b/lib/widgets/recipe/timeline.dart index e0aba3c..300ad63 100644 --- a/lib/widgets/recipe/timeline.dart +++ b/lib/widgets/recipe/timeline.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_common/widget/common_widget.dart'; +import 'package:flutter_common/widget/year_selector.dart'; import 'package:food_hub_app/models/recipe.dart'; import 'package:food_hub_app/provider/food_provider.dart'; import 'package:food_hub_app/widgets/common/image.dart'; -import 'package:food_hub_app/widgets/common/index.dart'; -import 'package:food_hub_app/widgets/common/year_selector.dart'; import 'package:timelines_plus/timelines_plus.dart'; import 'package:provider/provider.dart'; @@ -35,7 +35,7 @@ class _RecipeTimeline extends State { SizedBox(height: 6), Padding( padding: EdgeInsets.symmetric(vertical: 8, horizontal: 0), - child: BuildCard( + child: CommonCard( child: Column( children: [ Text( @@ -43,7 +43,7 @@ class _RecipeTimeline extends State { style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 5), - buildNetworkImage(context, [record.imageUrl], 0), + CommonImage(imageUrls: [record.imageUrl], index: 0), ], ), ), @@ -87,12 +87,11 @@ class _RecipeTimeline extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ YearSelector( - initialYear: DateTime.now().year, - minYear: 2000, - maxYear: 2100, - onYearChanged: - (year) => - provider.refreshRecordList("$year-01-01", "$year-12-31"), + currentYear: provider.selectYear, + onYearChanged: (year) { + provider.selectYear = year; + provider.refreshRecordList(); + }, ), Expanded( child: Padding( @@ -111,7 +110,7 @@ class _RecipeTimeline extends State { return Stack( children: [ if (provider.isLoading) - buildLoadingIndicator(context) + buildLoadingIndicator() else _buildContent(provider), ], diff --git a/lib/widgets/stats/card.dart b/lib/widgets/stats/card.dart index e8ba197..7236995 100644 --- a/lib/widgets/stats/card.dart +++ b/lib/widgets/stats/card.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:food_hub_app/models/stats.dart'; -import 'package:food_hub_app/widgets/common/index.dart'; +import 'package:flutter_common/widget/common_widget.dart'; class StatisticCard extends StatelessWidget { final IconData icon; @@ -20,7 +19,7 @@ class StatisticCard extends StatelessWidget { @override Widget build(BuildContext context) { - return BuildCard( + return CommonCard( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/pubspec.lock b/pubspec.lock index a6b153a..6c63e95 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -170,7 +170,7 @@ packages: source: hosted version: "0.3.4+2" crypto: - dependency: "direct main" + dependency: transitive description: name: crypto sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf @@ -202,7 +202,7 @@ packages: source: hosted version: "0.7.11" dio: - dependency: "direct main" + dependency: transitive description: name: dio sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" @@ -310,6 +310,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" + flutter_common: + dependency: "direct main" + description: + path: "D:\\Projects\\FlutterProjects\\flutter_common" + relative: false + source: path + version: "1.0.0+1" flutter_form_builder: dependency: "direct main" description: @@ -366,7 +373,7 @@ packages: source: sdk version: "0.0.0" fluttertoast: - dependency: "direct main" + dependency: transitive description: name: fluttertoast sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1" @@ -614,7 +621,7 @@ packages: source: hosted version: "2.0.0" minio: - dependency: "direct main" + dependency: transitive description: name: minio sha256: ee2ce47766e46c7d164f960f2f5ed6a9a82844d877f6b82574f6876ec50c56d1 @@ -790,7 +797,7 @@ packages: source: hosted version: "6.1.0" shared_preferences: - dependency: "direct main" + dependency: transitive description: name: shared_preferences sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" @@ -931,7 +938,7 @@ packages: source: hosted version: "1.4.1" syncfusion_flutter_charts: - dependency: "direct main" + dependency: transitive description: name: syncfusion_flutter_charts sha256: c58ca79e072680af6f0554f7c4b91886c1d8808f2522b49d557f16fb5cb3bb04 diff --git a/pubspec.yaml b/pubspec.yaml index c7a5d08..b923477 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,7 +35,6 @@ dependencies: # The following adds the Cupertino Icons fonts to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 - dio: ^5.7.0 provider: ^6.1.1 timelines_plus: ^1.0.7 table_calendar: ^3.1.3 @@ -45,18 +44,15 @@ dependencies: intl: ^0.19.0 tdesign_flutter: ^0.2.3 json_annotation: ^4.9.0 - fluttertoast: ^8.2.0 - shared_preferences: ^2.3.0 logger: ^2.6.0 photo_view: ^0.15.0 flutter_carousel_widget: ^3.1.0 easy_refresh: ^3.4.0 - syncfusion_flutter_charts: ^30.1.41 - minio: ^3.5.8 - crypto: ^3.0.7 file_picker: ^10.3.3 share_plus: ^11.0.0 path_provider: ^2.1.5 + flutter_common: + path: ..\flutter_common dependency_overrides: tdesign_flutter_adaptation: 3.16.0