feat:更新美食记录表单
This commit is contained in:
@@ -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<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) {
|
||||
return HttpUtil().put<bool>("/moment/$id", data: moment);
|
||||
return httpUtil.put<bool>("/moment/$id", data: moment);
|
||||
}
|
||||
|
||||
Future<bool> deleteMomentApi(int id) {
|
||||
return HttpUtil().delete<bool>("/moment/$id");
|
||||
return httpUtil.delete<bool>("/moment/$id");
|
||||
}
|
||||
|
||||
Future<List<Moment>> queryMomentListApi() {
|
||||
return HttpUtil().get<List<Moment>>(
|
||||
return httpUtil.get<List<Moment>>(
|
||||
"/moment",
|
||||
converter: (data) => convertListResponse<Moment>(data, Moment.fromJson),
|
||||
converter: (data) => convertList<Moment>(data, Moment.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
Future<PageMoment> queryMomentByPageApi(int currentPage, int pageSize) {
|
||||
return HttpUtil().get<PageMoment>(
|
||||
return httpUtil.get<PageMoment>(
|
||||
"/moment/page",
|
||||
queryParameters: {"currentPage": currentPage, "pageSize": pageSize},
|
||||
converter: (data) => PageMoment.fromJson(data),
|
||||
@@ -30,13 +33,13 @@ Future<PageMoment> queryMomentByPageApi(int currentPage, int pageSize) {
|
||||
}
|
||||
|
||||
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) {
|
||||
return HttpUtil().post<bool>("/food/moment/$id/like");
|
||||
return httpUtil.post<bool>("/food/moment/$id/like");
|
||||
}
|
||||
|
||||
Future<bool> deleteMomentLikeApi(int id) {
|
||||
return HttpUtil().delete<bool>("/food/moment/$id/like");
|
||||
return httpUtil.delete<bool>("/food/moment/$id/like");
|
||||
}
|
||||
|
||||
@@ -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<RecipeDetail> queryRecipeByIdApi(int id) {
|
||||
return HttpUtil().get<RecipeDetail>(
|
||||
return httpUtil.get<RecipeDetail>(
|
||||
"/food/recipe/$id",
|
||||
converter: (data) => RecipeDetail.fromJson(data),
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
return HttpUtil().put<bool>("/food/recipe/$id", data: recipe);
|
||||
return httpUtil.put<bool>("/food/recipe/$id", data: recipe);
|
||||
}
|
||||
|
||||
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) {
|
||||
return HttpUtil().get<List<Recipe>>(
|
||||
return httpUtil.get<List<Recipe>>(
|
||||
"/food/recipe/user/$id",
|
||||
converter: (data) => convertListResponse<Recipe>(data, Recipe.fromJson),
|
||||
converter: (data) => convertList<Recipe>(data, Recipe.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Recipe>> queryRecipeUserFavouriteApi() {
|
||||
return HttpUtil().get<List<Recipe>>(
|
||||
return httpUtil.get<List<Recipe>>(
|
||||
"/food/recipe/user/favourite",
|
||||
converter: (data) => convertListResponse<Recipe>(data, Recipe.fromJson),
|
||||
converter: (data) => convertList<Recipe>(data, Recipe.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<RecipeSummary>> queryRecipeApi(RecipeQuery recipeQuery) {
|
||||
return HttpUtil().get<List<RecipeSummary>>(
|
||||
return httpUtil.get<List<RecipeSummary>>(
|
||||
"/food/recipe",
|
||||
queryParameters: recipeQuery.toJson(),
|
||||
converter:
|
||||
(data) =>
|
||||
convertListResponse<RecipeSummary>(data, RecipeSummary.fromJson),
|
||||
(data) => convertList<RecipeSummary>(data, RecipeSummary.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<String>> queryFoodNameListApi() {
|
||||
return HttpUtil().get<List<String>>(
|
||||
return httpUtil.get<List<String>>(
|
||||
"/food/recipe/name",
|
||||
converter: (data) => convertListStringResponse(data),
|
||||
converter: (data) => convertStringList(data),
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
return HttpUtil().put<bool>("/food/record/$id", data: record);
|
||||
return httpUtil.put<bool>("/food/record/$id", data: record);
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
return HttpUtil().post<bool>("/food/recipe/$id/like");
|
||||
return httpUtil.post<bool>("/food/recipe/$id/like");
|
||||
}
|
||||
|
||||
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) {
|
||||
return HttpUtil().post<bool>("/food/recipe/$id/favourite");
|
||||
return httpUtil.post<bool>("/food/recipe/$id/favourite");
|
||||
}
|
||||
|
||||
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) {
|
||||
return HttpUtil().get<List<FoodRecord>>(
|
||||
return httpUtil.get<List<FoodRecord>>(
|
||||
"/food/record",
|
||||
queryParameters: {"startDate": startDate, "endDate": endDate},
|
||||
converter:
|
||||
(data) => convertListResponse<FoodRecord>(data, FoodRecord.fromJson),
|
||||
converter: (data) => convertList<FoodRecord>(data, FoodRecord.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<String>> queryCategoryApi() {
|
||||
return HttpUtil().get<List<String>>(
|
||||
return httpUtil.get<List<String>>(
|
||||
"/food/category",
|
||||
converter: (data) => convertListStringResponse(data),
|
||||
);
|
||||
|
||||
@@ -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<Session> loginApi(String username, String password) {
|
||||
return HttpUtil().post<Session>(
|
||||
return httpUtil.post<Session>(
|
||||
"/session",
|
||||
queryParameters: {"username": username, "password": password},
|
||||
converter: (data) => Session.fromJson(data),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SummaryStats> queryStatsApi() {
|
||||
return HttpUtil().get<SummaryStats>(
|
||||
return httpUtil.get<SummaryStats>(
|
||||
"/food/stats",
|
||||
converter: (data) => SummaryStats.fromJson(data),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ChartData>> queryRecordStatsApi() {
|
||||
return HttpUtil().get<List<ChartData>>(
|
||||
return httpUtil.get<List<ChartData>>(
|
||||
"/food/stats/record",
|
||||
converter: (data) => convertListResponse<ChartData>(data, ChartData.fromJson),
|
||||
converter: (data) => convertList<ChartData>(data, ChartData.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ChartData>> queryCategoryStatsApi() {
|
||||
return HttpUtil().get<List<ChartData>>(
|
||||
return httpUtil.get<List<ChartData>>(
|
||||
"/food/stats/category",
|
||||
converter: (data) => convertListResponse<ChartData>(data, ChartData.fromJson),
|
||||
converter: (data) => convertList<ChartData>(data, ChartData.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ChartData>> queryRankStatsApi() {
|
||||
return HttpUtil().get<List<ChartData>>(
|
||||
return httpUtil.get<List<ChartData>>(
|
||||
"/food/stats/rank",
|
||||
converter: (data) => convertListResponse<ChartData>(data, ChartData.fromJson),
|
||||
converter: (data) => convertList<ChartData>(data, ChartData.fromJson),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/';
|
||||
}
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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: [
|
||||
|
||||
@@ -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<String> imageList;
|
||||
final String? date;
|
||||
final List<int>? likeList;
|
||||
final List<Comment>? commentList;
|
||||
int? id;
|
||||
int? userId;
|
||||
String? username;
|
||||
String? avatar;
|
||||
String content;
|
||||
List<String> imageList;
|
||||
String? date;
|
||||
List<int>? likeList;
|
||||
List<Comment>? commentList;
|
||||
|
||||
Moment({
|
||||
this.id,
|
||||
|
||||
@@ -83,9 +83,9 @@ class FoodRecord {
|
||||
});
|
||||
|
||||
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() {
|
||||
return FoodRecord(
|
||||
|
||||
@@ -51,7 +51,7 @@ Map<String, dynamic> _$RecipeCommentToJson(RecipeComment instance) =>
|
||||
'date': instance.date,
|
||||
};
|
||||
|
||||
FoodRecord _$RecordFromJson(Map<String, dynamic> json) => FoodRecord(
|
||||
FoodRecord _$FoodRecordFromJson(Map<String, dynamic> 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<String, dynamic> json) => FoodRecord(
|
||||
imageUrl: json['imageUrl'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RecordToJson(FoodRecord instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'category': instance.category,
|
||||
'person': instance.person,
|
||||
'date': instance.date,
|
||||
'imageUrl': instance.imageUrl,
|
||||
};
|
||||
Map<String, dynamic> _$FoodRecordToJson(FoodRecord instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'category': instance.category,
|
||||
'person': instance.person,
|
||||
'date': instance.date,
|
||||
'imageUrl': instance.imageUrl,
|
||||
};
|
||||
|
||||
RecipeQuery _$RecipeQueryFromJson(Map<String, dynamic> json) =>
|
||||
RecipeQuery(category: json['category'] as String);
|
||||
|
||||
@@ -19,16 +19,3 @@ class SummaryStats {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -18,13 +18,3 @@ Map<String, dynamic> _$SummaryStatsToJson(SummaryStats instance) =>
|
||||
'categoryCount': instance.categoryCount,
|
||||
'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,
|
||||
};
|
||||
|
||||
@@ -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<String> categoryList = [];
|
||||
late List<RecipeSummary> recipeSummaryList = [];
|
||||
|
||||
int selectYear = DateTime.now().year;
|
||||
DateTime selectedDay = DateTime.now();
|
||||
DateTime focusedDay = DateTime.now();
|
||||
|
||||
@@ -61,6 +66,18 @@ class FoodProvider with ChangeNotifier {
|
||||
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 {
|
||||
try {
|
||||
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 (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<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() {
|
||||
selectRecordList =
|
||||
recordList
|
||||
@@ -136,7 +192,7 @@ class FoodProvider with ChangeNotifier {
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<void> refreshBlogStats() async {
|
||||
Future<void> refreshFoodStats() async {
|
||||
if (isLoading) return;
|
||||
|
||||
try {
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,18 +16,7 @@ String formatDateTime(DateTime dateTime, [String format = 'yyyy-MM-dd']) {
|
||||
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) {
|
||||
if (data is List) {
|
||||
|
||||
@@ -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')),
|
||||
);
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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],
|
||||
);
|
||||
}
|
||||
@@ -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<LoginPage> {
|
||||
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);
|
||||
|
||||
@@ -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<MomentPage> {
|
||||
body: Stack(
|
||||
children: [
|
||||
if (provider.isLoading)
|
||||
buildLoadingIndicator(context)
|
||||
buildLoadingIndicator()
|
||||
else
|
||||
_buildMomentList(provider),
|
||||
],
|
||||
|
||||
@@ -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<MomentFormPage> {
|
||||
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<MomentFormPage> {
|
||||
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<MomentFormPage> {
|
||||
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<MomentFormPage> {
|
||||
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<MomentFormPage> {
|
||||
final remainingCount = maxImageCount - currentCount;
|
||||
|
||||
if (remainingCount <= 0) {
|
||||
showErrorToast('最多只能上传$maxImageCount张图片');
|
||||
ToastUtil.error('最多只能上传$maxImageCount张图片');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -147,7 +151,11 @@ class _MomentFormPageState extends State<MomentFormPage> {
|
||||
|
||||
// 逐个上传图片
|
||||
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<MomentFormPage> {
|
||||
provider.momentFormItem.imageList.addAll(newImageUrls);
|
||||
});
|
||||
|
||||
showSuccessToast('图片上传成功');
|
||||
ToastUtil.success('图片上传成功');
|
||||
} catch (e) {
|
||||
showErrorToast('图片上传失败');
|
||||
ToastUtil.error('图片上传失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,7 +207,7 @@ class _MomentFormPageState extends State<MomentFormPage> {
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: BuildCard(child: _buildFormBuilder()),
|
||||
child: CommonCard(child: _buildFormBuilder()),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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<RecipeDetailPage> {
|
||||
required String title,
|
||||
required Widget content,
|
||||
}) {
|
||||
return BuildCard(
|
||||
return CommonCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
@@ -24,7 +24,6 @@ class RecordPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RecordPageState extends State<RecordPage> {
|
||||
RecordTab _currentTab = RecordTab.recipe;
|
||||
final List<Widget> _tabPages = [
|
||||
RecipeList(),
|
||||
RecipeCalendar(),
|
||||
@@ -49,22 +48,8 @@ class _RecordPageState extends State<RecordPage> {
|
||||
}
|
||||
|
||||
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<RecordPage> {
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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<RecordFormPage> {
|
||||
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<RecordFormPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecipeAutocomplete(FoodProvider provider) {
|
||||
Widget _buildRecipeField(FoodProvider provider) {
|
||||
return Autocomplete<String>(
|
||||
initialValue: TextEditingValue(text: provider.recordFormItem.name),
|
||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||
@@ -118,9 +87,7 @@ class _RecordFormPageState extends State<RecordFormPage> {
|
||||
},
|
||||
|
||||
onSelected: (String value) {
|
||||
setState(() {
|
||||
provider.recordFormItem.name = value;
|
||||
});
|
||||
provider.recordFormItem.name = value;
|
||||
},
|
||||
|
||||
optionsViewBuilder: (
|
||||
@@ -149,7 +116,7 @@ class _RecordFormPageState extends State<RecordFormPage> {
|
||||
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<RecordFormPage> {
|
||||
|
||||
void onClearRecipeName() {
|
||||
controller.clear();
|
||||
setState(() {
|
||||
provider.recordFormItem.name = '';
|
||||
});
|
||||
provider.recordFormItem.name = '';
|
||||
}
|
||||
|
||||
Widget? buildSuffixIcon() {
|
||||
@@ -201,9 +166,7 @@ class _RecordFormPageState extends State<RecordFormPage> {
|
||||
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<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 {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
@@ -237,12 +224,26 @@ class _RecordFormPageState extends State<RecordFormPage> {
|
||||
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<void> _pickImage(FoodProvider provider) async {
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||
@@ -253,7 +254,11 @@ class _RecordFormPageState extends State<RecordFormPage> {
|
||||
|
||||
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<RecordFormPage> {
|
||||
}
|
||||
|
||||
/// 提交表单
|
||||
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<RecordFormPage> {
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: BuildCard(child: _buildFormBuilder()),
|
||||
child: CommonCard(child: _buildFormBuilder()),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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<StatsPage> {
|
||||
|
||||
// 初始化加载数据
|
||||
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) {
|
||||
return Column(
|
||||
children: [
|
||||
@@ -124,17 +74,33 @@ class _StatsPage extends State<StatsPage> {
|
||||
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<StatsPage> {
|
||||
return Stack(
|
||||
children: [
|
||||
if (provider.isLoading)
|
||||
buildLoadingIndicator(context)
|
||||
buildLoadingIndicator()
|
||||
else
|
||||
SingleChildScrollView(child: _buildContent(provider)),
|
||||
],
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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<ImagePreviewPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildNetworkImage(
|
||||
BuildContext context,
|
||||
List<String> 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<String> 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<String> 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<String> imageUrls,
|
||||
int index,
|
||||
) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
void _navigateToImagePreview(
|
||||
BuildContext context,
|
||||
List<String> imageUrls,
|
||||
int index,
|
||||
) {
|
||||
final route = MaterialPageRoute(
|
||||
builder:
|
||||
(context) => ImagePreviewPage(images: imageUrls, initialIndex: index),
|
||||
),
|
||||
);
|
||||
// Navigator.of(context).push(
|
||||
// PageRouteBuilder(
|
||||
// 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),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
);
|
||||
|
||||
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<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(
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<T extends Enum>({
|
||||
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<Color>(colors.primary),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
'加载中',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: colors.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<RecipeCalendar> {
|
||||
},
|
||||
onPageChanged: (focusedDay) {
|
||||
provider.focusedDay = focusedDay;
|
||||
provider.refreshRecordList(null, null);
|
||||
provider.refreshRecordList();
|
||||
},
|
||||
// 自定义日期单元格构建器
|
||||
calendarBuilders: CalendarBuilders(
|
||||
@@ -191,9 +192,9 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
|
||||
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<RecipeCalendar> {
|
||||
return Stack(
|
||||
children: [
|
||||
if (provider.isLoading)
|
||||
buildLoadingIndicator(context)
|
||||
buildLoadingIndicator()
|
||||
else
|
||||
_buildContent(provider),
|
||||
],
|
||||
|
||||
@@ -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<String> 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),
|
||||
|
||||
@@ -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<RecipeList> {
|
||||
return Stack(
|
||||
children: [
|
||||
if (provider.isLoading)
|
||||
buildLoadingIndicator(context)
|
||||
buildLoadingIndicator()
|
||||
else
|
||||
_buildContent(provider),
|
||||
],
|
||||
|
||||
@@ -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<RecipeTimeline> {
|
||||
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<RecipeTimeline> {
|
||||
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<RecipeTimeline> {
|
||||
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<RecipeTimeline> {
|
||||
return Stack(
|
||||
children: [
|
||||
if (provider.isLoading)
|
||||
buildLoadingIndicator(context)
|
||||
buildLoadingIndicator()
|
||||
else
|
||||
_buildContent(provider),
|
||||
],
|
||||
|
||||
@@ -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: [
|
||||
|
||||
19
pubspec.lock
19
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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user