Compare commits
10 Commits
65958d925e
...
6bba9a488c
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bba9a488c | |||
| 504ee073ce | |||
| b5dd0ab749 | |||
| 8923d9e63a | |||
| 34780651a8 | |||
| a2ed0f3d98 | |||
| 502bb9df52 | |||
| 7da3ec9507 | |||
| d9f1781277 | |||
| 5d156d3a02 |
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 4.0 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
android/app/src/main/res/mipmap-ldpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 2.1 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 6.2 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 13 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 19 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
@@ -1,108 +0,0 @@
|
|||||||
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';
|
|
||||||
|
|
||||||
Future<Recipe> queryRecipeByIdApi(int id) {
|
|
||||||
return HttpUtil().get<Recipe>(
|
|
||||||
"/food-service/food/recipe/$id",
|
|
||||||
converter: (data) => Recipe.fromJson(data),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> addRecipeApi(Recipe recipe) {
|
|
||||||
return HttpUtil().post<bool>("/food-service/food/food/recipe", data: recipe);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> updateRecipeApi(int id, Recipe recipe) {
|
|
||||||
return HttpUtil().put<bool>(
|
|
||||||
"/food-service/food/food/recipe/$id",
|
|
||||||
data: recipe,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> deleteRecipeApi(int id) {
|
|
||||||
return HttpUtil().delete<bool>("/food-service/food/food/recipe/$id");
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<Recipe>> queryRecipeByUserApi(int id) {
|
|
||||||
return HttpUtil().get<List<Recipe>>(
|
|
||||||
"/food-service/food/recipe/user/$id",
|
|
||||||
converter: (data) => convertListResponse<Recipe>(data, Recipe.fromJson),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<Recipe>> queryRecipeUserFavouriteApi() {
|
|
||||||
return HttpUtil().get<List<Recipe>>(
|
|
||||||
"/food-service/food/recipe/user/favourite",
|
|
||||||
converter: (data) => convertListResponse<Recipe>(data, Recipe.fromJson),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<Recipe>> queryRecipeApi(RecipeQuery recipeQuery) {
|
|
||||||
return HttpUtil().get<List<Recipe>>(
|
|
||||||
"/food-service/food/recipe",
|
|
||||||
queryParameters: recipeQuery.toJson(),
|
|
||||||
converter: (data) => convertListResponse<Recipe>(data, Recipe.fromJson),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<String>> queryFoodNameListApi() {
|
|
||||||
return HttpUtil().get<List<String>>(
|
|
||||||
"/food-service/food/recipe/name",
|
|
||||||
converter:
|
|
||||||
(data) => convertListResponse<String>(data, (json) => json.toString()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> addRecordApi(Record record) {
|
|
||||||
return HttpUtil().post<bool>("/food-service/food/record", data: record);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> updateRecordApi(int id, Record record) {
|
|
||||||
return HttpUtil().put<bool>("/food-service/food/record/$id", data: record);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> deleteRecordApi(int id) {
|
|
||||||
return HttpUtil().delete<bool>("/food-service/food/record/$id");
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> addRecipeCommentApi(int id, String content) {
|
|
||||||
return HttpUtil().post<bool>(
|
|
||||||
"/food-service/food/recipe/$id/comment",
|
|
||||||
data: content,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> addRecipeLikeApi(int id) {
|
|
||||||
return HttpUtil().post<bool>("/food-service/food/recipe/$id/like");
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> deleteRecipeLikeApi(int id) {
|
|
||||||
return HttpUtil().delete<bool>("/food-service/food/recipe/$id/like");
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> addRecipeFavouriteApi(int id) {
|
|
||||||
return HttpUtil().post<bool>("/food-service/food/recipe/$id/favourite");
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> deleteRecipeFavouriteApi(int id) {
|
|
||||||
return HttpUtil().delete<bool>("/food-service/food/recipe/$id/like");
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<Record>> queryRecordApi(String startDate, String endDate) {
|
|
||||||
return HttpUtil().get<List<Record>>(
|
|
||||||
"/food-service/food/record",
|
|
||||||
queryParameters: {
|
|
||||||
"startDate": startDate,
|
|
||||||
"endDate": endDate
|
|
||||||
},
|
|
||||||
converter: (data) => convertListResponse<Record>(data, Record.fromJson),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<String>> queryCategoryApi() {
|
|
||||||
return HttpUtil().get<List<String>>(
|
|
||||||
"/food-service/food/category",
|
|
||||||
converter: (data) => convertListResponse<String>(data, (json) => json.toString()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
42
lib/apis/moment.dart
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
Future<bool> addMomentApi(Moment moment) {
|
||||||
|
return HttpUtil().post<bool>("/moment", data: moment);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> updateMomentApi(int id, Moment moment) {
|
||||||
|
return HttpUtil().put<bool>("/moment/$id", data: moment);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> deleteMomentApi(int id) {
|
||||||
|
return HttpUtil().delete<bool>("/moment/$id");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Moment>> queryMomentListApi() {
|
||||||
|
return HttpUtil().get<List<Moment>>(
|
||||||
|
"/moment",
|
||||||
|
converter: (data) => convertListResponse<Moment>(data, Moment.fromJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<PageMoment> queryMomentByPageApi(int currentPage, int pageSize) {
|
||||||
|
return HttpUtil().get<PageMoment>(
|
||||||
|
"/moment/page",
|
||||||
|
queryParameters: {"currentPage": currentPage, "pageSize": pageSize},
|
||||||
|
converter: (data) => PageMoment.fromJson(data),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> addMomentCommentApi(int id, String content) {
|
||||||
|
return HttpUtil().post<bool>("/moment/$id/comment", data: content);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> addMomentLikeApi(int id) {
|
||||||
|
return HttpUtil().post<bool>("/food/moment/$id/like");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> deleteMomentLikeApi(int id) {
|
||||||
|
return HttpUtil().delete<bool>("/food/moment/$id/like");
|
||||||
|
}
|
||||||
100
lib/apis/recipe.dart
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
Future<RecipeDetail> queryRecipeByIdApi(int id) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> updateRecipeApi(int id, Recipe recipe) {
|
||||||
|
return HttpUtil().put<bool>("/food/recipe/$id", data: recipe);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> deleteRecipeApi(int id) {
|
||||||
|
return HttpUtil().delete<bool>("/food/recipe/$id");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Recipe>> queryRecipeByUserApi(int id) {
|
||||||
|
return HttpUtil().get<List<Recipe>>(
|
||||||
|
"/food/recipe/user/$id",
|
||||||
|
converter: (data) => convertListResponse<Recipe>(data, Recipe.fromJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Recipe>> queryRecipeUserFavouriteApi() {
|
||||||
|
return HttpUtil().get<List<Recipe>>(
|
||||||
|
"/food/recipe/user/favourite",
|
||||||
|
converter: (data) => convertListResponse<Recipe>(data, Recipe.fromJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<RecipeSummary>> queryRecipeApi(RecipeQuery recipeQuery) {
|
||||||
|
return HttpUtil().get<List<RecipeSummary>>(
|
||||||
|
"/food/recipe",
|
||||||
|
queryParameters: recipeQuery.toJson(),
|
||||||
|
converter: (data) => convertListResponse<RecipeSummary>(data, RecipeSummary.fromJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> queryFoodNameListApi() {
|
||||||
|
return HttpUtil().get<List<String>>(
|
||||||
|
"/food/recipe/name",
|
||||||
|
converter:
|
||||||
|
(data) => convertListResponse<String>(data, (json) => json.toString()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> addRecordApi(Record record) {
|
||||||
|
return HttpUtil().post<bool>("/food/record", data: record);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> updateRecordApi(int id, Record record) {
|
||||||
|
return HttpUtil().put<bool>("/food/record/$id", data: record);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> deleteRecordApi(int 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> addRecipeLikeApi(int id) {
|
||||||
|
return HttpUtil().post<bool>("/food/recipe/$id/like");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> deleteRecipeLikeApi(int id) {
|
||||||
|
return HttpUtil().delete<bool>("/food/recipe/$id/like");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> addRecipeFavouriteApi(int id) {
|
||||||
|
return HttpUtil().post<bool>("/food/recipe/$id/favourite");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> deleteRecipeFavouriteApi(int id) {
|
||||||
|
return HttpUtil().delete<bool>("/food/recipe/$id/like");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Record>> queryRecordApi(String startDate, String endDate) {
|
||||||
|
return HttpUtil().get<List<Record>>(
|
||||||
|
"/food/record",
|
||||||
|
queryParameters: {"startDate": startDate, "endDate": endDate},
|
||||||
|
converter: (data) => convertListResponse<Record>(data, Record.fromJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> queryCategoryApi() {
|
||||||
|
return HttpUtil().get<List<String>>(
|
||||||
|
"/food/category",
|
||||||
|
converter:
|
||||||
|
(data) => convertListResponse<String>(data, (json) => json.toString()),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import 'package:food_hub_app/utils/http_util.dart';
|
|||||||
|
|
||||||
Future<Session> loginApi(String username, String password) {
|
Future<Session> loginApi(String username, String password) {
|
||||||
return HttpUtil().post<Session>(
|
return HttpUtil().post<Session>(
|
||||||
"/food-service/session",
|
"/session",
|
||||||
queryParameters: {"username": username, "password": password},
|
queryParameters: {"username": username, "password": password},
|
||||||
converter: (data) => Session.fromJson(data),
|
converter: (data) => Session.fromJson(data),
|
||||||
);
|
);
|
||||||
31
lib/apis/stats.dart
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
Future<SummaryStats> queryStatsApi() {
|
||||||
|
return HttpUtil().get<SummaryStats>(
|
||||||
|
"/food/stats",
|
||||||
|
converter: (data) => SummaryStats.fromJson(data),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ChartData>> queryRecordStatsApi() {
|
||||||
|
return HttpUtil().get<List<ChartData>>(
|
||||||
|
"/food/stats/record",
|
||||||
|
converter: (data) => convertListResponse<ChartData>(data, ChartData.fromJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ChartData>> queryCategoryStatsApi() {
|
||||||
|
return HttpUtil().get<List<ChartData>>(
|
||||||
|
"/food/stats/category",
|
||||||
|
converter: (data) => convertListResponse<ChartData>(data, ChartData.fromJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ChartData>> queryRankStatsApi() {
|
||||||
|
return HttpUtil().get<List<ChartData>>(
|
||||||
|
"/food/stats/rank",
|
||||||
|
converter: (data) => convertListResponse<ChartData>(data, ChartData.fromJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
9
lib/config/app_config.dart
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/// 应用信息
|
||||||
|
class AppConfig {
|
||||||
|
// 网络配置
|
||||||
|
// http://192.168.1.3:8100
|
||||||
|
// http://14.103.235.151:81/food-service
|
||||||
|
// static const String baseApiUrl = "http://14.103.235.151:81/food-service";
|
||||||
|
// static const String baseApiUrl = "http://192.168.1.3:8100";
|
||||||
|
static const String baseApiUrl = "https://cxx0822.iepose.cn/food-api";
|
||||||
|
}
|
||||||
@@ -2,42 +2,30 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
class NavBar extends StatelessWidget {
|
class NavBar extends StatelessWidget {
|
||||||
final int currentIndex;
|
final int currentIndex;
|
||||||
|
final List<BottomNavigationBarItem> navItems;
|
||||||
final Function(int) onTap;
|
final Function(int) onTap;
|
||||||
|
|
||||||
static const List<BottomNavigationBarItem> navItems = [
|
const NavBar({
|
||||||
BottomNavigationBarItem(
|
super.key,
|
||||||
icon: Icon(Icons.home_outlined),
|
required this.currentIndex,
|
||||||
activeIcon: Icon(Icons.home),
|
required this.onTap,
|
||||||
label: "记录",
|
required this.navItems,
|
||||||
),
|
});
|
||||||
BottomNavigationBarItem(
|
|
||||||
icon: Icon(Icons.pie_chart_outline),
|
|
||||||
activeIcon: Icon(Icons.pie_chart),
|
|
||||||
label: "统计",
|
|
||||||
),
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
icon: Icon(Icons.group_outlined),
|
|
||||||
activeIcon: Icon(Icons.group),
|
|
||||||
label: "朋友圈",
|
|
||||||
),
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
icon: Icon(Icons.account_circle_outlined),
|
|
||||||
activeIcon: Icon(Icons.account_circle),
|
|
||||||
label: "我的",
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
const NavBar({super.key, required this.currentIndex, required this.onTap});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BottomNavigationBar(
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(top: BorderSide(color: Theme.of(context).primaryColor)),
|
||||||
|
),
|
||||||
|
child: BottomNavigationBar(
|
||||||
currentIndex: currentIndex,
|
currentIndex: currentIndex,
|
||||||
iconSize: 25,
|
iconSize: 25,
|
||||||
type: BottomNavigationBarType.fixed,
|
type: BottomNavigationBarType.fixed,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
items: navItems,
|
items: navItems,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,7 +98,7 @@ class SettingsDrawer extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<StatelessWidget> homeActions() {
|
List<StatelessWidget> homeActions(BuildContext context) {
|
||||||
return [
|
return [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.search, color: Colors.white),
|
icon: Icon(Icons.search, color: Colors.white),
|
||||||
@@ -118,15 +106,11 @@ List<StatelessWidget> homeActions() {
|
|||||||
// 搜索功能
|
// 搜索功能
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Builder(
|
// IconButton(
|
||||||
builder: (BuildContext context) {
|
// icon: Icon(Icons.add, color: Colors.white),
|
||||||
return IconButton(
|
// onPressed: () {
|
||||||
icon: Icon(Icons.settings, color: Colors.white),
|
// Navigator.pushNamed(context, '/recordForm');
|
||||||
onPressed: () {
|
// },
|
||||||
Scaffold.of(context).openEndDrawer();
|
// ),
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import 'package:flutter_localizations/flutter_localizations.dart';
|
|||||||
import 'package:food_hub_app/utils/sp_util.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/recordForm.dart';
|
import 'package:food_hub_app/views/record_form.dart';
|
||||||
|
import 'package:food_hub_app/views/recipe_detail.dart';
|
||||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ class MyApp extends StatelessWidget {
|
|||||||
routes: {
|
routes: {
|
||||||
'/home': (context) => HomePage(),
|
'/home': (context) => HomePage(),
|
||||||
'/recordForm': (context) => RecordFormPage(),
|
'/recordForm': (context) => RecordFormPage(),
|
||||||
|
'/recipeDetail': (context) => RecipeDetailPage(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
16
lib/models/layout.dart
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// 导航栏
|
||||||
|
class NavItem {
|
||||||
|
final String label;
|
||||||
|
final IconData icon;
|
||||||
|
final IconData activeIcon;
|
||||||
|
final Widget page;
|
||||||
|
|
||||||
|
const NavItem({
|
||||||
|
required this.label,
|
||||||
|
required this.icon,
|
||||||
|
required this.activeIcon,
|
||||||
|
required this.page,
|
||||||
|
});
|
||||||
|
}
|
||||||
74
lib/models/moment.dart
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'moment.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class Comment {
|
||||||
|
final int id;
|
||||||
|
final String username;
|
||||||
|
final String avatar;
|
||||||
|
final String content;
|
||||||
|
final String date;
|
||||||
|
|
||||||
|
const Comment({
|
||||||
|
required this.id,
|
||||||
|
required this.username,
|
||||||
|
required this.avatar,
|
||||||
|
required this.content,
|
||||||
|
required this.date,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Comment.fromJson(Map<String, dynamic> json) => _$CommentFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$CommentToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
|
||||||
|
Moment({
|
||||||
|
this.id,
|
||||||
|
this.userId,
|
||||||
|
this.username,
|
||||||
|
this.avatar,
|
||||||
|
required this.content,
|
||||||
|
required this.imageList,
|
||||||
|
this.date,
|
||||||
|
this.likeList,
|
||||||
|
this.commentList,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Moment.fromJson(Map<String, dynamic> json) => _$MomentFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$MomentToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class PageMoment {
|
||||||
|
final List<Moment> records;
|
||||||
|
final int total;
|
||||||
|
final int size;
|
||||||
|
final int current;
|
||||||
|
final int pages;
|
||||||
|
|
||||||
|
PageMoment({
|
||||||
|
required this.records,
|
||||||
|
required this.total,
|
||||||
|
required this.size,
|
||||||
|
required this.current,
|
||||||
|
required this.pages,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory PageMoment.fromJson(Map<String, dynamic> json) => _$PageMomentFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$PageMomentToJson(this);
|
||||||
|
}
|
||||||
74
lib/models/moment.g.dart
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'moment.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
Comment _$CommentFromJson(Map<String, dynamic> json) => Comment(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
username: json['username'] as String,
|
||||||
|
avatar: json['avatar'] as String,
|
||||||
|
content: json['content'] as String,
|
||||||
|
date: json['date'] as String,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$CommentToJson(Comment instance) => <String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'username': instance.username,
|
||||||
|
'avatar': instance.avatar,
|
||||||
|
'content': instance.content,
|
||||||
|
'date': instance.date,
|
||||||
|
};
|
||||||
|
|
||||||
|
Moment _$MomentFromJson(Map<String, dynamic> json) => Moment(
|
||||||
|
id: (json['id'] as num?)?.toInt(),
|
||||||
|
userId: (json['userId'] as num?)?.toInt(),
|
||||||
|
username: json['username'] as String?,
|
||||||
|
avatar: json['avatar'] as String?,
|
||||||
|
content: json['content'] as String,
|
||||||
|
imageList:
|
||||||
|
(json['imageList'] as List<dynamic>).map((e) => e as String).toList(),
|
||||||
|
date: json['date'] as String?,
|
||||||
|
likeList:
|
||||||
|
(json['likeList'] as List<dynamic>?)
|
||||||
|
?.map((e) => (e as num).toInt())
|
||||||
|
.toList(),
|
||||||
|
commentList:
|
||||||
|
(json['commentList'] as List<dynamic>?)
|
||||||
|
?.map((e) => Comment.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$MomentToJson(Moment instance) => <String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'userId': instance.userId,
|
||||||
|
'username': instance.username,
|
||||||
|
'avatar': instance.avatar,
|
||||||
|
'content': instance.content,
|
||||||
|
'imageList': instance.imageList,
|
||||||
|
'date': instance.date,
|
||||||
|
'likeList': instance.likeList,
|
||||||
|
'commentList': instance.commentList,
|
||||||
|
};
|
||||||
|
|
||||||
|
PageMoment _$PageMomentFromJson(Map<String, dynamic> json) => PageMoment(
|
||||||
|
records:
|
||||||
|
(json['records'] as List<dynamic>)
|
||||||
|
.map((e) => Moment.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
total: (json['total'] as num).toInt(),
|
||||||
|
size: (json['size'] as num).toInt(),
|
||||||
|
current: (json['current'] as num).toInt(),
|
||||||
|
pages: (json['pages'] as num).toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$PageMomentToJson(PageMoment instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'records': instance.records,
|
||||||
|
'total': instance.total,
|
||||||
|
'size': instance.size,
|
||||||
|
'current': instance.current,
|
||||||
|
'pages': instance.pages,
|
||||||
|
};
|
||||||
@@ -4,43 +4,43 @@ part 'recipe.g.dart';
|
|||||||
|
|
||||||
/// 食材信息
|
/// 食材信息
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Material {
|
class RecipeMaterial {
|
||||||
String type;
|
String type;
|
||||||
String name;
|
String name;
|
||||||
String amount;
|
String amount;
|
||||||
|
|
||||||
Material({required this.type, required this.name, required this.amount});
|
RecipeMaterial({required this.type, required this.name, required this.amount});
|
||||||
|
|
||||||
factory Material.fromJson(Map<String, dynamic> json) =>
|
factory RecipeMaterial.fromJson(Map<String, dynamic> json) =>
|
||||||
_$MaterialFromJson(json);
|
_$RecipeMaterialFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$MaterialToJson(this);
|
Map<String, dynamic> toJson() => _$RecipeMaterialToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 步骤信息
|
/// 步骤信息
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Step {
|
class RecipeStep {
|
||||||
int sort;
|
int sort;
|
||||||
String content;
|
String content;
|
||||||
String imageUrl;
|
String imageUrl;
|
||||||
|
|
||||||
Step({required this.sort, required this.content, required this.imageUrl});
|
RecipeStep({required this.sort, required this.content, required this.imageUrl});
|
||||||
|
|
||||||
factory Step.fromJson(Map<String, dynamic> json) => _$StepFromJson(json);
|
factory RecipeStep.fromJson(Map<String, dynamic> json) => _$RecipeStepFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$StepToJson(this);
|
Map<String, dynamic> toJson() => _$RecipeStepToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 评论信息
|
/// 评论信息
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Comment {
|
class RecipeComment {
|
||||||
int id;
|
int id;
|
||||||
String username;
|
String username;
|
||||||
String avatar;
|
String avatar;
|
||||||
String content;
|
String content;
|
||||||
String date;
|
String date;
|
||||||
|
|
||||||
Comment({
|
RecipeComment({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.username,
|
required this.username,
|
||||||
required this.avatar,
|
required this.avatar,
|
||||||
@@ -48,10 +48,10 @@ class Comment {
|
|||||||
required this.date,
|
required this.date,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Comment.fromJson(Map<String, dynamic> json) =>
|
factory RecipeComment.fromJson(Map<String, dynamic> json) =>
|
||||||
_$CommentFromJson(json);
|
_$RecipeCommentFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$CommentToJson(this);
|
Map<String, dynamic> toJson() => _$RecipeCommentToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 成果信息
|
/// 成果信息
|
||||||
@@ -94,44 +94,44 @@ class RecipeQuery {
|
|||||||
/// 菜谱信息
|
/// 菜谱信息
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Recipe {
|
class Recipe {
|
||||||
int? id;
|
int id;
|
||||||
String name;
|
String name;
|
||||||
String category;
|
String category;
|
||||||
double recommendRate;
|
double recommendRate;
|
||||||
String? remark;
|
String remark;
|
||||||
bool isShare;
|
bool isShare;
|
||||||
int? userId;
|
int userId;
|
||||||
String? username;
|
String username;
|
||||||
String? avatar;
|
String avatar;
|
||||||
List<Material>? materialList;
|
List<RecipeMaterial> materialList;
|
||||||
List<Step>? stepList;
|
List<RecipeStep> stepList;
|
||||||
List<Record> recordList;
|
List<Record> recordList;
|
||||||
List<int>? likeList;
|
List<int> likeList;
|
||||||
int? likeCount;
|
int likeCount;
|
||||||
List<int>? favouriteList;
|
List<int> favouriteList;
|
||||||
int? favouriteCount;
|
int favouriteCount;
|
||||||
List<Comment>? commentList;
|
List<RecipeComment> commentList;
|
||||||
int? commentCount;
|
int commentCount;
|
||||||
|
|
||||||
Recipe({
|
Recipe({
|
||||||
this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.category,
|
required this.category,
|
||||||
required this.recommendRate,
|
required this.recommendRate,
|
||||||
this.remark,
|
required this.remark,
|
||||||
required this.isShare,
|
required this.isShare,
|
||||||
this.userId,
|
required this.userId,
|
||||||
this.username,
|
required this.username,
|
||||||
this.avatar,
|
required this.avatar,
|
||||||
this.materialList,
|
required this.materialList,
|
||||||
this.stepList,
|
required this.stepList,
|
||||||
required this.recordList,
|
required this.recordList,
|
||||||
this.likeList,
|
required this.likeList,
|
||||||
this.likeCount,
|
required this.likeCount,
|
||||||
this.favouriteList,
|
required this.favouriteList,
|
||||||
this.favouriteCount,
|
required this.favouriteCount,
|
||||||
this.commentList,
|
required this.commentList,
|
||||||
this.commentCount,
|
required this.commentCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Recipe.fromJson(Map<String, dynamic> json) => _$RecipeFromJson(json);
|
factory Recipe.fromJson(Map<String, dynamic> json) => _$RecipeFromJson(json);
|
||||||
@@ -139,4 +139,80 @@ class Recipe {
|
|||||||
Map<String, dynamic> toJson() => _$RecipeToJson(this);
|
Map<String, dynamic> toJson() => _$RecipeToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class RecipeSummary {
|
||||||
|
int id;
|
||||||
|
String name;
|
||||||
|
String category;
|
||||||
|
double recommendRate;
|
||||||
|
bool isShare;
|
||||||
|
List<Record> recordList;
|
||||||
|
int likeCount;
|
||||||
|
int favouriteCount;
|
||||||
|
int commentCount;
|
||||||
|
int userId;
|
||||||
|
String username;
|
||||||
|
String avatar;
|
||||||
|
|
||||||
|
RecipeSummary({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.category,
|
||||||
|
required this.recommendRate,
|
||||||
|
required this.isShare,
|
||||||
|
required this.userId,
|
||||||
|
required this.username,
|
||||||
|
required this.avatar,
|
||||||
|
required this.recordList,
|
||||||
|
required this.likeCount,
|
||||||
|
required this.favouriteCount,
|
||||||
|
required this.commentCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory RecipeSummary.fromJson(Map<String, dynamic> json) => _$RecipeSummaryFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$RecipeSummaryToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class RecipeDetail {
|
||||||
|
int id;
|
||||||
|
String name;
|
||||||
|
String category;
|
||||||
|
double recommendRate;
|
||||||
|
String remark;
|
||||||
|
bool isShare;
|
||||||
|
int userId;
|
||||||
|
String username;
|
||||||
|
String avatar;
|
||||||
|
List<RecipeMaterial> materialList;
|
||||||
|
List<RecipeStep> stepList;
|
||||||
|
List<Record> recordList;
|
||||||
|
List<int> likeList;
|
||||||
|
List<int> favouriteList;
|
||||||
|
List<RecipeComment> commentList;
|
||||||
|
|
||||||
|
RecipeDetail({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.category,
|
||||||
|
required this.recommendRate,
|
||||||
|
required this.remark,
|
||||||
|
required this.isShare,
|
||||||
|
required this.userId,
|
||||||
|
required this.username,
|
||||||
|
required this.avatar,
|
||||||
|
required this.materialList,
|
||||||
|
required this.stepList,
|
||||||
|
required this.recordList,
|
||||||
|
required this.likeList,
|
||||||
|
required this.favouriteList,
|
||||||
|
required this.commentList,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory RecipeDetail.fromJson(Map<String, dynamic> json) => _$RecipeDetailFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$RecipeDetailToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
enum ViewType { recipe, calendar, timeline }
|
enum ViewType { recipe, calendar, timeline }
|
||||||
|
|||||||
@@ -6,31 +6,35 @@ part of 'recipe.dart';
|
|||||||
// JsonSerializableGenerator
|
// JsonSerializableGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
Material _$MaterialFromJson(Map<String, dynamic> json) => Material(
|
RecipeMaterial _$RecipeMaterialFromJson(Map<String, dynamic> json) =>
|
||||||
|
RecipeMaterial(
|
||||||
type: json['type'] as String,
|
type: json['type'] as String,
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
amount: json['amount'] as String,
|
amount: json['amount'] as String,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$MaterialToJson(Material instance) => <String, dynamic>{
|
Map<String, dynamic> _$RecipeMaterialToJson(RecipeMaterial instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
'type': instance.type,
|
'type': instance.type,
|
||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
'amount': instance.amount,
|
'amount': instance.amount,
|
||||||
};
|
};
|
||||||
|
|
||||||
Step _$StepFromJson(Map<String, dynamic> json) => Step(
|
RecipeStep _$RecipeStepFromJson(Map<String, dynamic> json) => RecipeStep(
|
||||||
sort: (json['sort'] as num).toInt(),
|
sort: (json['sort'] as num).toInt(),
|
||||||
content: json['content'] as String,
|
content: json['content'] as String,
|
||||||
imageUrl: json['imageUrl'] as String,
|
imageUrl: json['imageUrl'] as String,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$StepToJson(Step instance) => <String, dynamic>{
|
Map<String, dynamic> _$RecipeStepToJson(RecipeStep instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
'sort': instance.sort,
|
'sort': instance.sort,
|
||||||
'content': instance.content,
|
'content': instance.content,
|
||||||
'imageUrl': instance.imageUrl,
|
'imageUrl': instance.imageUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
Comment _$CommentFromJson(Map<String, dynamic> json) => Comment(
|
RecipeComment _$RecipeCommentFromJson(Map<String, dynamic> json) =>
|
||||||
|
RecipeComment(
|
||||||
id: (json['id'] as num).toInt(),
|
id: (json['id'] as num).toInt(),
|
||||||
username: json['username'] as String,
|
username: json['username'] as String,
|
||||||
avatar: json['avatar'] as String,
|
avatar: json['avatar'] as String,
|
||||||
@@ -38,7 +42,8 @@ Comment _$CommentFromJson(Map<String, dynamic> json) => Comment(
|
|||||||
date: json['date'] as String,
|
date: json['date'] as String,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$CommentToJson(Comment instance) => <String, dynamic>{
|
Map<String, dynamic> _$RecipeCommentToJson(RecipeComment instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'username': instance.username,
|
'username': instance.username,
|
||||||
'avatar': instance.avatar,
|
'avatar': instance.avatar,
|
||||||
@@ -71,42 +76,42 @@ Map<String, dynamic> _$RecipeQueryToJson(RecipeQuery instance) =>
|
|||||||
<String, dynamic>{'category': instance.category};
|
<String, dynamic>{'category': instance.category};
|
||||||
|
|
||||||
Recipe _$RecipeFromJson(Map<String, dynamic> json) => Recipe(
|
Recipe _$RecipeFromJson(Map<String, dynamic> json) => Recipe(
|
||||||
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,
|
||||||
recommendRate: (json['recommendRate'] as num).toDouble(),
|
recommendRate: (json['recommendRate'] as num).toDouble(),
|
||||||
remark: json['remark'] as String?,
|
remark: json['remark'] as String,
|
||||||
isShare: json['isShare'] as bool,
|
isShare: json['isShare'] as bool,
|
||||||
userId: (json['userId'] as num?)?.toInt(),
|
userId: (json['userId'] as num).toInt(),
|
||||||
username: json['username'] as String?,
|
username: json['username'] as String,
|
||||||
avatar: json['avatar'] as String?,
|
avatar: json['avatar'] as String,
|
||||||
materialList:
|
materialList:
|
||||||
(json['materialList'] as List<dynamic>?)
|
(json['materialList'] as List<dynamic>)
|
||||||
?.map((e) => Material.fromJson(e as Map<String, dynamic>))
|
.map((e) => RecipeMaterial.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
stepList:
|
stepList:
|
||||||
(json['stepList'] as List<dynamic>?)
|
(json['stepList'] as List<dynamic>)
|
||||||
?.map((e) => Step.fromJson(e as Map<String, dynamic>))
|
.map((e) => RecipeStep.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
recordList:
|
recordList:
|
||||||
(json['recordList'] as List<dynamic>)
|
(json['recordList'] as List<dynamic>)
|
||||||
.map((e) => Record.fromJson(e as Map<String, dynamic>))
|
.map((e) => Record.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
likeList:
|
likeList:
|
||||||
(json['likeList'] as List<dynamic>?)
|
(json['likeList'] as List<dynamic>)
|
||||||
?.map((e) => (e as num).toInt())
|
.map((e) => (e as num).toInt())
|
||||||
.toList(),
|
.toList(),
|
||||||
likeCount: (json['likeCount'] as num?)?.toInt(),
|
likeCount: (json['likeCount'] as num).toInt(),
|
||||||
favouriteList:
|
favouriteList:
|
||||||
(json['favouriteList'] as List<dynamic>?)
|
(json['favouriteList'] as List<dynamic>)
|
||||||
?.map((e) => (e as num).toInt())
|
.map((e) => (e as num).toInt())
|
||||||
.toList(),
|
.toList(),
|
||||||
favouriteCount: (json['favouriteCount'] as num?)?.toInt(),
|
favouriteCount: (json['favouriteCount'] as num).toInt(),
|
||||||
commentList:
|
commentList:
|
||||||
(json['commentList'] as List<dynamic>?)
|
(json['commentList'] as List<dynamic>)
|
||||||
?.map((e) => Comment.fromJson(e as Map<String, dynamic>))
|
.map((e) => RecipeComment.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
commentCount: (json['commentCount'] as num?)?.toInt(),
|
commentCount: (json['commentCount'] as num).toInt(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{
|
Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{
|
||||||
@@ -129,3 +134,93 @@ Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{
|
|||||||
'commentList': instance.commentList,
|
'commentList': instance.commentList,
|
||||||
'commentCount': instance.commentCount,
|
'commentCount': instance.commentCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
RecipeSummary _$RecipeSummaryFromJson(Map<String, dynamic> json) =>
|
||||||
|
RecipeSummary(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
name: json['name'] as String,
|
||||||
|
category: json['category'] as String,
|
||||||
|
recommendRate: (json['recommendRate'] as num).toDouble(),
|
||||||
|
isShare: json['isShare'] as bool,
|
||||||
|
userId: (json['userId'] as num).toInt(),
|
||||||
|
username: json['username'] as String,
|
||||||
|
avatar: json['avatar'] as String,
|
||||||
|
recordList:
|
||||||
|
(json['recordList'] as List<dynamic>)
|
||||||
|
.map((e) => Record.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
likeCount: (json['likeCount'] as num).toInt(),
|
||||||
|
favouriteCount: (json['favouriteCount'] as num).toInt(),
|
||||||
|
commentCount: (json['commentCount'] as num).toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$RecipeSummaryToJson(RecipeSummary instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'name': instance.name,
|
||||||
|
'category': instance.category,
|
||||||
|
'recommendRate': instance.recommendRate,
|
||||||
|
'isShare': instance.isShare,
|
||||||
|
'recordList': instance.recordList,
|
||||||
|
'likeCount': instance.likeCount,
|
||||||
|
'favouriteCount': instance.favouriteCount,
|
||||||
|
'commentCount': instance.commentCount,
|
||||||
|
'userId': instance.userId,
|
||||||
|
'username': instance.username,
|
||||||
|
'avatar': instance.avatar,
|
||||||
|
};
|
||||||
|
|
||||||
|
RecipeDetail _$RecipeDetailFromJson(Map<String, dynamic> json) => RecipeDetail(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
name: json['name'] as String,
|
||||||
|
category: json['category'] as String,
|
||||||
|
recommendRate: (json['recommendRate'] as num).toDouble(),
|
||||||
|
remark: json['remark'] as String,
|
||||||
|
isShare: json['isShare'] as bool,
|
||||||
|
userId: (json['userId'] as num).toInt(),
|
||||||
|
username: json['username'] as String,
|
||||||
|
avatar: json['avatar'] as String,
|
||||||
|
materialList:
|
||||||
|
(json['materialList'] as List<dynamic>)
|
||||||
|
.map((e) => RecipeMaterial.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
stepList:
|
||||||
|
(json['stepList'] as List<dynamic>)
|
||||||
|
.map((e) => RecipeStep.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
recordList:
|
||||||
|
(json['recordList'] as List<dynamic>)
|
||||||
|
.map((e) => Record.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
likeList:
|
||||||
|
(json['likeList'] as List<dynamic>)
|
||||||
|
.map((e) => (e as num).toInt())
|
||||||
|
.toList(),
|
||||||
|
favouriteList:
|
||||||
|
(json['favouriteList'] as List<dynamic>)
|
||||||
|
.map((e) => (e as num).toInt())
|
||||||
|
.toList(),
|
||||||
|
commentList:
|
||||||
|
(json['commentList'] as List<dynamic>)
|
||||||
|
.map((e) => RecipeComment.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$RecipeDetailToJson(RecipeDetail instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'name': instance.name,
|
||||||
|
'category': instance.category,
|
||||||
|
'recommendRate': instance.recommendRate,
|
||||||
|
'remark': instance.remark,
|
||||||
|
'isShare': instance.isShare,
|
||||||
|
'userId': instance.userId,
|
||||||
|
'username': instance.username,
|
||||||
|
'avatar': instance.avatar,
|
||||||
|
'materialList': instance.materialList,
|
||||||
|
'stepList': instance.stepList,
|
||||||
|
'recordList': instance.recordList,
|
||||||
|
'likeList': instance.likeList,
|
||||||
|
'favouriteList': instance.favouriteList,
|
||||||
|
'commentList': instance.commentList,
|
||||||
|
};
|
||||||
|
|||||||
34
lib/models/stats.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'stats.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class SummaryStats {
|
||||||
|
final int recipeCount;
|
||||||
|
final int categoryCount;
|
||||||
|
final int workCount;
|
||||||
|
|
||||||
|
const SummaryStats({
|
||||||
|
required this.recipeCount,
|
||||||
|
required this.categoryCount,
|
||||||
|
required this.workCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory SummaryStats.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SummaryStatsFromJson(json);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
30
lib/models/stats.g.dart
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'stats.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
SummaryStats _$SummaryStatsFromJson(Map<String, dynamic> json) => SummaryStats(
|
||||||
|
recipeCount: (json['recipeCount'] as num).toInt(),
|
||||||
|
categoryCount: (json['categoryCount'] as num).toInt(),
|
||||||
|
workCount: (json['workCount'] as num).toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SummaryStatsToJson(SummaryStats instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'recipeCount': instance.recipeCount,
|
||||||
|
'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,5 +1,5 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:food_hub_app/config/app_config.dart';
|
||||||
import 'package:food_hub_app/utils/sp_util.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';
|
||||||
|
|
||||||
@@ -11,7 +11,6 @@ class HttpUtil {
|
|||||||
factory HttpUtil() => _instance;
|
factory HttpUtil() => _instance;
|
||||||
|
|
||||||
late Dio _dio;
|
late Dio _dio;
|
||||||
String baseUrl = kDebugMode ? "http://172.29.101.108:8100" : "http://14.103.235.151:81";
|
|
||||||
|
|
||||||
// 请求头配置
|
// 请求头配置
|
||||||
Map<String, dynamic> headers = {
|
Map<String, dynamic> headers = {
|
||||||
@@ -25,7 +24,7 @@ class HttpUtil {
|
|||||||
HttpUtil._internal() {
|
HttpUtil._internal() {
|
||||||
// 初始化Dio实例
|
// 初始化Dio实例
|
||||||
BaseOptions options = BaseOptions(
|
BaseOptions options = BaseOptions(
|
||||||
baseUrl: baseUrl,
|
baseUrl: AppConfig.baseApiUrl,
|
||||||
connectTimeout: Duration(seconds: timeout),
|
connectTimeout: Duration(seconds: timeout),
|
||||||
receiveTimeout: Duration(seconds: timeout),
|
receiveTimeout: Duration(seconds: timeout),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
@@ -81,10 +80,6 @@ class HttpUtil {
|
|||||||
T Function(dynamic data)? converter,
|
T Function(dynamic data)? converter,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
if (kDebugMode) {
|
|
||||||
path = path.replaceFirst('/food-service', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
Response response = await _dio.request(
|
Response response = await _dio.request(
|
||||||
path,
|
path,
|
||||||
data: data,
|
data: data,
|
||||||
|
|||||||
3063
lib/views/data.dart
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:food_hub_app/layout/index.dart';
|
import 'package:food_hub_app/layout/index.dart';
|
||||||
|
import 'package:food_hub_app/models/layout.dart';
|
||||||
import 'package:food_hub_app/views/moment.dart';
|
import 'package:food_hub_app/views/moment.dart';
|
||||||
import 'package:food_hub_app/views/profile.dart';
|
import 'package:food_hub_app/views/profile.dart';
|
||||||
import 'package:food_hub_app/views/record.dart';
|
import 'package:food_hub_app/views/record.dart';
|
||||||
@@ -15,15 +16,112 @@ class HomePage extends StatefulWidget {
|
|||||||
class _HomePage extends State<HomePage> {
|
class _HomePage extends State<HomePage> {
|
||||||
int _currentIndex = 0;
|
int _currentIndex = 0;
|
||||||
|
|
||||||
final List<Widget> _tabPages = const [
|
final List<NavItem> navItems = [
|
||||||
RecordPage(),
|
NavItem(
|
||||||
StatsPage(),
|
label: "记录",
|
||||||
MomentPage(),
|
icon: Icons.home_outlined,
|
||||||
ProfilePage(),
|
activeIcon: Icons.home,
|
||||||
|
page: RecordPage(),
|
||||||
|
),
|
||||||
|
NavItem(
|
||||||
|
label: "统计",
|
||||||
|
icon: Icons.pie_chart_outline,
|
||||||
|
activeIcon: Icons.pie_chart,
|
||||||
|
page: StatsPage(),
|
||||||
|
),
|
||||||
|
NavItem(
|
||||||
|
label: "朋友圈",
|
||||||
|
icon: Icons.group_outlined,
|
||||||
|
activeIcon: Icons.group,
|
||||||
|
page: MomentPage(),
|
||||||
|
),
|
||||||
|
NavItem(
|
||||||
|
label: "我的",
|
||||||
|
icon: Icons.account_circle_outlined,
|
||||||
|
activeIcon: Icons.account_circle,
|
||||||
|
page: ProfilePage(),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
bool isShow(int index) {
|
List<BottomNavigationBarItem> get bottomNavItems =>
|
||||||
return index == 0 || index == 2;
|
navItems
|
||||||
|
.map(
|
||||||
|
(item) => BottomNavigationBarItem(
|
||||||
|
icon: Icon(item.icon),
|
||||||
|
activeIcon: Icon(item.activeIcon),
|
||||||
|
label: item.label,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
List<Widget> get tabPages => navItems.map((item) => item.page).toList();
|
||||||
|
|
||||||
|
// 显示底部弹窗
|
||||||
|
void _showBottomSheet() {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(
|
||||||
|
top: Radius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
builder: (context) => Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'请选择操作',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(Icons.book, color: Theme.of(context).primaryColor),
|
||||||
|
title: const Text('新增菜谱'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
_handleAddRecipe();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(Icons.note_add, color: Theme.of(context).primaryColor),
|
||||||
|
title: const Text('新增记录'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
Navigator.pushNamed(context, "/recordForm");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(Icons.group, color: Theme.of(context).primaryColor),
|
||||||
|
title: const Text('发布朋友圈'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
_handleAddRecord();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理添加菜谱
|
||||||
|
void _handleAddRecipe() {
|
||||||
|
// 这里添加跳转或处理添加菜谱的逻辑
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('添加菜谱功能')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理添加记录
|
||||||
|
void _handleAddRecord() {
|
||||||
|
// 这里添加跳转或处理添加记录的逻辑
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('添加记录功能')),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -33,20 +131,29 @@ class _HomePage extends State<HomePage> {
|
|||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: Text('Food Hub', style: TextStyle(color: Colors.white)),
|
title: Text('Food Hub', style: TextStyle(color: Colors.white)),
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
leading: IconButton(
|
leading: Builder(
|
||||||
icon: Icon(Icons.menu, color: Colors.white),
|
builder: (context) {
|
||||||
onPressed: () {
|
return IconButton(
|
||||||
// 打开侧边栏或菜单
|
icon: const Icon(Icons.menu),
|
||||||
|
color: Colors.white,
|
||||||
|
onPressed: () => Scaffold.of(context).openDrawer(),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
actions: homeActions(),
|
actions: homeActions(context),
|
||||||
),
|
),
|
||||||
backgroundColor: Color(0xFFF5F5F5),
|
backgroundColor: Color(0xFFF5F5F5),
|
||||||
endDrawer: const SettingsDrawer(),
|
drawer: const SettingsDrawer(),
|
||||||
body: _tabPages[_currentIndex],
|
body: tabPages[_currentIndex],
|
||||||
floatingActionButton:
|
floatingActionButton: FloatingActionButton(
|
||||||
isShow(_currentIndex) ? addItemButton(context) : null,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
|
onPressed: _showBottomSheet,
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
child: const Icon(Icons.add, color: Colors.white, size: 30),
|
||||||
|
),
|
||||||
|
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
|
||||||
bottomNavigationBar: NavBar(
|
bottomNavigationBar: NavBar(
|
||||||
|
navItems: bottomNavItems,
|
||||||
currentIndex: _currentIndex,
|
currentIndex: _currentIndex,
|
||||||
onTap: (index) {
|
onTap: (index) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -57,17 +164,3 @@ class _HomePage extends State<HomePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget addItemButton(BuildContext context) {
|
|
||||||
void onAddItemClick(BuildContext context) {
|
|
||||||
Navigator.pushNamed(context, '/recordForm');
|
|
||||||
}
|
|
||||||
|
|
||||||
return FloatingActionButton(
|
|
||||||
mini: true,
|
|
||||||
onPressed: () => onAddItemClick(context),
|
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
|
||||||
shape: const CircleBorder(),
|
|
||||||
child: Icon(Icons.add, color: Colors.white),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:food_hub_app/api/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/utils/sp_util.dart';
|
||||||
import 'package:food_hub_app/widgets/common/index.dart';
|
import 'package:food_hub_app/widgets/common/index.dart';
|
||||||
|
|||||||
@@ -1,15 +1,194 @@
|
|||||||
|
import 'package:easy_refresh/easy_refresh.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:food_hub_app/apis/moment.dart';
|
||||||
|
import 'package:food_hub_app/models/moment.dart';
|
||||||
|
import 'package:food_hub_app/widgets/moment/card.dart';
|
||||||
|
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
||||||
|
|
||||||
class MomentPage extends StatefulWidget {
|
class MomentPage extends StatefulWidget {
|
||||||
const MomentPage({super.key});
|
const MomentPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MomentPage> createState() => _MomentPage();
|
State<MomentPage> createState() => _MomentPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MomentPageState extends State<MomentPage> {
|
||||||
|
List<Moment> momentList = [];
|
||||||
|
int _currentPage = 1;
|
||||||
|
final int _pageSize = 3;
|
||||||
|
bool _hasMore = true;
|
||||||
|
|
||||||
|
// 初始化EasyRefresh控制器
|
||||||
|
final EasyRefreshController _freshController = EasyRefreshController(
|
||||||
|
controlFinishRefresh: true,
|
||||||
|
controlFinishLoad: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
bool _showScrollToTop = false;
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// 初始加载数据
|
||||||
|
_loadData(isRefresh: true);
|
||||||
|
_scrollController.addListener(_onScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.removeListener(_onScroll);
|
||||||
|
_freshController.dispose();
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的数据加载方法
|
||||||
|
Future<void> _loadData({required bool isRefresh}) async {
|
||||||
|
try {
|
||||||
|
// 如果是刷新,重置页码
|
||||||
|
if (isRefresh) {
|
||||||
|
_currentPage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await queryMomentByPageApi(_currentPage, _pageSize);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (isRefresh) {
|
||||||
|
// 刷新时直接替换数据
|
||||||
|
momentList = result.records;
|
||||||
|
} else {
|
||||||
|
// 加载更多时追加数据
|
||||||
|
momentList.addAll(result.records);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否还有更多数据
|
||||||
|
_hasMore = result.current < result.pages;
|
||||||
|
// 如果有更多数据,准备加载下一页
|
||||||
|
if (_hasMore) {
|
||||||
|
_currentPage++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// 处理错误
|
||||||
|
debugPrint('加载数据失败: $e');
|
||||||
|
} finally {
|
||||||
|
_freshController.finishRefresh();
|
||||||
|
_freshController.resetFooter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下拉刷新
|
||||||
|
Future<void> _onRefresh() async {
|
||||||
|
await _loadData(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上拉加载
|
||||||
|
Future<void> _onLoad() async {
|
||||||
|
if (_hasMore) {
|
||||||
|
await _loadData(isRefresh: false);
|
||||||
|
_freshController.finishLoad(IndicatorResult.success);
|
||||||
|
} else {
|
||||||
|
_freshController.finishLoad(IndicatorResult.noMore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onScroll() {
|
||||||
|
// 当滚动距离超过300时显示返回顶部按钮
|
||||||
|
if (_scrollController.offset > 300) {
|
||||||
|
if (!_showScrollToTop) {
|
||||||
|
setState(() {
|
||||||
|
_showScrollToTop = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_showScrollToTop) {
|
||||||
|
setState(() {
|
||||||
|
_showScrollToTop = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动到顶部
|
||||||
|
void _scrollToTop() {
|
||||||
|
_scrollController.animateTo(
|
||||||
|
0,
|
||||||
|
duration: const Duration(milliseconds: 500), // 滚动动画时长
|
||||||
|
curve: Curves.easeInOut, // 滚动动画曲线
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MomentPage extends State<MomentPage>{
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Center(child: Text("朋友圈"));
|
// 空状态显示
|
||||||
|
if (momentList.isEmpty) {
|
||||||
|
return EasyRefresh(
|
||||||
|
controller: _freshController,
|
||||||
|
onRefresh: _onRefresh,
|
||||||
|
child: const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有数据时显示列表
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(5),
|
||||||
|
child: EasyRefresh(
|
||||||
|
controller: _freshController,
|
||||||
|
header: ClassicHeader(
|
||||||
|
dragText: '下拉刷新',
|
||||||
|
armedText: '释放刷新',
|
||||||
|
readyText: '准备刷新',
|
||||||
|
processingText: '刷新中...',
|
||||||
|
processedText: '刷新完成',
|
||||||
|
failedText: '刷新失败',
|
||||||
|
noMoreText: '没有更多数据',
|
||||||
|
showText: true,
|
||||||
|
messageText: '更新于 %T',
|
||||||
|
showMessage: true,
|
||||||
|
),
|
||||||
|
footer: ClassicFooter(
|
||||||
|
dragText: '上拉加载',
|
||||||
|
armedText: '释放加载',
|
||||||
|
readyText: '准备加载',
|
||||||
|
processingText: '加载中...',
|
||||||
|
processedText: '加载完成',
|
||||||
|
failedText: '加载失败',
|
||||||
|
noMoreText: '没有更多数据',
|
||||||
|
showText: true,
|
||||||
|
messageText: '更新于 %T',
|
||||||
|
showMessage: true,
|
||||||
|
),
|
||||||
|
onRefresh: _onRefresh,
|
||||||
|
onLoad: _onLoad,
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: _scrollController,
|
||||||
|
itemCount: momentList.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return MomentCard(moment: momentList[index]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 返回顶部按钮
|
||||||
|
if (_showScrollToTop)
|
||||||
|
Positioned(
|
||||||
|
right: 10,
|
||||||
|
bottom: 20,
|
||||||
|
child: FloatingActionButton(
|
||||||
|
onPressed: _scrollToTop,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
elevation: 5,
|
||||||
|
mini: true,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.arrow_upward
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
247
lib/views/recipe_detail.dart
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:food_hub_app/apis/recipe.dart';
|
||||||
|
import 'package:food_hub_app/models/recipe.dart';
|
||||||
|
import 'package:food_hub_app/widgets/common/index.dart';
|
||||||
|
|
||||||
|
class RecipeDetailPage extends StatefulWidget {
|
||||||
|
const RecipeDetailPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RecipeDetailPage> createState() => _RecipeDetailState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||||
|
RecipeDetail recipe = RecipeDetail(
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
category: '',
|
||||||
|
recommendRate: 0,
|
||||||
|
remark: '',
|
||||||
|
isShare: false,
|
||||||
|
userId: 0,
|
||||||
|
username: '',
|
||||||
|
avatar: '',
|
||||||
|
materialList: [],
|
||||||
|
stepList: [],
|
||||||
|
recordList: [],
|
||||||
|
likeList: [],
|
||||||
|
favouriteList: [],
|
||||||
|
commentList: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 定义三个分类列表
|
||||||
|
List<RecipeMaterial> mainMaterialList = []; // 主料
|
||||||
|
List<RecipeMaterial> auxiliaryMaterialList = []; // 配料
|
||||||
|
List<RecipeMaterial> accessoryMaterialList = []; // 辅料
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
refreshRecipeDetail();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshRecipeDetail() async {
|
||||||
|
// 获取参数
|
||||||
|
final args = ModalRoute.of(context)?.settings.arguments as Map;
|
||||||
|
final recipeId = args['id'];
|
||||||
|
final result = await queryRecipeByIdApi(recipeId);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
recipe = result;
|
||||||
|
getRecipeMaterial();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void getRecipeMaterial() {
|
||||||
|
for (var material in recipe.materialList) {
|
||||||
|
switch (material.type) {
|
||||||
|
case '主料':
|
||||||
|
mainMaterialList.add(material);
|
||||||
|
break;
|
||||||
|
case '配料':
|
||||||
|
auxiliaryMaterialList.add(material);
|
||||||
|
break;
|
||||||
|
case '辅料':
|
||||||
|
accessoryMaterialList.add(material);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('菜谱信息', style: TextStyle(color: Colors.white)),
|
||||||
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: Icon(Icons.arrow_back, color: Colors.white),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: Color(0xFFF5F5F5),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: Padding(padding: EdgeInsets.all(5), child: _buildRecipeDetail()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRecipeDetail() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_buildTitleSection(),
|
||||||
|
SizedBox(height: 10),
|
||||||
|
_buildInfoCard(
|
||||||
|
context,
|
||||||
|
icon: Icons.info,
|
||||||
|
title: "基础信息",
|
||||||
|
content: _buildTag(context, title: recipe.category),
|
||||||
|
),
|
||||||
|
_buildInfoCard(
|
||||||
|
context,
|
||||||
|
icon: Icons.shopping_cart,
|
||||||
|
title: "食材信息",
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildMaterialSection(context, '主料', mainMaterialList),
|
||||||
|
_buildMaterialSection(context, '辅料', auxiliaryMaterialList),
|
||||||
|
_buildMaterialSection(context, '调料', accessoryMaterialList),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildInfoCard(
|
||||||
|
context,
|
||||||
|
icon: Icons.list,
|
||||||
|
title: "步骤信息",
|
||||||
|
content: Column(
|
||||||
|
children:
|
||||||
|
recipe.stepList.map((step) => _buildStepSection(step)).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildInfoCard(
|
||||||
|
context,
|
||||||
|
icon: Icons.note,
|
||||||
|
title: "其他信息",
|
||||||
|
content: Text(
|
||||||
|
'备注:${recipe.remark.isEmpty ? '无' : recipe.remark}',
|
||||||
|
style: const TextStyle(height: 1.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTag(BuildContext context, {required String title}) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Color.lerp(Theme.of(context).primaryColor, Colors.white, 0.8),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(color: Theme.of(context).primaryColor),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTitleSection() {
|
||||||
|
return Text(
|
||||||
|
recipe.name,
|
||||||
|
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMaterialSection(
|
||||||
|
BuildContext context,
|
||||||
|
String title,
|
||||||
|
List<RecipeMaterial> materials,
|
||||||
|
) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8, top: 12),
|
||||||
|
child: Text(title, style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
if (materials.isEmpty)
|
||||||
|
_buildTag(context, title: '无')
|
||||||
|
else
|
||||||
|
Wrap(
|
||||||
|
spacing: 8.0,
|
||||||
|
runSpacing: 8.0,
|
||||||
|
children:
|
||||||
|
materials
|
||||||
|
.map(
|
||||||
|
(m) => _buildTag(context, title: '${m.name} ${m.amount}'),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStepSection(RecipeStep step) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("第${step.sort + 1}步"),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
step.content,
|
||||||
|
style: const TextStyle(
|
||||||
|
height: 1.4,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (step.imageUrl.isNotEmpty)
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Image.network(
|
||||||
|
step.imageUrl,
|
||||||
|
width: double.infinity,
|
||||||
|
height: 180,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoCard(
|
||||||
|
BuildContext context, {
|
||||||
|
required IconData icon,
|
||||||
|
required String title,
|
||||||
|
required Widget content,
|
||||||
|
}) {
|
||||||
|
return cardContainer(
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: Theme.of(context).primaryColor),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 5),
|
||||||
|
content,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:food_hub_app/widgets/recipe/recipe_calendar.dart';
|
import 'package:food_hub_app/widgets/recipe/calendar.dart';
|
||||||
import 'package:food_hub_app/widgets/recipe/recipe_list.dart';
|
import 'package:food_hub_app/widgets/recipe/list.dart';
|
||||||
import 'package:food_hub_app/widgets/recipe/recipe_timeline.dart';
|
import 'package:food_hub_app/widgets/recipe/timeline.dart';
|
||||||
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
||||||
|
|
||||||
|
|
||||||
@@ -39,9 +39,7 @@ class _RecordPageState extends State<RecordPage>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Column(
|
||||||
padding: EdgeInsets.all(0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
children: [
|
||||||
TDTabBar(
|
TDTabBar(
|
||||||
tabs: tabs,
|
tabs: tabs,
|
||||||
@@ -63,7 +61,6 @@ class _RecordPageState extends State<RecordPage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,243 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
|
||||||
import 'package:food_hub_app/utils/index.dart';
|
|
||||||
import 'package:food_hub_app/widgets/common/index.dart';
|
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
|
||||||
|
|
||||||
class RecordFormPage extends StatefulWidget {
|
|
||||||
const RecordFormPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<RecordFormPage> createState() => _RecordFormPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RecordFormPage extends State<RecordFormPage> {
|
|
||||||
final _formKey = GlobalKey<FormBuilderState>();
|
|
||||||
|
|
||||||
List<TextEditingController> _controller = [];
|
|
||||||
FormController _formController = FormController();
|
|
||||||
|
|
||||||
String _selected_1 = '';
|
|
||||||
String _selected_2 = '';
|
|
||||||
String? _initLocalData;
|
|
||||||
|
|
||||||
final ImagePicker _picker = ImagePicker();
|
|
||||||
XFile? _selectedImage;
|
|
||||||
|
|
||||||
/// 整个表单存放的数据
|
|
||||||
Map<String, dynamic> _formData = {"name": '', "date": '', "photo": ''};
|
|
||||||
|
|
||||||
/// 定义整个校验规则
|
|
||||||
final Map<String, TDFormValidation> _validationRules = {
|
|
||||||
'name': TDFormValidation(
|
|
||||||
validate: (value) => value == null || value.isEmpty ? 'empty' : null,
|
|
||||||
errorMessage: '输入不能为空',
|
|
||||||
type: TDFormItemType.input,
|
|
||||||
),
|
|
||||||
"birth": TDFormValidation(
|
|
||||||
validate: (value) => value == null || value.isEmpty ? 'empty' : null,
|
|
||||||
errorMessage: '不能为空',
|
|
||||||
type: TDFormItemType.dateTimePicker,
|
|
||||||
),
|
|
||||||
"photo": TDFormValidation(
|
|
||||||
validate: (value) => value == null || value.isEmpty ? 'empty' : null,
|
|
||||||
errorMessage: '不能为空',
|
|
||||||
type: TDFormItemType.upLoadImg,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
List<TDUploadFile> files = [];
|
|
||||||
|
|
||||||
List<TDUploadFile> _onValueChanged(
|
|
||||||
List<TDUploadFile> fileList,
|
|
||||||
List<TDUploadFile> value,
|
|
||||||
TDUploadType event,
|
|
||||||
) {
|
|
||||||
switch (event) {
|
|
||||||
case TDUploadType.add:
|
|
||||||
fileList.addAll(value);
|
|
||||||
break;
|
|
||||||
case TDUploadType.remove:
|
|
||||||
fileList.removeWhere((element) => element.key == value[0].key);
|
|
||||||
break;
|
|
||||||
case TDUploadType.replace:
|
|
||||||
final firstReplaceFile = value.first;
|
|
||||||
final index = fileList.indexWhere(
|
|
||||||
(file) => file.key == firstReplaceFile.key,
|
|
||||||
);
|
|
||||||
if (index != -1) {
|
|
||||||
fileList[index] = firstReplaceFile;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return fileList;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _confirmClick(BuildContext context) {
|
|
||||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
|
||||||
// showSuccessToast("新增记录成功");
|
|
||||||
print(_formData);
|
|
||||||
} else {
|
|
||||||
TDMessage.showMessage(
|
|
||||||
context: context,
|
|
||||||
visible: true,
|
|
||||||
icon: true,
|
|
||||||
content: "请先输入信息",
|
|
||||||
theme: MessageTheme.error,
|
|
||||||
duration: 3000,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TDFormItem buildNameItem() {
|
|
||||||
return TDFormItem(
|
|
||||||
label: '菜谱名称',
|
|
||||||
name: 'name',
|
|
||||||
type: TDFormItemType.input,
|
|
||||||
labelWidth: 82.0,
|
|
||||||
showErrorMessage: true,
|
|
||||||
requiredMark: true,
|
|
||||||
child: TDInput(
|
|
||||||
leftContentSpace: 0,
|
|
||||||
inputDecoration: InputDecoration(
|
|
||||||
hintText: "请输入菜谱名称",
|
|
||||||
border: InputBorder.none,
|
|
||||||
hintStyle: TextStyle(color: TDTheme.of(context).grayColor6),
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
additionInfoColor: TDTheme.of(context).errorColor6,
|
|
||||||
showBottomDivider: false,
|
|
||||||
onChanged: (value) {
|
|
||||||
_formData['name'] = value;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TDFormItem buildDateItem() {
|
|
||||||
return TDFormItem(
|
|
||||||
label: '完成时间',
|
|
||||||
name: 'date',
|
|
||||||
labelWidth: 82.0,
|
|
||||||
type: TDFormItemType.dateTimePicker,
|
|
||||||
contentAlign: TextAlign.left,
|
|
||||||
tipAlign: TextAlign.left,
|
|
||||||
hintText: '请选择完成时间',
|
|
||||||
select: _formData['date'],
|
|
||||||
selectFn: (BuildContext context) {
|
|
||||||
DateTime now = DateTime.now();
|
|
||||||
TDPicker.showDatePicker(
|
|
||||||
context,
|
|
||||||
title: '选择时间',
|
|
||||||
onConfirm: (selected) {
|
|
||||||
setState(() {
|
|
||||||
_formData['date'] = parseDatePickerSelected(selected);
|
|
||||||
});
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
dateStart: [2000, 01, 01],
|
|
||||||
dateEnd: [2100, 12, 31],
|
|
||||||
initialDate: [now.year, now.month, now.day],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TDFormItem buildImageItem() {
|
|
||||||
return TDFormItem(
|
|
||||||
label: '上传图片',
|
|
||||||
name: 'photo',
|
|
||||||
labelWidth: 82.0,
|
|
||||||
type: TDFormItemType.upLoadImg,
|
|
||||||
child: TDUpload(
|
|
||||||
files: files,
|
|
||||||
onError: print,
|
|
||||||
onValidate: print,
|
|
||||||
onChange: ((imgList, type) {
|
|
||||||
files = _onValueChanged(files ?? [], imgList, type);
|
|
||||||
List imgs = files.map((e) => e.remotePath ?? e.assetPath).toList();
|
|
||||||
setState(() {});
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildFormBtnGroup() {
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(color: TDTheme.of(context).whiteColor1),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TDButton(
|
|
||||||
text: '取消',
|
|
||||||
type: TDButtonType.fill,
|
|
||||||
theme: TDButtonTheme.light,
|
|
||||||
shape: TDButtonShape.rectangle,
|
|
||||||
onTap: () => Navigator.pop(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 20),
|
|
||||||
Expanded(
|
|
||||||
child: TDButton(
|
|
||||||
text: '提交',
|
|
||||||
type: TDButtonType.fill,
|
|
||||||
theme: TDButtonTheme.primary,
|
|
||||||
shape: TDButtonShape.rectangle,
|
|
||||||
onTap: () => _confirmClick(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text('新增记录', style: TextStyle(color: Colors.white)),
|
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
|
||||||
leading: IconButton(
|
|
||||||
icon: Icon(Icons.arrow_back, color: Colors.white),
|
|
||||||
onPressed: () => Navigator.pop(context)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
backgroundColor: Color(0xFFF5F5F5),
|
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: TDForm(
|
|
||||||
formController: _formController,
|
|
||||||
data: _formData,
|
|
||||||
rules: _validationRules,
|
|
||||||
formContentAlign: TextAlign.left,
|
|
||||||
formShowErrorMessage: true,
|
|
||||||
onSubmit: () => {},
|
|
||||||
items: [buildNameItem(), buildDateItem(), buildImageItem()],
|
|
||||||
btnGroup: [buildFormBtnGroup()],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
333
lib/views/record_form.dart
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:food_hub_app/widgets/common/form.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
class RecordFormPage extends StatefulWidget {
|
||||||
|
const RecordFormPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RecordFormPage> createState() => _RecordFormPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecordFormPageState extends State<RecordFormPage> {
|
||||||
|
final _formKey = GlobalKey<FormBuilderState>();
|
||||||
|
final _focusNode = FocusNode();
|
||||||
|
static const String _nameField = 'name';
|
||||||
|
static const String _dateField = 'date';
|
||||||
|
|
||||||
|
final ImagePicker _picker = ImagePicker();
|
||||||
|
File? imageUrl; // 改为单张图片变量
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_focusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('新增记录', style: TextStyle(color: Colors.white)),
|
||||||
|
backgroundColor: theme.primaryColor,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: const Color(0xFFF5F5F5),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(5),
|
||||||
|
child: Card(
|
||||||
|
elevation: 0,
|
||||||
|
color: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: _buildFormBuilder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFormBuilder() {
|
||||||
|
return FormBuilder(
|
||||||
|
key: _formKey,
|
||||||
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
buildFormLabel('菜谱名称', required: true),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildNameTextField(),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
buildFormLabel('完成时间', required: true),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildDateTextField(),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
buildFormLabel('上传图片', required: true),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_buildImageUploadArea(),
|
||||||
|
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
buildFormButtonGroup(context: context, onConfirm: () => _submitForm),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 菜谱名称输入框
|
||||||
|
Widget _buildNameTextField() {
|
||||||
|
return FormBuilderTextField(
|
||||||
|
name: _nameField,
|
||||||
|
focusNode: _focusNode,
|
||||||
|
decoration: buildInputDecoration(context: context, hintText: '请输入菜谱名称'),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '请输入菜谱名称';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 完成时间选择框
|
||||||
|
Widget _buildDateTextField() {
|
||||||
|
return FormBuilderTextField(
|
||||||
|
name: _dateField,
|
||||||
|
readOnly: true,
|
||||||
|
decoration: buildInputDecoration(
|
||||||
|
context: context,
|
||||||
|
hintText: '请选择完成时间',
|
||||||
|
prefixIcon: const Icon(
|
||||||
|
Icons.calendar_month,
|
||||||
|
size: 20,
|
||||||
|
color: Color(0xFF86909C),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () => _onSelectDate(),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '请选择完成时间';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 图片上传区域(预览+上传按钮)
|
||||||
|
Widget _buildImageUploadArea() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (imageUrl != null)
|
||||||
|
_buildImagePreviewItem()
|
||||||
|
else
|
||||||
|
_buildImageUploadButton(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImagePreviewItem() {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => _showImagePreview(),
|
||||||
|
child: Image(
|
||||||
|
image: FileImage(imageUrl!),
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
loadingBuilder: (context, child, loadingProgress) {
|
||||||
|
if (loadingProgress == null) return child;
|
||||||
|
return Container(
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
color: Colors.grey[100],
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Positioned(
|
||||||
|
top: 6,
|
||||||
|
right: 6,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: _removeImage,
|
||||||
|
child: Container(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.red,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black26,
|
||||||
|
blurRadius: 2,
|
||||||
|
spreadRadius: 0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.close, color: Colors.white, size: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showImagePreview() {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierColor: Colors.black87, // 半透明黑色背景
|
||||||
|
builder:
|
||||||
|
(context) => Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
insetPadding: const EdgeInsets.all(16),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => Navigator.pop(context), // 点击空白处关闭
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
// 预览图添加轻微阴影
|
||||||
|
boxShadow: [BoxShadow(color: Colors.black38, blurRadius: 10)],
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Image.file(
|
||||||
|
imageUrl!,
|
||||||
|
fit: BoxFit.contain, // 保持图片比例
|
||||||
|
height: MediaQuery.of(context).size.height * 0.7, // 限制最大高度
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 图片上传按钮
|
||||||
|
Widget _buildImageUploadButton() {
|
||||||
|
return InkWell(
|
||||||
|
onTap: _pickImage,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Container(
|
||||||
|
width: 96,
|
||||||
|
height: 96,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF2F3F5),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: const Color(0xFFDCDFE6), width: 1),
|
||||||
|
),
|
||||||
|
child: const Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.add, color: Color(0xFF86909C), size: 24),
|
||||||
|
SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
'添加图片',
|
||||||
|
style: TextStyle(color: Color(0xFF86909C), fontSize: 13),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 选择日期
|
||||||
|
Future<void> _onSelectDate() async {
|
||||||
|
final DateTime? picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: DateTime.now(),
|
||||||
|
firstDate: DateTime(2000),
|
||||||
|
lastDate: DateTime(2100),
|
||||||
|
builder:
|
||||||
|
(context, child) => Theme(
|
||||||
|
data: ThemeData.light().copyWith(
|
||||||
|
primaryColor: Theme.of(context).primaryColor,
|
||||||
|
colorScheme: ColorScheme.light(
|
||||||
|
primary: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
buttonTheme: const ButtonThemeData(
|
||||||
|
textTheme: ButtonTextTheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: child!,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (picked != null) {
|
||||||
|
_formKey.currentState?.fields[_dateField]?.didChange(
|
||||||
|
DateFormat('yyyy-MM-dd').format(picked),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 选择图片(限制单张)
|
||||||
|
Future<void> _pickImage() async {
|
||||||
|
final XFile? image = await _picker.pickImage(source: ImageSource.gallery);
|
||||||
|
if (image != null) {
|
||||||
|
setState(() {
|
||||||
|
imageUrl = File(image.path); // 直接覆盖现有图片
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 移除图片
|
||||||
|
void _removeImage() {
|
||||||
|
setState(() {
|
||||||
|
imageUrl = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 提交表单
|
||||||
|
void _submitForm() {
|
||||||
|
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||||
|
if (imageUrl == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('请上传图片'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final formData = {
|
||||||
|
..._formKey.currentState!.value,
|
||||||
|
'imageUrl': imageUrl?.path, // 单张图片路径
|
||||||
|
};
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('提交成功!'),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
print('表单数据: $formData');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:graphic/graphic.dart';
|
import 'package:food_hub_app/apis/stats.dart';
|
||||||
|
import 'package:food_hub_app/models/stats.dart';
|
||||||
import 'data.dart';
|
import 'package:food_hub_app/widgets/common/chart.dart';
|
||||||
|
import 'package:food_hub_app/widgets/stats/card.dart';
|
||||||
|
|
||||||
class StatsPage extends StatefulWidget {
|
class StatsPage extends StatefulWidget {
|
||||||
const StatsPage({super.key});
|
const StatsPage({super.key});
|
||||||
@@ -11,86 +12,93 @@ class StatsPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _StatsPage extends State<StatsPage> {
|
class _StatsPage extends State<StatsPage> {
|
||||||
var _smooth = false;
|
final double chartHeight = 400;
|
||||||
var _stepped = false;
|
|
||||||
|
SummaryStats summaryStats = SummaryStats(
|
||||||
|
recipeCount: 0,
|
||||||
|
categoryCount: 0,
|
||||||
|
workCount: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
late double averageRecordCount = 0;
|
||||||
|
|
||||||
|
List<ChartData> recordStats = [];
|
||||||
|
List<ChartData> categoryStats = [];
|
||||||
|
List<ChartData> rankStats = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
refreshStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshStats() async {
|
||||||
|
final result1 = await queryStatsApi();
|
||||||
|
final result2 = await queryRecordStatsApi();
|
||||||
|
final result3 = await queryCategoryStatsApi();
|
||||||
|
final result4 = await queryRankStatsApi();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
summaryStats = result1;
|
||||||
|
recordStats = result2;
|
||||||
|
categoryStats = result3;
|
||||||
|
rankStats = result4;
|
||||||
|
|
||||||
|
if (recordStats.isNotEmpty) {
|
||||||
|
double sumValue = recordStats.fold(0.0, (sum, item) => sum + item.value);
|
||||||
|
averageRecordCount = sumValue / recordStats.length;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Center(
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(5),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: [
|
||||||
Container(
|
_buildSummaryStats(),
|
||||||
margin: const EdgeInsets.only(top: 10),
|
SizedBox(
|
||||||
width: 350,
|
height: chartHeight,
|
||||||
height: 300,
|
child: Card(
|
||||||
child: Chart(
|
color: Colors.white,
|
||||||
data: basicData,
|
elevation: 0,
|
||||||
variables: {
|
shape: RoundedRectangleBorder(
|
||||||
'genre': Variable(
|
borderRadius: BorderRadius.circular(10),
|
||||||
accessor: (Map map) => map['genre'] as String,
|
|
||||||
),
|
),
|
||||||
'sold': Variable(accessor: (Map map) => map['sold'] as num),
|
child: Padding(
|
||||||
},
|
padding: const EdgeInsets.all(10),
|
||||||
marks: [
|
child: _buildRecordStats(),
|
||||||
IntervalMark(
|
|
||||||
label: LabelEncode(
|
|
||||||
encoder: (tuple) => Label(tuple['sold'].toString()),
|
|
||||||
),
|
|
||||||
elevation: ElevationEncode(
|
|
||||||
value: 0,
|
|
||||||
updaters: {
|
|
||||||
'tap': {true: (_) => 5},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
color: ColorEncode(
|
|
||||||
value: Defaults.primaryColor,
|
|
||||||
updaters: {
|
|
||||||
'tap': {false: (color) => color.withAlpha(100)},
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
axes: [Defaults.horizontalAxis, Defaults.verticalAxis],
|
SizedBox(
|
||||||
selections: {'tap': PointSelection(dim: Dim.x)},
|
height: chartHeight,
|
||||||
tooltip: TooltipGuide(),
|
child: Card(
|
||||||
crosshair: CrosshairGuide(),
|
color: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: _buildCategoryStats(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.fromLTRB(20, 40, 20, 5),
|
|
||||||
child: const Text(
|
|
||||||
'Transposed Bar Chart',
|
|
||||||
style: TextStyle(fontSize: 20),
|
|
||||||
),
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: chartHeight,
|
||||||
|
child: Card(
|
||||||
|
color: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
Container(
|
child: Padding(
|
||||||
margin: const EdgeInsets.only(top: 10),
|
padding: const EdgeInsets.all(10),
|
||||||
width: 350,
|
child: _buildRankStats(),
|
||||||
height: 300,
|
|
||||||
child: Chart(
|
|
||||||
data: basicData,
|
|
||||||
variables: {
|
|
||||||
'genre': Variable(
|
|
||||||
accessor: (Map map) => map['genre'] as String,
|
|
||||||
),
|
),
|
||||||
'sold': Variable(accessor: (Map map) => map['sold'] as num),
|
|
||||||
},
|
|
||||||
transforms: [Proportion(variable: 'sold', as: 'percent')],
|
|
||||||
marks: [
|
|
||||||
IntervalMark(
|
|
||||||
position: Varset('percent') / Varset('genre'),
|
|
||||||
label: LabelEncode(
|
|
||||||
encoder: (tuple) => Label(tuple['sold'].toString()),
|
|
||||||
),
|
|
||||||
color: ColorEncode(
|
|
||||||
variable: 'genre',
|
|
||||||
values: Defaults.colors10,
|
|
||||||
),
|
|
||||||
modifiers: [StackModifier()],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
coord: PolarCoord(transposed: true, dimCount: 1, dimFill: 1.05),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -98,4 +106,112 @@ class _StatsPage extends State<StatsPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildSummaryStats() {
|
||||||
|
return GridView.count(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
// 禁用网格自身滚动
|
||||||
|
crossAxisCount: 2,
|
||||||
|
// crossAxisSpacing: 5,
|
||||||
|
// mainAxisSpacing: 5,
|
||||||
|
childAspectRatio: 2,
|
||||||
|
children: [
|
||||||
|
StatisticCard(
|
||||||
|
icon: Icons.restaurant_menu,
|
||||||
|
color: Colors.blue,
|
||||||
|
title: '菜谱总数',
|
||||||
|
value: summaryStats.recipeCount,
|
||||||
|
unit: '个',
|
||||||
|
),
|
||||||
|
StatisticCard(
|
||||||
|
icon: Icons.grid_view_rounded,
|
||||||
|
color: Colors.green,
|
||||||
|
title: '菜谱类别',
|
||||||
|
value: summaryStats.categoryCount,
|
||||||
|
unit: '种',
|
||||||
|
),
|
||||||
|
StatisticCard(
|
||||||
|
icon: Icons.flag,
|
||||||
|
color: Colors.orange,
|
||||||
|
title: '做菜次数',
|
||||||
|
value: summaryStats.workCount,
|
||||||
|
unit: '个',
|
||||||
|
),
|
||||||
|
StatisticCard(
|
||||||
|
icon: Icons.show_chart,
|
||||||
|
color: Colors.red,
|
||||||
|
title: '平均次数',
|
||||||
|
value: double.parse(averageRecordCount.toStringAsFixed(2)),
|
||||||
|
unit: '次/月',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRecordStats() {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildTitleSection('记录统计'),
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
_buildDivider(),
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
Expanded(
|
||||||
|
child: lineChart(
|
||||||
|
context: context,
|
||||||
|
xAxisName: '日期',
|
||||||
|
yAxisName: '次数',
|
||||||
|
unit: '次',
|
||||||
|
data: recordStats,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCategoryStats() {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildTitleSection('菜谱统计'),
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
_buildDivider(),
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
Expanded(
|
||||||
|
child: pieChart(context: context, unit: '个', data: categoryStats),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRankStats() {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildTitleSection('排行榜'),
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
_buildDivider(),
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
Expanded(child: rankChart(rankStats)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建标题区域
|
||||||
|
Widget _buildTitleSection(String title) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.insert_chart, color: Theme.of(context).primaryColor),
|
||||||
|
Text(title),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDivider() {
|
||||||
|
return Divider(
|
||||||
|
height: 1,
|
||||||
|
thickness: 1,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
indent: 0,
|
||||||
|
endIndent: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
183
lib/widgets/common/chart.dart
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
102
lib/widgets/common/form.dart
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// 通用输入框样式
|
||||||
|
InputDecoration buildInputDecoration({
|
||||||
|
required BuildContext context,
|
||||||
|
required String hintText,
|
||||||
|
Widget? prefixIcon,
|
||||||
|
}) {
|
||||||
|
return InputDecoration(
|
||||||
|
hintText: hintText,
|
||||||
|
hintStyle: const TextStyle(
|
||||||
|
color: Color(0xFF999999),
|
||||||
|
fontSize: 15,
|
||||||
|
height: 1.2,
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFE5E7EB)),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 1.5),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderSide: const BorderSide(color: Colors.red, width: 1.5),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 14),
|
||||||
|
errorStyle: const TextStyle(fontSize: 12, height: 1, color: Colors.red),
|
||||||
|
prefixIcon: prefixIcon,
|
||||||
|
prefixIconConstraints: const BoxConstraints(minWidth: 40),
|
||||||
|
isDense: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildFormLabel(String text, {bool required = false}) {
|
||||||
|
return RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
text: text,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFF1D2129),
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontFamily: 'CustomFont',
|
||||||
|
height: 1.2,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
if (required)
|
||||||
|
const TextSpan(
|
||||||
|
text: ' *',
|
||||||
|
style: TextStyle(color: Colors.red, fontSize: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 表单底部按钮组组件
|
||||||
|
Widget buildFormButtonGroup({
|
||||||
|
required BuildContext context,
|
||||||
|
required VoidCallback onConfirm,
|
||||||
|
}) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildCancelButton(context)),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(child: _buildSubmitButton(context, onConfirm)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 取消按钮
|
||||||
|
Widget _buildCancelButton(BuildContext context) {
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(context), // 直接使用传入的context返回
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: const Color(0xFF4E5969),
|
||||||
|
side: const BorderSide(color: Color(0xFFDCDFE6)),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: const Text('取消'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 提交按钮
|
||||||
|
Widget _buildSubmitButton(BuildContext context, VoidCallback onConfirm) {
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: onConfirm,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Theme.of(context).primaryColor, // 使用传入的context获取主题
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: const Text('提交', style: TextStyle(color: Colors.white)),
|
||||||
|
);
|
||||||
|
}
|
||||||
115
lib/widgets/common/image.dart
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
import 'package:photo_view/photo_view_gallery.dart';
|
||||||
|
|
||||||
|
class ImagePreviewPage extends StatefulWidget {
|
||||||
|
final List<String> images;
|
||||||
|
final int initialIndex;
|
||||||
|
|
||||||
|
const ImagePreviewPage({
|
||||||
|
super.key,
|
||||||
|
required this.images,
|
||||||
|
required this.initialIndex,
|
||||||
|
}) : assert(initialIndex >= 0 && initialIndex < images.length);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ImagePreviewPage> createState() => _ImagePreviewPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImagePreviewPageState extends State<ImagePreviewPage> {
|
||||||
|
// 声明 PageController 并初始化初始索引
|
||||||
|
late PageController _pageController;
|
||||||
|
// 记录当前显示的图片索引(用于更新页码)
|
||||||
|
int _currentIndex = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// 初始化控制器,设置初始页面
|
||||||
|
_pageController = PageController(initialPage: widget.initialIndex);
|
||||||
|
// 初始化当前索引为初始索引
|
||||||
|
_currentIndex = widget.initialIndex;
|
||||||
|
|
||||||
|
// 监听页面切换事件
|
||||||
|
_pageController.addListener(() {
|
||||||
|
// 取当前页面的整数索引(避免滑动过程中的小数)
|
||||||
|
final currentPage = _pageController.page?.round() ?? 0;
|
||||||
|
// 只有当索引变化时才更新状态
|
||||||
|
if (currentPage != _currentIndex) {
|
||||||
|
setState(() {
|
||||||
|
_currentIndex = currentPage;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pageController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.black54,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.close, color: Colors.white),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
// 显示实时更新的页码(当前索引+1 / 总数量)
|
||||||
|
title: Text(
|
||||||
|
'${_currentIndex + 1}/${widget.images.length}',
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: PhotoViewGallery(
|
||||||
|
pageOptions: widget.images.map((url) {
|
||||||
|
return PhotoViewGalleryPageOptions(
|
||||||
|
imageProvider: NetworkImage(url),
|
||||||
|
minScale: PhotoViewComputedScale.contained,
|
||||||
|
maxScale: PhotoViewComputedScale.covered * 2,
|
||||||
|
// 点击空白处关闭预览
|
||||||
|
onTapDown: (context, details, controllerValue) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
pageController: _pageController,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget networkImage(String url) {
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Image.network(
|
||||||
|
url,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
loadingBuilder: (context, child, loadingProgress) {
|
||||||
|
// 加载中显示占位符
|
||||||
|
if (loadingProgress == null) return child;
|
||||||
|
return Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: loadingProgress.expectedTotalBytes != null
|
||||||
|
? loadingProgress.cumulativeBytesLoaded /
|
||||||
|
loadingProgress.expectedTotalBytes!
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
child: const Icon(Icons.image, color: Colors.grey, size: 30),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -27,6 +27,38 @@ InputDecoration formInputDecoration({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget cardContainer(Widget content) {
|
||||||
|
return Card(
|
||||||
|
elevation: 0,
|
||||||
|
color: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: content,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 圆形图标按钮
|
||||||
|
Widget circleIconButton({
|
||||||
|
required IconData icon,
|
||||||
|
required VoidCallback onPressed,
|
||||||
|
required BuildContext context,
|
||||||
|
}) {
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
fixedSize: Size(32, 32),
|
||||||
|
shape: CircleBorder(),
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
minimumSize: const Size(0, 0),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: Colors.white, size: 18),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// 确认按钮样式
|
/// 确认按钮样式
|
||||||
ButtonStyle primaryButtonStyle() {
|
ButtonStyle primaryButtonStyle() {
|
||||||
return ButtonStyle(
|
return ButtonStyle(
|
||||||
@@ -51,18 +83,13 @@ Text buttonText({required String text}) {
|
|||||||
return Text(text, style: TextStyle(color: Colors.white));
|
return Text(text, style: TextStyle(color: Colors.white));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 图片错误显示
|
||||||
Widget errorImageContainer(double height) {
|
Widget errorImageContainer(double height) {
|
||||||
return Container(
|
return Container(
|
||||||
height: height,
|
height: 200,
|
||||||
|
width: double.infinity,
|
||||||
color: Colors.grey[200],
|
color: Colors.grey[200],
|
||||||
child: Row(
|
child: const Icon(Icons.image, color: Colors.grey, size: 30),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.error, color: Colors.red),
|
|
||||||
const SizedBox(width: 5),
|
|
||||||
const Text("加载失败", style: TextStyle(color: Colors.red)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
119
lib/widgets/common/year_selector.dart
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
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,
|
||||||
|
onPressed: () => _previousYear(),
|
||||||
|
),
|
||||||
|
// 年份显示
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: _scaleAnimation,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Transform.scale(scale: _scaleAnimation.value, child: child);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'$_currentYear',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
circleIconButton(
|
||||||
|
context: context,
|
||||||
|
icon: Icons.chevron_right,
|
||||||
|
onPressed: () => _nextYear(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
216
lib/widgets/moment/card.dart
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import 'package:flutter/material.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:tdesign_flutter/tdesign_flutter.dart';
|
||||||
|
|
||||||
|
class MomentCard extends StatelessWidget {
|
||||||
|
final Moment moment;
|
||||||
|
|
||||||
|
const MomentCard({super.key, required this.moment});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
elevation: 0,
|
||||||
|
color: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildAvatar(context),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildNickname(),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
_buildContent(),
|
||||||
|
if (moment.imageList.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildPostImages(context),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_buildTime(),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
_buildActionButtons(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建头像组件
|
||||||
|
Widget _buildAvatar(BuildContext context) {
|
||||||
|
if (moment.avatar!.isEmpty) {
|
||||||
|
return TDAvatar(
|
||||||
|
size: TDAvatarSize.medium,
|
||||||
|
type: TDAvatarType.customText,
|
||||||
|
shape: TDAvatarShape.circle,
|
||||||
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
|
text: moment.username?[0],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return TDAvatar(
|
||||||
|
size: TDAvatarSize.medium,
|
||||||
|
type: TDAvatarType.normal,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
avatarUrl: '${AppConfig.baseApiUrl}/${moment.avatar}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建昵称组件
|
||||||
|
Widget _buildNickname() {
|
||||||
|
return Text(
|
||||||
|
moment.username ?? "",
|
||||||
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建内容组件
|
||||||
|
Widget _buildContent() {
|
||||||
|
return Text(
|
||||||
|
moment.content,
|
||||||
|
style: const TextStyle(fontSize: 15, height: 1.3),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建朋友圈图片列表(网格布局)
|
||||||
|
Widget _buildPostImages(BuildContext context) {
|
||||||
|
final imageCount = moment.imageList.length;
|
||||||
|
|
||||||
|
// 计算网格列数
|
||||||
|
int crossAxisCount;
|
||||||
|
if (imageCount == 1) {
|
||||||
|
crossAxisCount = 1;
|
||||||
|
} else if (imageCount == 2 || imageCount == 4) {
|
||||||
|
crossAxisCount = 2;
|
||||||
|
} else {
|
||||||
|
crossAxisCount = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算宽高比
|
||||||
|
double itemAspectRatio = 1.0;
|
||||||
|
if (imageCount == 1) {
|
||||||
|
itemAspectRatio = 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成图片URL列表(用于预览时切换)
|
||||||
|
final List<String> imageUrls =
|
||||||
|
moment.imageList
|
||||||
|
.map((path) => '${AppConfig.baseApiUrl}/$path')
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
void imageTapClick(int index) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder:
|
||||||
|
(context) =>
|
||||||
|
ImagePreviewPage(images: imageUrls, initialIndex: index),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GridView.count(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
crossAxisCount: crossAxisCount,
|
||||||
|
crossAxisSpacing: 4,
|
||||||
|
mainAxisSpacing: 4,
|
||||||
|
childAspectRatio: itemAspectRatio,
|
||||||
|
children: List.generate(imageCount, (index) {
|
||||||
|
// 单个图片项:添加点击事件
|
||||||
|
return GestureDetector(
|
||||||
|
// 点击图片时,跳转到预览页面
|
||||||
|
onTap: () => imageTapClick(index),
|
||||||
|
// 原图片组件
|
||||||
|
child: networkImage(imageUrls[index]),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建时间组件
|
||||||
|
Widget _buildTime() {
|
||||||
|
return Text(
|
||||||
|
moment.date ?? "",
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建点赞和评论按钮
|
||||||
|
Widget _buildActionButtons() {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 42,
|
||||||
|
height: 36,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.bottomLeft,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.thumb_up_alt_outlined),
|
||||||
|
Positioned(
|
||||||
|
left: 20,
|
||||||
|
bottom: 15,
|
||||||
|
child: TDBadge(
|
||||||
|
TDBadgeType.message,
|
||||||
|
count: (moment.likeList?.length ?? 0).toString(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 42,
|
||||||
|
height: 36,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.bottomLeft,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.comment_outlined),
|
||||||
|
Positioned(
|
||||||
|
left: 20,
|
||||||
|
bottom: 15,
|
||||||
|
child: TDBadge(
|
||||||
|
TDBadgeType.message,
|
||||||
|
count: (moment.commentList?.length ?? 0).toString(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// _buildActionButton(
|
||||||
|
// icon: Icons.thumb_up_alt_outlined,
|
||||||
|
// count: moment.likeList?.length ?? 0,
|
||||||
|
// ),
|
||||||
|
// const SizedBox(width: 20),
|
||||||
|
// _buildActionButton(
|
||||||
|
// icon: Icons.comment_outlined,
|
||||||
|
// count: moment.commentList?.length ?? 0,
|
||||||
|
// ),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建带数字标记的动作按钮
|
||||||
|
Widget _buildActionButton({required IconData icon, required int count}) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: Colors.grey[500], size: 18),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
if (count > 0)
|
||||||
|
Text(
|
||||||
|
count.toString(),
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:food_hub_app/api/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/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/widgets/common/index.dart';
|
||||||
import 'package:table_calendar/table_calendar.dart';
|
import 'package:table_calendar/table_calendar.dart';
|
||||||
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
||||||
|
|
||||||
@@ -145,12 +146,10 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: 10),
|
circleIconButton(
|
||||||
TDButton(
|
context: context,
|
||||||
icon: TDIcons.arrow_right,
|
icon: Icons.chevron_right,
|
||||||
type: TDButtonType.fill,
|
onPressed: () => {}
|
||||||
shape: TDButtonShape.circle,
|
|
||||||
theme: TDButtonTheme.primary,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -171,9 +170,9 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color:
|
color:
|
||||||
isSelected
|
isSelected
|
||||||
? Colors.blue
|
? Theme.of(context).primaryColor
|
||||||
: isToday
|
: isToday
|
||||||
? Colors.grey[200]
|
? Colors.grey[300]
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
@@ -184,7 +183,7 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
|
|||||||
// 日期数字
|
// 日期数字
|
||||||
Text(
|
Text(
|
||||||
day.day.toString(),
|
day.day.toString(),
|
||||||
style: TextStyle(color: isSelected ? Colors.white : Colors.black87),
|
style: TextStyle(color: isSelected ? Colors.white : Colors.black),
|
||||||
),
|
),
|
||||||
// 底部红点 - 只在指定日期显示
|
// 底部红点 - 只在指定日期显示
|
||||||
if (shouldShowRedDot)
|
if (shouldShowRedDot)
|
||||||
@@ -207,7 +206,6 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
|
|||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Card(elevation: 0, color: Colors.white, child: recipeCalendar()),
|
Card(elevation: 0, color: Colors.white, child: recipeCalendar()),
|
||||||
const SizedBox(height: 10),
|
|
||||||
Expanded(child: dailyItem()),
|
Expanded(child: dailyItem()),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -1,30 +1,68 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_carousel_widget/flutter_carousel_widget.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/widgets/common/image.dart';
|
||||||
import 'package:food_hub_app/widgets/common/index.dart';
|
import 'package:food_hub_app/widgets/common/index.dart';
|
||||||
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
|
||||||
|
|
||||||
class RecipeCard extends StatelessWidget {
|
class RecipeCard extends StatelessWidget {
|
||||||
final Recipe recipe;
|
final RecipeSummary recipe;
|
||||||
|
|
||||||
const RecipeCard({super.key, required this.recipe});
|
const RecipeCard({super.key, required this.recipe});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final List<String> imageUrls =
|
||||||
|
recipe.recordList
|
||||||
|
.map((item) => '${AppConfig.baseApiUrl}/${item.imageUrl}')
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Widget buildCarouselItem(String url) {
|
||||||
|
return Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AspectRatio(
|
||||||
|
aspectRatio: 2,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: networkImage(url),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildRecipeCarousel() {
|
||||||
|
return FlutterCarousel(
|
||||||
|
// 轮播项
|
||||||
|
items:
|
||||||
|
imageUrls.map((url) {
|
||||||
|
return buildCarouselItem(url);
|
||||||
|
}).toList(),
|
||||||
|
// 轮播配置
|
||||||
|
options: FlutterCarouselOptions(
|
||||||
|
height: 300,
|
||||||
|
autoPlay: imageUrls.length > 1,
|
||||||
|
enableInfiniteScroll: imageUrls.length > 1,
|
||||||
|
autoPlayInterval: const Duration(seconds: 3),
|
||||||
|
viewportFraction: 0.9,
|
||||||
|
showIndicator: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
side: BorderSide(color: Theme.of(context).primaryColor, width: 1.0),
|
||||||
|
borderRadius: BorderRadius.circular(10.0),
|
||||||
|
),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Image.network(
|
buildRecipeCarousel(),
|
||||||
'http://172.29.101.108:8100/${recipe.recordList[0].imageUrl}',
|
|
||||||
height: 200,
|
|
||||||
width: double.infinity,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
errorBuilder: (context, error, stackTrace) => errorImageContainer(200),
|
|
||||||
),
|
|
||||||
Divider(
|
Divider(
|
||||||
height: 1,
|
height: 1,
|
||||||
thickness: 1,
|
thickness: 1,
|
||||||
@@ -58,22 +96,20 @@ class RecipeCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
TDButton(
|
circleIconButton(
|
||||||
|
context: context,
|
||||||
icon: Icons.edit,
|
icon: Icons.edit,
|
||||||
size: TDButtonSize.small,
|
onPressed: () => {},
|
||||||
type: TDButtonType.fill,
|
|
||||||
shape: TDButtonShape.circle,
|
|
||||||
theme: TDButtonTheme.primary,
|
|
||||||
onTap: () => {},
|
|
||||||
),
|
),
|
||||||
SizedBox(width: 10),
|
circleIconButton(
|
||||||
TDButton(
|
context: context,
|
||||||
icon: Icons.book,
|
icon: Icons.book,
|
||||||
size: TDButtonSize.small,
|
onPressed:
|
||||||
type: TDButtonType.fill,
|
() => Navigator.pushNamed(
|
||||||
shape: TDButtonShape.circle,
|
context,
|
||||||
theme: TDButtonTheme.primary,
|
'/recipeDetail',
|
||||||
onTap: () => {},
|
arguments: {'id': recipe.id},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:food_hub_app/api/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/widgets/recipe/recipe_card.dart';
|
import 'package:food_hub_app/widgets/recipe/card.dart';
|
||||||
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
||||||
|
|
||||||
class RecipeList extends StatefulWidget {
|
class RecipeList extends StatefulWidget {
|
||||||
const RecipeList({super.key});
|
const RecipeList({super.key});
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<RecipeList> createState() => _RecipeListState();
|
State<RecipeList> createState() => _RecipeListState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RecipeListState extends State<RecipeList> {
|
class _RecipeListState extends State<RecipeList> {
|
||||||
List<Recipe> recipeList = [];
|
List<RecipeSummary> recipeSummaryList = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -25,23 +24,20 @@ class _RecipeListState extends State<RecipeList> {
|
|||||||
final result = await queryRecipeApi(RecipeQuery(category: ""));
|
final result = await queryRecipeApi(RecipeQuery(category: ""));
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
recipeList = result;
|
recipeSummaryList = result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (recipeList.isEmpty) {
|
if (recipeSummaryList.isEmpty) {
|
||||||
return const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据');
|
return const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据');
|
||||||
} else {
|
} else {
|
||||||
return ListView.separated(
|
return ListView.builder(
|
||||||
itemCount: recipeList.length,
|
itemCount: recipeSummaryList.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return RecipeCard(recipe: recipeList[index]);
|
return RecipeCard(recipe: recipeSummaryList[index]);
|
||||||
},
|
}
|
||||||
separatorBuilder: (context, index) {
|
|
||||||
return const SizedBox(height: 10);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:food_hub_app/api/recipe.dart';
|
|
||||||
import 'package:food_hub_app/models/recipe.dart';
|
|
||||||
import 'package:food_hub_app/widgets/common/index.dart';
|
|
||||||
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
|
||||||
import 'package:timelines_plus/timelines_plus.dart';
|
|
||||||
|
|
||||||
class RecipeTimeline extends StatefulWidget {
|
|
||||||
const RecipeTimeline({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() => _RecipeTimeline();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RecipeTimeline extends State<RecipeTimeline> {
|
|
||||||
List<Record> recordList = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
refreshRecord();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> refreshRecord() async {
|
|
||||||
final result = await queryRecordApi("2025-01-01", "2025-07-08");
|
|
||||||
setState(() {
|
|
||||||
recordList = result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
YearSelector(
|
|
||||||
initialYear: DateTime.now().year,
|
|
||||||
minYear: 2000,
|
|
||||||
maxYear: 2100,
|
|
||||||
onYearChanged: (year) {
|
|
||||||
print('选中的年份: $year');
|
|
||||||
},
|
|
||||||
accentColor: Colors.blue,
|
|
||||||
),
|
|
||||||
Expanded(child: TimelineContainer(recordList))
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget TimelineContainer(List<Record> recordList) {
|
|
||||||
if (recordList.isEmpty) {
|
|
||||||
return const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据');
|
|
||||||
} else {
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.all(10),
|
|
||||||
child: Timeline.tileBuilder(
|
|
||||||
theme: TimelineThemeData(nodePosition: 0, indicatorPosition: 0),
|
|
||||||
builder: TimelineTileBuilder.connected(
|
|
||||||
itemCount: recordList.length,
|
|
||||||
connectorBuilder:
|
|
||||||
(context, index, type) => Connector.solidLine(thickness: 2),
|
|
||||||
indicatorBuilder: (context, index) {
|
|
||||||
return Indicator.dot(size: 12.0);
|
|
||||||
},
|
|
||||||
contentsBuilder: (context, index) {
|
|
||||||
return TimelineCard(record: recordList[index]);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TimelineCard extends StatelessWidget {
|
|
||||||
final Record record;
|
|
||||||
|
|
||||||
const TimelineCard({super.key, required this.record});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 10, bottom: 5),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
record.date,
|
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
Card(
|
|
||||||
elevation: 0,
|
|
||||||
color: Colors.white,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(10),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(record.name, style: TextStyle(fontSize: 16)),
|
|
||||||
const SizedBox(height: 5),
|
|
||||||
Image.network(
|
|
||||||
'http://172.29.101.108:8100/${record.imageUrl}',
|
|
||||||
width: double.infinity,
|
|
||||||
height: 250,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
errorBuilder:
|
|
||||||
(context, error, stackTrace) =>
|
|
||||||
errorImageContainer(250),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class YearSelector extends StatefulWidget {
|
|
||||||
final int initialYear;
|
|
||||||
final int? minYear;
|
|
||||||
final int? maxYear;
|
|
||||||
final Function(int) onYearChanged;
|
|
||||||
final Color? accentColor;
|
|
||||||
|
|
||||||
const YearSelector({
|
|
||||||
super.key,
|
|
||||||
required this.initialYear,
|
|
||||||
required this.onYearChanged,
|
|
||||||
this.minYear,
|
|
||||||
this.maxYear,
|
|
||||||
this.accentColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<YearSelector> createState() => _YearSelectorState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _YearSelectorState extends State<YearSelector>
|
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
late int _currentYear;
|
|
||||||
late Color _accentColor;
|
|
||||||
|
|
||||||
// 用于动画效果
|
|
||||||
late AnimationController _animationController;
|
|
||||||
late Animation<double> _scaleAnimation;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_currentYear = widget.initialYear;
|
|
||||||
_accentColor = widget.accentColor ?? Theme
|
|
||||||
.of(context)
|
|
||||||
.primaryColor;
|
|
||||||
|
|
||||||
// 初始化动画控制器
|
|
||||||
_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: [
|
|
||||||
TDButton(
|
|
||||||
icon: Icons.chevron_left,
|
|
||||||
size: TDButtonSize.small,
|
|
||||||
type: TDButtonType.fill,
|
|
||||||
shape: TDButtonShape.circle,
|
|
||||||
theme: TDButtonTheme.primary,
|
|
||||||
onTap: () => _previousYear(),
|
|
||||||
),
|
|
||||||
SizedBox(width: 10),
|
|
||||||
// 年份显示
|
|
||||||
AnimatedBuilder(
|
|
||||||
animation: _scaleAnimation,
|
|
||||||
builder: (context, child) {
|
|
||||||
return Transform.scale(scale: _scaleAnimation.value, child: child);
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
'$_currentYear',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 28,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: _accentColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TDButton(
|
|
||||||
icon: Icons.chevron_right,
|
|
||||||
size: TDButtonSize.small,
|
|
||||||
type: TDButtonType.fill,
|
|
||||||
shape: TDButtonShape.circle,
|
|
||||||
theme: TDButtonTheme.primary,
|
|
||||||
onTap: () => _nextYear(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
135
lib/widgets/recipe/timeline.dart
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:food_hub_app/apis/recipe.dart';
|
||||||
|
import 'package:food_hub_app/config/app_config.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/year_selector.dart';
|
||||||
|
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
||||||
|
import 'package:timelines_plus/timelines_plus.dart';
|
||||||
|
|
||||||
|
class RecipeTimeline extends StatefulWidget {
|
||||||
|
const RecipeTimeline({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _RecipeTimeline();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecipeTimeline extends State<RecipeTimeline> {
|
||||||
|
List<Record> recordList = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
refreshRecord(DateTime.now().year);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshRecord(int year) async {
|
||||||
|
final result = await queryRecordApi("$year-01-01", "$year-12-31");
|
||||||
|
setState(() {
|
||||||
|
recordList = result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
YearSelector(
|
||||||
|
initialYear: DateTime.now().year,
|
||||||
|
minYear: 2000,
|
||||||
|
maxYear: 2100,
|
||||||
|
onYearChanged: (year) => refreshRecord(year),
|
||||||
|
),
|
||||||
|
Expanded(child: timelineContainer(recordList)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget timelineContainer(List<Record> recordList) {
|
||||||
|
if (recordList.isEmpty) {
|
||||||
|
return const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据');
|
||||||
|
} else {
|
||||||
|
return Timeline.tileBuilder(
|
||||||
|
theme: TimelineThemeData(nodePosition: 0, indicatorPosition: 0),
|
||||||
|
builder: TimelineTileBuilder.connected(
|
||||||
|
itemCount: recordList.length,
|
||||||
|
connectorBuilder:
|
||||||
|
(context, index, type) => Connector.solidLine(
|
||||||
|
thickness: 2,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
indicatorBuilder: (context, index) {
|
||||||
|
return Indicator.dot(
|
||||||
|
size: 12.0,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
contentsBuilder: (context, index) {
|
||||||
|
return TimelineCard(record: recordList[index]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimelineCard extends StatelessWidget {
|
||||||
|
final Record record;
|
||||||
|
|
||||||
|
const TimelineCard({super.key, required this.record});
|
||||||
|
|
||||||
|
Widget cardContent(BuildContext context) {
|
||||||
|
final imageUrls = ['${AppConfig.baseApiUrl}/${record.imageUrl}'];
|
||||||
|
|
||||||
|
void imageTapClick() {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder:
|
||||||
|
(context) => ImagePreviewPage(images: imageUrls, initialIndex: 0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
elevation: 0,
|
||||||
|
color: Colors.white,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(10),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(record.name, style: TextStyle(fontSize: 16)),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => imageTapClick(),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1.5,
|
||||||
|
child: networkImage(imageUrls[0]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 10, bottom: 5),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
record.date,
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
cardContent(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
115
lib/widgets/stats/card.dart
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:food_hub_app/models/stats.dart';
|
||||||
|
|
||||||
|
class StatisticCard extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final Color color;
|
||||||
|
final String title;
|
||||||
|
final num value;
|
||||||
|
final String unit;
|
||||||
|
|
||||||
|
const StatisticCard({
|
||||||
|
super.key,
|
||||||
|
required this.icon,
|
||||||
|
required this.color,
|
||||||
|
required this.title,
|
||||||
|
required this.value,
|
||||||
|
required this.unit,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
color: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
child: Icon(icon, color: color, size: 40),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(title, style: TextStyle(fontSize: 20, color: color)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value.toString(),
|
||||||
|
style: TextStyle(fontSize: 20, color: color),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
Text(unit, style: TextStyle(fontSize: 20, color: color)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取排名对应的颜色
|
||||||
|
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(List<ChartData> items) {
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: items.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = items[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)} 次',
|
||||||
|
style: TextStyle(color: _getRankColor(index)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
42
pubspec.lock
@@ -202,7 +202,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.1"
|
||||||
easy_refresh:
|
easy_refresh:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: easy_refresh
|
name: easy_refresh
|
||||||
sha256: "486e30abfcaae66c0f2c2798a10de2298eb9dc5e0bb7e1dba9328308968cae0c"
|
sha256: "486e30abfcaae66c0f2c2798a10de2298eb9dc5e0bb7e1dba9328308968cae0c"
|
||||||
@@ -278,6 +278,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_carousel_widget:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_carousel_widget
|
||||||
|
sha256: "6473e6df04bfafea70efd58251fe5945d5aa8d19461575c1b9d83643f08e0c77"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
flutter_form_builder:
|
flutter_form_builder:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -365,14 +373,6 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.1.3"
|
||||||
graphic:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: graphic
|
|
||||||
sha256: af3a5a967d95ce2c2c9f7dee83c21f8bd6e6a458341820920cf15c0311cdaddc
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "2.6.0"
|
|
||||||
graphs:
|
graphs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -645,6 +645,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
photo_view:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: photo_view
|
||||||
|
sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.0"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -826,6 +834,22 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.1"
|
||||||
|
syncfusion_flutter_charts:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: syncfusion_flutter_charts
|
||||||
|
sha256: c58ca79e072680af6f0554f7c4b91886c1d8808f2522b49d557f16fb5cb3bb04
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "30.1.41"
|
||||||
|
syncfusion_flutter_core:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: syncfusion_flutter_core
|
||||||
|
sha256: "536753489e168f49659261d0abd02b101b34c0b1bde27898f2869aab05f1cbb6"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "30.1.41"
|
||||||
table_calendar:
|
table_calendar:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -40,13 +40,16 @@ dependencies:
|
|||||||
table_calendar: ^3.1.3
|
table_calendar: ^3.1.3
|
||||||
flutter_form_builder: ^10.0.0
|
flutter_form_builder: ^10.0.0
|
||||||
form_builder_validators: ^11.1.2
|
form_builder_validators: ^11.1.2
|
||||||
graphic: ^2.6.0
|
|
||||||
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
|
fluttertoast: ^8.2.0
|
||||||
shared_preferences: ^2.3.0
|
shared_preferences: ^2.3.0
|
||||||
logger: ^2.6.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
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
tdesign_flutter_adaptation: 3.16.0
|
tdesign_flutter_adaptation: 3.16.0
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace {
|
|||||||
///
|
///
|
||||||
/// Redefined in case the developer's machine has a Windows SDK older than
|
/// Redefined in case the developer's machine has a Windows SDK older than
|
||||||
/// version 10.0.22000.0.
|
/// version 10.0.22000.0.
|
||||||
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
|
/// See: https://docs.microsoft.com/windows/win32/apis/dwmapi/ne-dwmapi-dwmwindowattribute
|
||||||
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
|
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||||
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
|
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
|
||||||
#endif
|
#endif
|
||||||
|
|||||||