feat:更新美食记录表单

This commit is contained in:
2025-11-23 00:02:41 +08:00
parent 1ac504f3d7
commit 6308169013
41 changed files with 465 additions and 1618 deletions

View File

@@ -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/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<bool> addMomentApi(Moment moment) { Future<bool> addMomentApi(Moment moment) {
return HttpUtil().post<bool>("/moment", data: moment); return httpUtil.post<bool>("/moment", data: moment);
} }
Future<bool> updateMomentApi(int id, Moment moment) { Future<bool> updateMomentApi(int id, Moment moment) {
return HttpUtil().put<bool>("/moment/$id", data: moment); return httpUtil.put<bool>("/moment/$id", data: moment);
} }
Future<bool> deleteMomentApi(int id) { Future<bool> deleteMomentApi(int id) {
return HttpUtil().delete<bool>("/moment/$id"); return httpUtil.delete<bool>("/moment/$id");
} }
Future<List<Moment>> queryMomentListApi() { Future<List<Moment>> queryMomentListApi() {
return HttpUtil().get<List<Moment>>( return httpUtil.get<List<Moment>>(
"/moment", "/moment",
converter: (data) => convertListResponse<Moment>(data, Moment.fromJson), converter: (data) => convertList<Moment>(data, Moment.fromJson),
); );
} }
Future<PageMoment> queryMomentByPageApi(int currentPage, int pageSize) { Future<PageMoment> queryMomentByPageApi(int currentPage, int pageSize) {
return HttpUtil().get<PageMoment>( return httpUtil.get<PageMoment>(
"/moment/page", "/moment/page",
queryParameters: {"currentPage": currentPage, "pageSize": pageSize}, queryParameters: {"currentPage": currentPage, "pageSize": pageSize},
converter: (data) => PageMoment.fromJson(data), converter: (data) => PageMoment.fromJson(data),
@@ -30,13 +33,13 @@ Future<PageMoment> queryMomentByPageApi(int currentPage, int pageSize) {
} }
Future<bool> addMomentCommentApi(int id, String content) { Future<bool> addMomentCommentApi(int id, String content) {
return HttpUtil().post<bool>("/moment/$id/comment", data: content); return httpUtil.post<bool>("/moment/$id/comment", data: content);
} }
Future<bool> addMomentLikeApi(int id) { Future<bool> addMomentLikeApi(int id) {
return HttpUtil().post<bool>("/food/moment/$id/like"); return httpUtil.post<bool>("/food/moment/$id/like");
} }
Future<bool> deleteMomentLikeApi(int id) { Future<bool> deleteMomentLikeApi(int id) {
return HttpUtil().delete<bool>("/food/moment/$id/like"); return httpUtil.delete<bool>("/food/moment/$id/like");
} }

View File

@@ -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/models/recipe.dart';
import 'package:food_hub_app/utils/http_util.dart';
import 'package:food_hub_app/utils/index.dart'; import 'package:food_hub_app/utils/index.dart';
final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl);
Future<RecipeDetail> queryRecipeByIdApi(int id) { Future<RecipeDetail> queryRecipeByIdApi(int id) {
return HttpUtil().get<RecipeDetail>( return httpUtil.get<RecipeDetail>(
"/food/recipe/$id", "/food/recipe/$id",
converter: (data) => RecipeDetail.fromJson(data), converter: (data) => RecipeDetail.fromJson(data),
); );
} }
Future<bool> addRecipeApi(Recipe recipe) { Future<bool> addRecipeApi(Recipe recipe) {
return HttpUtil().post<bool>("/food/recipe", data: recipe); return httpUtil.post<bool>("/food/recipe", data: recipe);
} }
Future<bool> updateRecipeApi(int id, Recipe recipe) { Future<bool> updateRecipeApi(int id, Recipe recipe) {
return HttpUtil().put<bool>("/food/recipe/$id", data: recipe); return httpUtil.put<bool>("/food/recipe/$id", data: recipe);
} }
Future<bool> deleteRecipeApi(int id) { Future<bool> deleteRecipeApi(int id) {
return HttpUtil().delete<bool>("/food/recipe/$id"); return httpUtil.delete<bool>("/food/recipe/$id");
} }
Future<List<Recipe>> queryRecipeByUserApi(int id) { Future<List<Recipe>> queryRecipeByUserApi(int id) {
return HttpUtil().get<List<Recipe>>( return httpUtil.get<List<Recipe>>(
"/food/recipe/user/$id", "/food/recipe/user/$id",
converter: (data) => convertListResponse<Recipe>(data, Recipe.fromJson), converter: (data) => convertList<Recipe>(data, Recipe.fromJson),
); );
} }
Future<List<Recipe>> queryRecipeUserFavouriteApi() { Future<List<Recipe>> queryRecipeUserFavouriteApi() {
return HttpUtil().get<List<Recipe>>( return httpUtil.get<List<Recipe>>(
"/food/recipe/user/favourite", "/food/recipe/user/favourite",
converter: (data) => convertListResponse<Recipe>(data, Recipe.fromJson), converter: (data) => convertList<Recipe>(data, Recipe.fromJson),
); );
} }
Future<List<RecipeSummary>> queryRecipeApi(RecipeQuery recipeQuery) { Future<List<RecipeSummary>> queryRecipeApi(RecipeQuery recipeQuery) {
return HttpUtil().get<List<RecipeSummary>>( return httpUtil.get<List<RecipeSummary>>(
"/food/recipe", "/food/recipe",
queryParameters: recipeQuery.toJson(), queryParameters: recipeQuery.toJson(),
converter: converter:
(data) => (data) => convertList<RecipeSummary>(data, RecipeSummary.fromJson),
convertListResponse<RecipeSummary>(data, RecipeSummary.fromJson),
); );
} }
Future<List<String>> queryFoodNameListApi() { Future<List<String>> queryFoodNameListApi() {
return HttpUtil().get<List<String>>( return httpUtil.get<List<String>>(
"/food/recipe/name", "/food/recipe/name",
converter: (data) => convertListStringResponse(data), converter: (data) => convertStringList(data),
); );
} }
Future<bool> addRecordApi(FoodRecord record) { Future<bool> addRecordApi(FoodRecord record) {
return HttpUtil().post<bool>("/food/record", data: record); return httpUtil.post<bool>("/food/record", data: record);
} }
Future<bool> updateRecordApi(int id, FoodRecord record) { Future<bool> updateRecordApi(int id, FoodRecord record) {
return HttpUtil().put<bool>("/food/record/$id", data: record); return httpUtil.put<bool>("/food/record/$id", data: record);
} }
Future<bool> deleteRecordApi(int id) { Future<bool> deleteRecordApi(int id) {
return HttpUtil().delete<bool>("/food/record/$id"); return httpUtil.delete<bool>("/food/record/$id");
} }
Future<bool> addRecipeCommentApi(int id, String content) { Future<bool> addRecipeCommentApi(int id, String content) {
return HttpUtil().post<bool>("/food/recipe/$id/comment", data: content); return httpUtil.post<bool>("/food/recipe/$id/comment", data: content);
} }
Future<bool> addRecipeLikeApi(int id) { Future<bool> addRecipeLikeApi(int id) {
return HttpUtil().post<bool>("/food/recipe/$id/like"); return httpUtil.post<bool>("/food/recipe/$id/like");
} }
Future<bool> deleteRecipeLikeApi(int id) { Future<bool> deleteRecipeLikeApi(int id) {
return HttpUtil().delete<bool>("/food/recipe/$id/like"); return httpUtil.delete<bool>("/food/recipe/$id/like");
} }
Future<bool> addRecipeFavouriteApi(int id) { Future<bool> addRecipeFavouriteApi(int id) {
return HttpUtil().post<bool>("/food/recipe/$id/favourite"); return httpUtil.post<bool>("/food/recipe/$id/favourite");
} }
Future<bool> deleteRecipeFavouriteApi(int id) { Future<bool> deleteRecipeFavouriteApi(int id) {
return HttpUtil().delete<bool>("/food/recipe/$id/like"); return httpUtil.delete<bool>("/food/recipe/$id/like");
} }
Future<List<FoodRecord>> queryRecordApi(String startDate, String endDate) { Future<List<FoodRecord>> queryRecordApi(String startDate, String endDate) {
return HttpUtil().get<List<FoodRecord>>( return httpUtil.get<List<FoodRecord>>(
"/food/record", "/food/record",
queryParameters: {"startDate": startDate, "endDate": endDate}, queryParameters: {"startDate": startDate, "endDate": endDate},
converter: converter: (data) => convertList<FoodRecord>(data, FoodRecord.fromJson),
(data) => convertListResponse<FoodRecord>(data, FoodRecord.fromJson),
); );
} }
Future<List<String>> queryCategoryApi() { Future<List<String>> queryCategoryApi() {
return HttpUtil().get<List<String>>( return httpUtil.get<List<String>>(
"/food/category", "/food/category",
converter: (data) => convertListStringResponse(data), converter: (data) => convertListStringResponse(data),
); );

View File

@@ -1,8 +1,11 @@
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/models/session.dart';
import 'package:food_hub_app/utils/http_util.dart';
final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl);
Future<Session> loginApi(String username, String password) { Future<Session> loginApi(String username, String password) {
return HttpUtil().post<Session>( return httpUtil.post<Session>(
"/session", "/session",
queryParameters: {"username": username, "password": password}, queryParameters: {"username": username, "password": password},
converter: (data) => Session.fromJson(data), converter: (data) => Session.fromJson(data),

View File

@@ -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/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<SummaryStats> queryStatsApi() { Future<SummaryStats> queryStatsApi() {
return HttpUtil().get<SummaryStats>( return httpUtil.get<SummaryStats>(
"/food/stats", "/food/stats",
converter: (data) => SummaryStats.fromJson(data), converter: (data) => SummaryStats.fromJson(data),
); );
} }
Future<List<ChartData>> queryRecordStatsApi() { Future<List<ChartData>> queryRecordStatsApi() {
return HttpUtil().get<List<ChartData>>( return httpUtil.get<List<ChartData>>(
"/food/stats/record", "/food/stats/record",
converter: (data) => convertListResponse<ChartData>(data, ChartData.fromJson), converter: (data) => convertList<ChartData>(data, ChartData.fromJson),
); );
} }
Future<List<ChartData>> queryCategoryStatsApi() { Future<List<ChartData>> queryCategoryStatsApi() {
return HttpUtil().get<List<ChartData>>( return httpUtil.get<List<ChartData>>(
"/food/stats/category", "/food/stats/category",
converter: (data) => convertListResponse<ChartData>(data, ChartData.fromJson), converter: (data) => convertList<ChartData>(data, ChartData.fromJson),
); );
} }
Future<List<ChartData>> queryRankStatsApi() { Future<List<ChartData>> queryRankStatsApi() {
return HttpUtil().get<List<ChartData>>( return httpUtil.get<List<ChartData>>(
"/food/stats/rank", "/food/stats/rank",
converter: (data) => convertListResponse<ChartData>(data, ChartData.fromJson), converter: (data) => convertList<ChartData>(data, ChartData.fromJson),
); );
} }

View File

@@ -10,6 +10,6 @@ class AppConfig {
static const String rustfsIp = '14.103.235.151'; static const String rustfsIp = '14.103.235.151';
static const String rustfsFileUrl = 'http://14.103.235.151:9100'; static const String rustfsFileUrl = 'http://14.103.235.151:9100';
static const String bucketName = 'food'; static const String bucketName = 'food';
// static const String imageBaseUrl = '$rustfsFileUrl/$bucketName/'; static const String imageBaseUrl = '$rustfsFileUrl/$bucketName/';
static const String imageBaseUrl = '$baseApiUrl/'; // static const String imageBaseUrl = '$baseApiUrl/';
} }

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; 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 { class AppDrawer extends StatefulWidget {
const AppDrawer({super.key}); const AppDrawer({super.key});

View File

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

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; 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:flutter_localizations/flutter_localizations.dart';
import 'package:food_hub_app/provider/food_provider.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/home.dart';
import 'package:food_hub_app/views/login.dart'; import 'package:food_hub_app/views/login.dart';
import 'package:food_hub_app/views/moment_form.dart'; import 'package:food_hub_app/views/moment_form.dart';
@@ -13,8 +13,10 @@ import 'package:provider/provider.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
FormBuilderLocalizations.delegate.load(const Locale('zh', 'CN')); FormBuilderLocalizations.delegate.load(const Locale('zh', 'CN'));
await SPUtil.init(); await SPUtil.init();
runApp( runApp(
MultiProvider( MultiProvider(
providers: [ providers: [

View File

@@ -4,13 +4,13 @@ part 'moment.g.dart';
@JsonSerializable() @JsonSerializable()
class Comment { class Comment {
final int id; int id;
final String username; String username;
final String avatar; String avatar;
final String content; String content;
final String date; String date;
const Comment({ Comment({
required this.id, required this.id,
required this.username, required this.username,
required this.avatar, required this.avatar,
@@ -25,15 +25,15 @@ class Comment {
@JsonSerializable() @JsonSerializable()
class Moment { class Moment {
final int? id; int? id;
final int? userId; int? userId;
final String? username; String? username;
final String? avatar; String? avatar;
final String content; String content;
final List<String> imageList; List<String> imageList;
final String? date; String? date;
final List<int>? likeList; List<int>? likeList;
final List<Comment>? commentList; List<Comment>? commentList;
Moment({ Moment({
this.id, this.id,

View File

@@ -83,9 +83,9 @@ class FoodRecord {
}); });
factory FoodRecord.fromJson(Map<String, dynamic> json) => factory FoodRecord.fromJson(Map<String, dynamic> json) =>
_$RecordFromJson(json); _$FoodRecordFromJson(json);
Map<String, dynamic> toJson() => _$RecordToJson(this); Map<String, dynamic> toJson() => _$FoodRecordToJson(this);
static FoodRecord getEmpty() { static FoodRecord getEmpty() {
return FoodRecord( return FoodRecord(

View File

@@ -51,7 +51,7 @@ Map<String, dynamic> _$RecipeCommentToJson(RecipeComment instance) =>
'date': instance.date, 'date': instance.date,
}; };
FoodRecord _$RecordFromJson(Map<String, dynamic> json) => FoodRecord( FoodRecord _$FoodRecordFromJson(Map<String, dynamic> json) => FoodRecord(
id: (json['id'] as num?)?.toInt(), id: (json['id'] as num?)?.toInt(),
name: json['name'] as String, name: json['name'] as String,
category: json['category'] as String, category: json['category'] as String,
@@ -60,7 +60,8 @@ FoodRecord _$RecordFromJson(Map<String, dynamic> json) => FoodRecord(
imageUrl: json['imageUrl'] as String, imageUrl: json['imageUrl'] as String,
); );
Map<String, dynamic> _$RecordToJson(FoodRecord instance) => <String, dynamic>{ Map<String, dynamic> _$FoodRecordToJson(FoodRecord instance) =>
<String, dynamic>{
'id': instance.id, 'id': instance.id,
'name': instance.name, 'name': instance.name,
'category': instance.category, 'category': instance.category,

View File

@@ -19,16 +19,3 @@ class SummaryStats {
Map<String, dynamic> toJson() => _$SummaryStatsToJson(this); Map<String, dynamic> toJson() => _$SummaryStatsToJson(this);
} }
@JsonSerializable()
class ChartData {
final String name;
final double value;
const ChartData({required this.name, required this.value});
factory ChartData.fromJson(Map<String, dynamic> json) =>
_$ChartDataFromJson(json);
Map<String, dynamic> toJson() => _$ChartDataToJson(this);
}

View File

@@ -18,13 +18,3 @@ Map<String, dynamic> _$SummaryStatsToJson(SummaryStats instance) =>
'categoryCount': instance.categoryCount, 'categoryCount': instance.categoryCount,
'workCount': instance.workCount, 'workCount': instance.workCount,
}; };
ChartData _$ChartDataFromJson(Map<String, dynamic> json) => ChartData(
name: json['name'] as String,
value: (json['value'] as num).toDouble(),
);
Map<String, dynamic> _$ChartDataToJson(ChartData instance) => <String, dynamic>{
'name': instance.name,
'value': instance.value,
};

View File

@@ -1,4 +1,6 @@
import 'package:flutter/material.dart'; 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/moment.dart';
import 'package:food_hub_app/apis/recipe.dart'; import 'package:food_hub_app/apis/recipe.dart';
import 'package:food_hub_app/apis/stats.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/models/stats.dart';
import 'package:food_hub_app/utils/date_util.dart'; import 'package:food_hub_app/utils/date_util.dart';
import 'package:food_hub_app/utils/index.dart'; import 'package:food_hub_app/utils/index.dart';
import 'package:food_hub_app/views/record.dart';
class FoodProvider with ChangeNotifier { class FoodProvider with ChangeNotifier {
late RecordTab currentRecordTab = RecordTab.recipe;
late FoodRecord recordFormItem; late FoodRecord recordFormItem;
late Moment momentFormItem; late Moment momentFormItem;
@@ -20,6 +24,7 @@ class FoodProvider with ChangeNotifier {
late List<String> categoryList = []; late List<String> categoryList = [];
late List<RecipeSummary> recipeSummaryList = []; late List<RecipeSummary> recipeSummaryList = [];
int selectYear = DateTime.now().year;
DateTime selectedDay = DateTime.now(); DateTime selectedDay = DateTime.now();
DateTime focusedDay = DateTime.now(); DateTime focusedDay = DateTime.now();
@@ -61,6 +66,18 @@ class FoodProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
Future<void> onRecordTabChange() async {
switch (currentRecordTab) {
case RecordTab.recipe:
await refreshRecipeList();
break;
case RecordTab.calendar:
case RecordTab.timeline:
await refreshRecordList();
break;
}
}
Future<void> queryCategoryList() async { Future<void> queryCategoryList() async {
try { try {
error = null; error = null;
@@ -98,16 +115,31 @@ class FoodProvider with ChangeNotifier {
} }
} }
Future<void> refreshRecordList(String? startDate, String? endDate) async { Future<void> refreshRecordList() async {
if (isLoading) return; if (isLoading) return;
if (currentRecordTab == RecordTab.recipe) return;
try { try {
isLoading = true; isLoading = true;
error = null; error = null;
notifyListeners(); notifyListeners();
startDate ??= getFirstDayOfMonth(focusedDay); late String startDate;
endDate ??= getLastDayOfMonth(focusedDay); 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); final result = await queryRecordApi(startDate, endDate);
@@ -129,6 +161,30 @@ class FoodProvider with ChangeNotifier {
} }
} }
Future<void> 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() { void refreshSelectRecord() {
selectRecordList = selectRecordList =
recordList recordList
@@ -136,7 +192,7 @@ class FoodProvider with ChangeNotifier {
.toList(); .toList();
} }
Future<void> refreshBlogStats() async { Future<void> refreshFoodStats() async {
if (isLoading) return; if (isLoading) return;
try { try {

View File

@@ -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<void> _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<void> _saveTheme() async {
await _prefs.setString(_themeKey, currentTheme.name);
}
// 保存暗黑模式
Future<void> _saveDarkMode() async {
await _prefs.setBool(_darkModeKey, isDarkMode);
}
List<ThemeColor> 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',
);
}
}

View File

@@ -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<String, dynamic> 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<T> _request<T>(
String path, {
required String method,
dynamic data,
Map<String, dynamic>? 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<T> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
T Function(dynamic data)? converter,
}) => _request(
path,
method: "GET",
queryParameters: queryParameters,
converter: converter,
);
// POST请求
Future<T> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
T Function(dynamic data)? converter,
}) => _request(
path,
method: "POST",
data: data,
queryParameters: queryParameters,
converter: converter,
);
// PUT请求
Future<T> put<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
T Function(dynamic data)? converter,
}) => _request(
path,
method: "PUT",
data: data,
queryParameters: queryParameters,
converter: converter,
);
// DELETE请求
Future<T> delete<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
T Function(dynamic data)? converter,
}) => _request(
path,
method: "DELETE",
data: data,
queryParameters: queryParameters,
converter: converter,
);
// 文件上传
Future<dynamic> upload(
String path,
String filePath, {
Map<String, dynamic>? 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<String, dynamic> && 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';
}
}
}

View File

@@ -16,18 +16,7 @@ String formatDateTime(DateTime dateTime, [String format = 'yyyy-MM-dd']) {
return Intl.withLocale('zh_CN', () => formatter.format(dateTime)); return Intl.withLocale('zh_CN', () => formatter.format(dateTime));
} }
/// 通用列表转换函数
List<T> convertListResponse<T>(
dynamic data,
T Function(Map<String, dynamic>) fromJson,
) {
if (data is List) {
return data.map((item) => fromJson(item as Map<String, dynamic>)).toList();
}
throw FormatException(
'Expected a list of items for conversion, but got ${data.runtimeType}',
);
}
List<String> convertListStringResponse(dynamic data) { List<String> convertListStringResponse(dynamic data) {
if (data is List) { if (data is List) {

View File

@@ -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')),
);

View File

@@ -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<String> 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<String> _generateMD5HashName(String filePath) async {
final file = File(filePath);
final bytes = await file.readAsBytes();
final hash = md5.convert(bytes);
return hash.toString();
}
}

View File

@@ -3,7 +3,7 @@ import 'dart:typed_data';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.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:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
@@ -115,10 +115,10 @@ class ScreenshotUtil {
// 分享后删除临时文件 // 分享后删除临时文件
await imageFile.delete(); await imageFile.delete();
} else { } else {
showErrorToast('生成图片失败,请重试'); ToastUtil.error('生成图片失败,请重试');
} }
} catch (e) { } catch (e) {
showErrorToast('分享失败: $e'); ToastUtil.error('分享失败: $e');
} }
} }
} }

View File

@@ -1,61 +0,0 @@
import 'package:shared_preferences/shared_preferences.dart';
class SPUtil {
static late SharedPreferences _prefs;
/// 初始化
static Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
/// 存储数据
static Future<bool> 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<String>) 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<String> getStringList(String key, [List<String> defaultValue = const []]) {
return _prefs.getStringList(key) ?? defaultValue;
}
/// 删除数据
static Future<bool> remove(String key) {
return _prefs.remove(key);
}
/// 清空所有数据
static Future<bool> clear() {
return _prefs.clear();
}
}

View File

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

View File

@@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; 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:flutter_form_builder/flutter_form_builder.dart';
import 'package:food_hub_app/apis/session.dart'; import 'package:food_hub_app/apis/session.dart';
import 'package:food_hub_app/models/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:food_hub_app/widgets/common/index.dart';
import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:form_builder_validators/form_builder_validators.dart';
@@ -69,7 +70,7 @@ class _LoginPage extends State<LoginPage> {
if ((_formKey.currentState as FormState).validate()) { if ((_formKey.currentState as FormState).validate()) {
(_formKey.currentState as FormState).save(); (_formKey.currentState as FormState).save();
Session session = await loginApi(_username, _password); Session session = await loginApi(_username, _password);
showSuccessToast('登录成功'); ToastUtil.success('登录成功');
handleRememberState(); handleRememberState();
handleTokenState(session.saToken.tokenValue); handleTokenState(session.saToken.tokenValue);

View File

@@ -1,8 +1,8 @@
import 'package:easy_refresh/easy_refresh.dart'; import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.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/provider/food_provider.dart';
import 'package:food_hub_app/widgets/common/easy_refresh.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:food_hub_app/widgets/moment/card.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -108,7 +108,7 @@ class _MomentPageState extends State<MomentPage> {
body: Stack( body: Stack(
children: [ children: [
if (provider.isLoading) if (provider.isLoading)
buildLoadingIndicator(context) buildLoadingIndicator()
else else
_buildMomentList(provider), _buildMomentList(provider),
], ],

View File

@@ -1,11 +1,14 @@
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.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: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/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/form.dart';
import 'package:food_hub_app/widgets/common/image.dart'; import 'package:food_hub_app/widgets/common/image.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class MomentFormPage extends StatefulWidget { class MomentFormPage extends StatefulWidget {
@@ -22,7 +25,9 @@ class _MomentFormPageState extends State<MomentFormPage> {
static const int maxContentLength = 200; static const int maxContentLength = 200;
static const int maxImageCount = 9; // 最大图片数量 static const int maxImageCount = 9; // 最大图片数量
Widget _buildContentField() { Widget _buildContentField(FoodProvider provider) {
final colors = Theme.of(context).colorScheme;
return FormBuilderTextField( return FormBuilderTextField(
name: _contentField, name: _contentField,
focusNode: _focusNode, focusNode: _focusNode,
@@ -38,16 +43,13 @@ class _MomentFormPageState extends State<MomentFormPage> {
return Text( return Text(
'$currentLength/$maxContentLength', '$currentLength/$maxContentLength',
style: TextStyle( style: TextStyle(
color: color: currentLength > maxLength! ? Colors.red : colors.primary,
currentLength > maxLength!
? Colors.red
: Theme.of(context).colorScheme.primary,
), ),
); );
}, },
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
// provider.momentFormItem.content = value!; provider.momentFormItem.content = value!;
}); });
}, },
decoration: buildInputDecoration(context: context, hintText: '请输入朋友圈内容'), decoration: buildInputDecoration(context: context, hintText: '请输入朋友圈内容'),
@@ -56,39 +58,41 @@ class _MomentFormPageState extends State<MomentFormPage> {
return '请输入朋友圈内容'; return '请输入朋友圈内容';
} }
if (value.length > maxContentLength) { if (value.length > maxContentLength) {
// 修正为使用 maxContentLength return '内容不能超过$maxContentLength';
return '内容不能超过${maxContentLength}';
} }
return null; return null;
}, },
); );
} }
Widget _buildImageGrid(FoodProvider provider) { Widget _buildImageField(FoodProvider provider) {
final imageUrls = provider.momentFormItem.imageList; final imageUrls = provider.momentFormItem.imageList;
final totalItems =
imageUrls.length + (imageUrls.length < maxImageCount ? 1 : 0);
return Wrap( return GridView.builder(
spacing: 8, shrinkWrap: true,
runSpacing: 8, physics: const NeverScrollableScrollPhysics(),
children: [ gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
...imageUrls.asMap().entries.map((entry) { crossAxisCount: 3,
final index = entry.key; crossAxisSpacing: 8,
final imageUrl = entry.value; mainAxisSpacing: 8,
return Stack( childAspectRatio: 1,
children: [ ),
buildImagePreviewItem( itemCount: totalItems,
context: context, itemBuilder: (context, index) {
if (index < imageUrls.length) {
return ImagePreview(
imageUrls: imageUrls, imageUrls: imageUrls,
index: index, index: index,
onRemoveImage: () => _removeImage(provider, index), onRemoveImage: () => _removeImage(provider, index),
)
],
); );
}), }
// 上传按钮(如果还有剩余位置 // 上传按钮(最后一个
if (imageUrls.length < maxImageCount) else {
buildImageUploadButton(onPickImage: () => _pickImages(provider)), return buildImageUploadButton(onTap: () => _pickImages(provider));
], }
},
); );
} }
@@ -102,14 +106,14 @@ class _MomentFormPageState extends State<MomentFormPage> {
children: [ children: [
buildFormLabel('朋友圈内容', required: true), buildFormLabel('朋友圈内容', required: true),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildContentField(), _buildContentField(provider),
const SizedBox(height: 20), const SizedBox(height: 8),
buildFormLabel('上传图片', required: true), buildFormLabel('上传图片', required: true),
const SizedBox(height: 10), const SizedBox(height: 8),
_buildImageGrid(provider), _buildImageField(provider),
const SizedBox(height: 8),
const SizedBox(height: 20),
buildFormButtonGroup( buildFormButtonGroup(
context: context, context: context,
onConfirm: () => _submitForm(provider), onConfirm: () => _submitForm(provider),
@@ -125,7 +129,7 @@ class _MomentFormPageState extends State<MomentFormPage> {
final remainingCount = maxImageCount - currentCount; final remainingCount = maxImageCount - currentCount;
if (remainingCount <= 0) { if (remainingCount <= 0) {
showErrorToast('最多只能上传$maxImageCount张图片'); ToastUtil.error('最多只能上传$maxImageCount张图片');
return; return;
} }
@@ -147,7 +151,11 @@ class _MomentFormPageState extends State<MomentFormPage> {
// 逐个上传图片 // 逐个上传图片
for (final file in filesToUpload) { 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); newImageUrls.add(fileName);
} }
@@ -156,9 +164,9 @@ class _MomentFormPageState extends State<MomentFormPage> {
provider.momentFormItem.imageList.addAll(newImageUrls); provider.momentFormItem.imageList.addAll(newImageUrls);
}); });
showSuccessToast('图片上传成功'); ToastUtil.success('图片上传成功');
} catch (e) { } catch (e) {
showErrorToast('图片上传失败'); ToastUtil.error('图片上传失败');
} }
} }
} }
@@ -199,7 +207,7 @@ class _MomentFormPageState extends State<MomentFormPage> {
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: BuildCard(child: _buildFormBuilder()), child: CommonCard(child: _buildFormBuilder()),
), ),
), ),
); );

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; 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/apis/recipe.dart';
import 'package:food_hub_app/models/recipe.dart'; import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/utils/screenshot_util.dart'; import 'package:food_hub_app/utils/screenshot_util.dart';
@@ -226,7 +227,7 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
required String title, required String title,
required Widget content, required Widget content,
}) { }) {
return BuildCard( return CommonCard(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

View File

@@ -24,7 +24,6 @@ class RecordPage extends StatefulWidget {
} }
class _RecordPageState extends State<RecordPage> { class _RecordPageState extends State<RecordPage> {
RecordTab _currentTab = RecordTab.recipe;
final List<Widget> _tabPages = [ final List<Widget> _tabPages = [
RecipeList(), RecipeList(),
RecipeCalendar(), RecipeCalendar(),
@@ -49,22 +48,8 @@ class _RecordPageState extends State<RecordPage> {
} }
void _onTabChange(RecordTab tab, FoodProvider provider) { void _onTabChange(RecordTab tab, FoodProvider provider) {
setState(() { provider.currentRecordTab = tab;
_currentTab = tab; provider.onRecordTabChange();
});
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;
}
} }
@override @override
@@ -75,11 +60,14 @@ class _RecordPageState extends State<RecordPage> {
children: [ children: [
_buildTabs( _buildTabs(
context: context, context: context,
currentTab: _currentTab, currentTab: provider.currentRecordTab,
onTabChanged: (tab) => _onTabChange(tab, provider), onTabChanged: (tab) => _onTabChange(tab, provider),
), ),
Expanded( Expanded(
child: IndexedStack(index: _currentTab.index, children: _tabPages), child: IndexedStack(
index: provider.currentRecordTab.index,
children: _tabPages,
),
), ),
], ],
); );

View File

@@ -1,12 +1,14 @@
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.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:flutter_form_builder/flutter_form_builder.dart';
import 'package:food_hub_app/apis/recipe.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/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/form.dart';
import 'package:food_hub_app/widgets/common/image.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:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -48,52 +50,19 @@ class _RecordFormPageState extends State<RecordFormPage> {
children: [ children: [
buildFormLabel('菜谱名称', required: true), buildFormLabel('菜谱名称', required: true),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildRecipeAutocomplete(provider), _buildRecipeField(provider),
const SizedBox(height: 8),
const SizedBox(height: 10),
buildFormLabel('完成时间', required: true), buildFormLabel('完成时间', required: true),
const SizedBox(height: 8), const SizedBox(height: 8),
FormBuilderTextField( _buildDateField(provider),
name: _dateField, const SizedBox(height: 8),
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),
buildFormLabel('上传图片', required: true), buildFormLabel('上传图片', required: true),
const SizedBox(height: 10), const SizedBox(height: 8),
Column( _buildImageField(provider),
crossAxisAlignment: CrossAxisAlignment.start, const SizedBox(height: 8),
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: 10),
buildFormButtonGroup( buildFormButtonGroup(
context: context, context: context,
onConfirm: () => _submitForm(provider), onConfirm: () => _submitForm(provider),
@@ -103,7 +72,7 @@ class _RecordFormPageState extends State<RecordFormPage> {
); );
} }
Widget _buildRecipeAutocomplete(FoodProvider provider) { Widget _buildRecipeField(FoodProvider provider) {
return Autocomplete<String>( return Autocomplete<String>(
initialValue: TextEditingValue(text: provider.recordFormItem.name), initialValue: TextEditingValue(text: provider.recordFormItem.name),
optionsBuilder: (TextEditingValue textEditingValue) { optionsBuilder: (TextEditingValue textEditingValue) {
@@ -118,9 +87,7 @@ class _RecordFormPageState extends State<RecordFormPage> {
}, },
onSelected: (String value) { onSelected: (String value) {
setState(() {
provider.recordFormItem.name = value; provider.recordFormItem.name = value;
});
}, },
optionsViewBuilder: ( optionsViewBuilder: (
@@ -149,7 +116,7 @@ class _RecordFormPageState extends State<RecordFormPage> {
child: Container( child: Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxHeight: 200, maxHeight: 200,
maxWidth: MediaQuery.of(context).size.width - 32, maxWidth: MediaQuery.of(context).size.width - 58,
), ),
child: ListView.builder( child: ListView.builder(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
@@ -179,9 +146,7 @@ class _RecordFormPageState extends State<RecordFormPage> {
void onClearRecipeName() { void onClearRecipeName() {
controller.clear(); controller.clear();
setState(() {
provider.recordFormItem.name = ''; provider.recordFormItem.name = '';
});
} }
Widget? buildSuffixIcon() { Widget? buildSuffixIcon() {
@@ -201,9 +166,7 @@ class _RecordFormPageState extends State<RecordFormPage> {
focusNode: focusNode, focusNode: focusNode,
enabled: !provider.isEditing, enabled: !provider.isEditing,
onChanged: (value) { onChanged: (value) {
setState(() {
provider.recordFormItem.name = value ?? ''; provider.recordFormItem.name = value ?? '';
});
}, },
decoration: buildInputDecoration( decoration: buildInputDecoration(
context: context, context: context,
@@ -225,6 +188,30 @@ class _RecordFormPageState extends State<RecordFormPage> {
); );
} }
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<void> _onSelectDate(FoodProvider provider) async { Future<void> _onSelectDate(FoodProvider provider) async {
final DateTime? picked = await showDatePicker( final DateTime? picked = await showDatePicker(
@@ -237,12 +224,26 @@ class _RecordFormPageState extends State<RecordFormPage> {
if (picked != null) { if (picked != null) {
final String date = DateFormat('yyyy-MM-dd').format(picked); final String date = DateFormat('yyyy-MM-dd').format(picked);
_formKey.currentState?.fields[_dateField]?.didChange(date); _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<void> _pickImage(FoodProvider provider) async { Future<void> _pickImage(FoodProvider provider) async {
FilePickerResult? result = await FilePicker.platform.pickFiles( FilePickerResult? result = await FilePicker.platform.pickFiles(
@@ -253,7 +254,11 @@ class _RecordFormPageState extends State<RecordFormPage> {
if (result != null) { if (result != null) {
PlatformFile file = result.files.first; PlatformFile file = result.files.first;
final fileName = await MinIOHelper().uploadFile(file: file); final fileName = await MinIOHelper().uploadFile(
bucketName: AppConfig.bucketName,
file: file,
);
setState(() { setState(() {
provider.recordFormItem.imageUrl = fileName; provider.recordFormItem.imageUrl = fileName;
}); });
@@ -268,16 +273,16 @@ class _RecordFormPageState extends State<RecordFormPage> {
} }
/// 提交表单 /// 提交表单
void _submitForm(FoodProvider provider) { void _submitForm(FoodProvider provider) async {
if (_formKey.currentState?.saveAndValidate() ?? false) { if (_formKey.currentState?.saveAndValidate() ?? false) {
if (provider.recordFormItem.imageUrl.isEmpty) { if (provider.recordFormItem.imageUrl.isEmpty) {
showErrorToast('请上传图片'); ToastUtil.error('请上传图片');
return; return;
} }
print(provider.recordFormItem.name); await provider.handleRecord();
print(provider.recordFormItem.date); await provider.onRecordTabChange();
print(provider.recordFormItem.imageUrl); Navigator.pop(context);
} }
} }
@@ -300,7 +305,7 @@ class _RecordFormPageState extends State<RecordFormPage> {
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding( child: Padding(
padding: EdgeInsets.all(10), padding: EdgeInsets.all(10),
child: BuildCard(child: _buildFormBuilder()), child: CommonCard(child: _buildFormBuilder()),
), ),
), ),
); );

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; 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/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:food_hub_app/widgets/stats/card.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -21,7 +21,7 @@ class _StatsPage extends State<StatsPage> {
// 初始化加载数据 // 初始化加载数据
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<FoodProvider>().refreshBlogStats(); context.read<FoodProvider>().refreshFoodStats();
}); });
} }
@@ -67,56 +67,6 @@ class _StatsPage extends State<StatsPage> {
); );
} }
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) { Widget _buildContent(FoodProvider provider) {
return Column( return Column(
children: [ children: [
@@ -124,17 +74,33 @@ class _StatsPage extends State<StatsPage> {
SizedBox(height: 8), SizedBox(height: 8),
SizedBox( SizedBox(
height: chartHeight, height: chartHeight,
child: BuildCard(child: _buildRecordStats(provider)), child: CommonCard(
child: LineChart(
title: '记录统计',
xAxisName: '日期',
yAxisName: '次数',
unit: '',
data: provider.recordStats,
),
),
), ),
SizedBox(height: 8), SizedBox(height: 8),
SizedBox( SizedBox(
height: chartHeight, height: chartHeight,
child: BuildCard(child: _buildCategoryStats(provider)), child: CommonCard(
child: PieChart(
title: '菜谱统计',
unit: '',
data: provider.categoryStats,
),
),
), ),
SizedBox(height: 8), SizedBox(height: 8),
SizedBox( SizedBox(
height: chartHeight, height: chartHeight,
child: BuildCard(child: _buildRankStats(provider)), child: CommonCard(
child: RankChart(title: '排行榜', data: provider.rankStats, unit: ''),
),
), ),
], ],
); );
@@ -147,7 +113,7 @@ class _StatsPage extends State<StatsPage> {
return Stack( return Stack(
children: [ children: [
if (provider.isLoading) if (provider.isLoading)
buildLoadingIndicator(context) buildLoadingIndicator()
else else
SingleChildScrollView(child: _buildContent(provider)), SingleChildScrollView(child: _buildContent(provider)),
], ],

View File

@@ -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<ChartData> 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<ChartData, String>(
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<ChartData> 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<ChartData, String>(
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<ChartData> 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<ChartData, String>(
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<ChartData> 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)),
),
],
),
);
},
);
}

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:food_hub_app/config/app_config.dart'; import 'package:food_hub_app/config/app_config.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
@@ -91,138 +89,123 @@ class _ImagePreviewPageState extends State<ImagePreviewPage> {
} }
} }
Widget buildNetworkImage( class CommonImage extends StatelessWidget {
BuildContext context, final List<String> imageUrls;
List<String> imageUrls, final int index;
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(),
),
),
),
);
}
Widget buildImagePreviewItem({ const CommonImage({super.key, required this.imageUrls, required this.index});
required BuildContext context,
required List<String> imageUrls, Widget _buildImageLoadingIndicator(ImageChunkEvent? progress) {
required int index, final value =
required VoidCallback onRemoveImage, progress?.expectedTotalBytes != null
}) { ? progress!.cumulativeBytesLoaded / progress.expectedTotalBytes!
return Container( : null;
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Stack(
children: [
buildNetworkImage(context, imageUrls, index),
buildDeleteImage(onRemoveImage: onRemoveImage),
],
),
),
);
}
Widget buildImageLoadingIndicator(ImageChunkEvent? loadingProgress) {
return Center( return Center(
child: SizedBox( child: SizedBox(
width: 30, width: 30,
height: 30, height: 30,
child: CircularProgressIndicator( child: CircularProgressIndicator(value: value),
value:
loadingProgress?.expectedTotalBytes != null
? loadingProgress!.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
), ),
); );
} }
Widget buildErrorImage() { Widget _buildErrorImage() {
return Container( return Container(
color: Colors.grey[200], color: Colors.grey[200],
child: const Icon(Icons.image, color: Colors.grey, size: 30), child: const Icon(Icons.image, color: Colors.grey, size: 30),
); );
} }
Widget _buildPhotoView(ImageProvider imageProvider) { void _navigateToImagePreview(
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, BuildContext context,
List<String> imageUrls, List<String> imageUrls,
int index, int index,
) { ) {
Navigator.push( final route = MaterialPageRoute(
context,
MaterialPageRoute(
builder: builder:
(context) => ImagePreviewPage(images: imageUrls, initialIndex: index), (context) => ImagePreviewPage(images: imageUrls, initialIndex: index),
),
); );
// Navigator.of(context).push(
// PageRouteBuilder( Navigator.push(context, route);
// opaque: false,
// pageBuilder: (
// BuildContext context,
// Animation<double> animation,
// Animation<double> secondaryAnimation,
// ) {
// return Scaffold(
// backgroundColor: Colors.black.withAlpha(200),
// body: Stack(
// children: [
// // 可缩放图片
// Positioned.fill(child: _buildPhotoView(imageProvider)),
// // 关闭按钮
// _buildCloseImage(context),
// ],
// ),
// );
// },
// ),
// );
} }
Widget buildImageUploadButton({required VoidCallback onPickImage}) { @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(),
),
),
),
);
}
}
class ImagePreview extends StatelessWidget {
final List<String> 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( return InkWell(
onTap: onPickImage, onTap: onTap,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Container( child: Container(
width: 96, 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),
),
),
);
}

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:toggle_switch/toggle_switch.dart'; import 'package:toggle_switch/toggle_switch.dart';
Widget formLabelText({required String labelText, bool isRequired = false}) { 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<T extends Enum>({ Widget buildToggleSwitch<T extends Enum>({
required BuildContext context, required BuildContext context,
required T currentTab, required T currentTab,
@@ -153,63 +125,3 @@ Widget errorImageContainer(double height) {
child: const Icon(Icons.image, color: Colors.grey, size: 30), 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<Color>(colors.primary),
),
SizedBox(height: 12),
Text(
'加载中',
style: TextStyle(
fontSize: 14,
color: colors.onSurface,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}

View File

@@ -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<YearSelector> createState() => _YearSelectorState();
}
// SingleTickerProviderStateMixin 动画控制器
class _YearSelectorState extends State<YearSelector>
with SingleTickerProviderStateMixin {
late int _currentYear;
// 用于动画效果
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_currentYear = widget.initialYear;
// 初始化动画控制器
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
// 缩放动画
_scaleAnimation = Tween<double>(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(),
)
],
);
}
}

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; 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/config/app_config.dart';
import 'package:food_hub_app/models/moment.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/image.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart';
class MomentCard extends StatelessWidget { class MomentCard extends StatelessWidget {
@@ -12,7 +12,7 @@ class MomentCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BuildCard( return CommonCard(
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -113,7 +113,7 @@ class MomentCard extends StatelessWidget {
mainAxisSpacing: 4, mainAxisSpacing: 4,
childAspectRatio: itemAspectRatio, childAspectRatio: itemAspectRatio,
children: List.generate(imageCount, (index) { children: List.generate(imageCount, (index) {
return buildNetworkImage(context, imageUrls, index); return CommonImage(imageUrls: imageUrls, index: index);
}), }),
); );
} }

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; 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/models/recipe.dart';
import 'package:food_hub_app/provider/food_provider.dart'; import 'package:food_hub_app/provider/food_provider.dart';
import 'package:food_hub_app/utils/index.dart'; import 'package:food_hub_app/utils/index.dart';
@@ -37,7 +38,7 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
}, },
onPageChanged: (focusedDay) { onPageChanged: (focusedDay) {
provider.focusedDay = focusedDay; provider.focusedDay = focusedDay;
provider.refreshRecordList(null, null); provider.refreshRecordList();
}, },
// 自定义日期单元格构建器 // 自定义日期单元格构建器
calendarBuilders: CalendarBuilders( calendarBuilders: CalendarBuilders(
@@ -191,9 +192,9 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
Widget _buildContent(FoodProvider provider) { Widget _buildContent(FoodProvider provider) {
return Column( return Column(
children: [ children: [
BuildCard(child: _recipeCalendar(provider)), CommonCard(child: _recipeCalendar(provider)),
SizedBox(height: 8), 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<RecipeCalendar> {
return Stack( return Stack(
children: [ children: [
if (provider.isLoading) if (provider.isLoading)
buildLoadingIndicator(context) buildLoadingIndicator()
else else
_buildContent(provider), _buildContent(provider),
], ],

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; 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/models/recipe.dart';
import 'package:food_hub_app/widgets/common/image.dart'; import 'package:food_hub_app/widgets/common/image.dart';
import 'package:food_hub_app/widgets/common/index.dart';
class RecipeCard extends StatelessWidget { class RecipeCard extends StatelessWidget {
final RecipeSummary recipe; final RecipeSummary recipe;
@@ -17,12 +17,12 @@ class RecipeCard extends StatelessWidget {
final List<String> imageUrls = final List<String> imageUrls =
recipe.recordList.reversed.map((record) => record.imageUrl).toList(); recipe.recordList.reversed.map((record) => record.imageUrl).toList();
return BuildCard( return CommonCard(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
buildNetworkImage(context, imageUrls, 0), CommonImage(imageUrls: imageUrls, index: 0),
SizedBox(height: 8), SizedBox(height: 8),
GestureDetector( GestureDetector(
onTap: () => navigatorToRecipeDetail(context), onTap: () => navigatorToRecipeDetail(context),

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.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/provider/food_provider.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:food_hub_app/widgets/recipe/card.dart'; import 'package:food_hub_app/widgets/recipe/card.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -107,7 +107,7 @@ class _RecipeListState extends State<RecipeList> {
return Stack( return Stack(
children: [ children: [
if (provider.isLoading) if (provider.isLoading)
buildLoadingIndicator(context) buildLoadingIndicator()
else else
_buildContent(provider), _buildContent(provider),
], ],

View File

@@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; 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/models/recipe.dart';
import 'package:food_hub_app/provider/food_provider.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/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:timelines_plus/timelines_plus.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -35,7 +35,7 @@ class _RecipeTimeline extends State<RecipeTimeline> {
SizedBox(height: 6), SizedBox(height: 6),
Padding( Padding(
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 0), padding: EdgeInsets.symmetric(vertical: 8, horizontal: 0),
child: BuildCard( child: CommonCard(
child: Column( child: Column(
children: [ children: [
Text( Text(
@@ -43,7 +43,7 @@ class _RecipeTimeline extends State<RecipeTimeline> {
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
buildNetworkImage(context, [record.imageUrl], 0), CommonImage(imageUrls: [record.imageUrl], index: 0),
], ],
), ),
), ),
@@ -87,12 +87,11 @@ class _RecipeTimeline extends State<RecipeTimeline> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
YearSelector( YearSelector(
initialYear: DateTime.now().year, currentYear: provider.selectYear,
minYear: 2000, onYearChanged: (year) {
maxYear: 2100, provider.selectYear = year;
onYearChanged: provider.refreshRecordList();
(year) => },
provider.refreshRecordList("$year-01-01", "$year-12-31"),
), ),
Expanded( Expanded(
child: Padding( child: Padding(
@@ -111,7 +110,7 @@ class _RecipeTimeline extends State<RecipeTimeline> {
return Stack( return Stack(
children: [ children: [
if (provider.isLoading) if (provider.isLoading)
buildLoadingIndicator(context) buildLoadingIndicator()
else else
_buildContent(provider), _buildContent(provider),
], ],

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:food_hub_app/models/stats.dart'; import 'package:flutter_common/widget/common_widget.dart';
import 'package:food_hub_app/widgets/common/index.dart';
class StatisticCard extends StatelessWidget { class StatisticCard extends StatelessWidget {
final IconData icon; final IconData icon;
@@ -20,7 +19,7 @@ class StatisticCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BuildCard( return CommonCard(
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [

View File

@@ -170,7 +170,7 @@ packages:
source: hosted source: hosted
version: "0.3.4+2" version: "0.3.4+2"
crypto: crypto:
dependency: "direct main" dependency: transitive
description: description:
name: crypto name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
@@ -202,7 +202,7 @@ packages:
source: hosted source: hosted
version: "0.7.11" version: "0.7.11"
dio: dio:
dependency: "direct main" dependency: transitive
description: description:
name: dio name: dio
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
@@ -310,6 +310,13 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.1.0" 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: flutter_form_builder:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -366,7 +373,7 @@ packages:
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
fluttertoast: fluttertoast:
dependency: "direct main" dependency: transitive
description: description:
name: fluttertoast name: fluttertoast
sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1" sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1"
@@ -614,7 +621,7 @@ packages:
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
minio: minio:
dependency: "direct main" dependency: transitive
description: description:
name: minio name: minio
sha256: ee2ce47766e46c7d164f960f2f5ed6a9a82844d877f6b82574f6876ec50c56d1 sha256: ee2ce47766e46c7d164f960f2f5ed6a9a82844d877f6b82574f6876ec50c56d1
@@ -790,7 +797,7 @@ packages:
source: hosted source: hosted
version: "6.1.0" version: "6.1.0"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: transitive
description: description:
name: shared_preferences name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
@@ -931,7 +938,7 @@ packages:
source: hosted source: hosted
version: "1.4.1" version: "1.4.1"
syncfusion_flutter_charts: syncfusion_flutter_charts:
dependency: "direct main" dependency: transitive
description: description:
name: syncfusion_flutter_charts name: syncfusion_flutter_charts
sha256: c58ca79e072680af6f0554f7c4b91886c1d8808f2522b49d557f16fb5cb3bb04 sha256: c58ca79e072680af6f0554f7c4b91886c1d8808f2522b49d557f16fb5cb3bb04

View File

@@ -35,7 +35,6 @@ dependencies:
# The following adds the Cupertino Icons fonts to your application. # The following adds the Cupertino Icons fonts to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
dio: ^5.7.0
provider: ^6.1.1 provider: ^6.1.1
timelines_plus: ^1.0.7 timelines_plus: ^1.0.7
table_calendar: ^3.1.3 table_calendar: ^3.1.3
@@ -45,18 +44,15 @@ dependencies:
intl: ^0.19.0 intl: ^0.19.0
tdesign_flutter: ^0.2.3 tdesign_flutter: ^0.2.3
json_annotation: ^4.9.0 json_annotation: ^4.9.0
fluttertoast: ^8.2.0
shared_preferences: ^2.3.0
logger: ^2.6.0 logger: ^2.6.0
photo_view: ^0.15.0 photo_view: ^0.15.0
flutter_carousel_widget: ^3.1.0 flutter_carousel_widget: ^3.1.0
easy_refresh: ^3.4.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 file_picker: ^10.3.3
share_plus: ^11.0.0 share_plus: ^11.0.0
path_provider: ^2.1.5 path_provider: ^2.1.5
flutter_common:
path: ..\flutter_common
dependency_overrides: dependency_overrides:
tdesign_flutter_adaptation: 3.16.0 tdesign_flutter_adaptation: 3.16.0