Compare commits
38 Commits
65958d925e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| a5b3053920 | |||
| 780526ddf4 | |||
| 9d3a3f2e80 | |||
| 613e50b0da | |||
| c10e8e5ffb | |||
| a340ba424e | |||
| a5e788eee6 | |||
| ed0ceb29d3 | |||
| 8cff5228ae | |||
| 97e7a6f44b | |||
| 245edd9d27 | |||
| 6308169013 | |||
| 1ac504f3d7 | |||
| 41fe9d6a24 | |||
| 515021781f | |||
| 4f033a4d0a | |||
| a9f2e102c5 | |||
| eb4f7a3637 | |||
| e6ee934a3e | |||
| 5151430132 | |||
| 07180965cc | |||
| 8acad9c63a | |||
| 625a872d2f | |||
| 7ede1c2d89 | |||
| b6995d8dd1 | |||
| 9a8e1a7419 | |||
| df26790389 | |||
| 21ff0ad94f | |||
| 6bba9a488c | |||
| 504ee073ce | |||
| b5dd0ab749 | |||
| 8923d9e63a | |||
| 34780651a8 | |||
| a2ed0f3d98 | |||
| 502bb9df52 | |||
| 7da3ec9507 | |||
| d9f1781277 | |||
| 5d156d3a02 |
15
.metadata
@@ -15,21 +15,6 @@ migration:
|
|||||||
- platform: root
|
- platform: root
|
||||||
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
||||||
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
||||||
- platform: android
|
|
||||||
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
|
||||||
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
|
||||||
- platform: ios
|
|
||||||
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
|
||||||
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
|
||||||
- platform: linux
|
|
||||||
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
|
||||||
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
|
||||||
- platform: macos
|
|
||||||
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
|
||||||
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
|
||||||
- platform: web
|
|
||||||
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
|
||||||
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
|
||||||
- platform: windows
|
- platform: windows
|
||||||
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
||||||
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="food_hub_app"
|
android:label="食光集"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
|
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 |
@@ -27,7 +27,7 @@ pluginManagement {
|
|||||||
plugins {
|
plugins {
|
||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.7.0" apply false
|
id("com.android.application") version "8.7.0" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
|
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|||||||
@@ -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()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
52
lib/apis/moment.dart
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import 'package:flutter_common/utils/convert_utils.dart';
|
||||||
|
import 'package:flutter_common/utils/http_utils.dart';
|
||||||
|
import 'package:food_hub_app/config/app_config.dart';
|
||||||
|
import 'package:food_hub_app/models/moment.dart';
|
||||||
|
|
||||||
|
final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl);
|
||||||
|
|
||||||
|
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) => convertList<Moment>(data, Moment.fromJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Moment>> queryMomentListByUserIdApi(int userId) {
|
||||||
|
return httpUtil.get<List<Moment>>(
|
||||||
|
"/moment/$userId",
|
||||||
|
converter: (data) => convertList<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");
|
||||||
|
}
|
||||||
103
lib/apis/recipe.dart
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import 'package:flutter_common/utils/convert_utils.dart';
|
||||||
|
import 'package:flutter_common/utils/http_utils.dart';
|
||||||
|
import 'package:food_hub_app/config/app_config.dart';
|
||||||
|
import 'package:food_hub_app/models/recipe.dart';
|
||||||
|
import 'package:food_hub_app/utils/index.dart';
|
||||||
|
|
||||||
|
final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl);
|
||||||
|
|
||||||
|
Future<Recipe> queryRecipeByIdApi(int id) {
|
||||||
|
return httpUtil.get<Recipe>(
|
||||||
|
"/food/recipe/$id",
|
||||||
|
converter: (data) => Recipe.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<RecipeSummary>> queryRecipeByUserApi(int id) {
|
||||||
|
return httpUtil.get<List<RecipeSummary>>(
|
||||||
|
"/food/recipe/user/$id",
|
||||||
|
converter: (data) => convertList<RecipeSummary>(data, RecipeSummary.fromJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Recipe>> queryRecipeUserFavouriteApi() {
|
||||||
|
return httpUtil.get<List<Recipe>>(
|
||||||
|
"/food/recipe/user/favourite",
|
||||||
|
converter: (data) => convertList<Recipe>(data, Recipe.fromJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<RecipeSummary>> queryRecipeApi(RecipeQuery recipeQuery) {
|
||||||
|
return httpUtil.get<List<RecipeSummary>>(
|
||||||
|
"/food/recipe",
|
||||||
|
queryParameters: recipeQuery.toJson(),
|
||||||
|
converter:
|
||||||
|
(data) => convertList<RecipeSummary>(data, RecipeSummary.fromJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> queryFoodNameListApi() {
|
||||||
|
return httpUtil.get<List<String>>(
|
||||||
|
"/food/recipe/name",
|
||||||
|
converter: (data) => convertStringList(data),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> addRecordApi(FoodRecord record) {
|
||||||
|
return httpUtil.post<bool>("/food/record", data: record);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> updateRecordApi(int id, FoodRecord record) {
|
||||||
|
return httpUtil.put<bool>("/food/record/$id", data: record);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<FoodRecord>> queryRecordApi(String startDate, String endDate) {
|
||||||
|
return httpUtil.get<List<FoodRecord>>(
|
||||||
|
"/food/record",
|
||||||
|
queryParameters: {"startDate": startDate, "endDate": endDate},
|
||||||
|
converter: (data) => convertList<FoodRecord>(data, FoodRecord.fromJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> queryCategoryApi() {
|
||||||
|
return httpUtil.get<List<String>>(
|
||||||
|
"/food/category",
|
||||||
|
converter: (data) => convertListStringResponse(data),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import 'package:flutter_common/utils/http_utils.dart';
|
||||||
|
import 'package:food_hub_app/config/app_config.dart';
|
||||||
import 'package:food_hub_app/models/session.dart';
|
import 'package:food_hub_app/models/session.dart';
|
||||||
import 'package:food_hub_app/utils/http_util.dart';
|
|
||||||
|
final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl);
|
||||||
|
|
||||||
Future<Session> loginApi(String username, String password) {
|
Future<Session> loginApi(String username, String password) {
|
||||||
return HttpUtil().post<Session>(
|
return httpUtil.post<Session>(
|
||||||
"/food-service/session",
|
"/session",
|
||||||
queryParameters: {"username": username, "password": password},
|
queryParameters: {"username": username, "password": password},
|
||||||
converter: (data) => Session.fromJson(data),
|
converter: (data) => Session.fromJson(data),
|
||||||
);
|
);
|
||||||
35
lib/apis/stats.dart
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import 'package:flutter_common/models/common_model.dart';
|
||||||
|
import 'package:flutter_common/utils/convert_utils.dart';
|
||||||
|
import 'package:flutter_common/utils/http_utils.dart';
|
||||||
|
import 'package:food_hub_app/config/app_config.dart';
|
||||||
|
import 'package:food_hub_app/models/stats.dart';
|
||||||
|
|
||||||
|
final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl);
|
||||||
|
|
||||||
|
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) => convertList<ChartData>(data, ChartData.fromJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ChartData>> queryCategoryStatsApi() {
|
||||||
|
return httpUtil.get<List<ChartData>>(
|
||||||
|
"/food/stats/category",
|
||||||
|
converter: (data) => convertList<ChartData>(data, ChartData.fromJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ChartData>> queryRankStatsApi() {
|
||||||
|
return httpUtil.get<List<ChartData>>(
|
||||||
|
"/food/stats/rank",
|
||||||
|
converter: (data) => convertList<ChartData>(data, ChartData.fromJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
16
lib/apis/user.dart
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:flutter_common/utils/http_utils.dart';
|
||||||
|
import 'package:food_hub_app/config/app_config.dart';
|
||||||
|
import 'package:food_hub_app/models/session.dart';
|
||||||
|
|
||||||
|
final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl);
|
||||||
|
|
||||||
|
Future<User> queryUserApi(int number) {
|
||||||
|
return httpUtil.get<User>(
|
||||||
|
"/user/$number",
|
||||||
|
converter: (data) => User.fromJson(data),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> updateUserApi(int id, User user) {
|
||||||
|
return httpUtil.put<bool>("/user/$id", data: user);
|
||||||
|
}
|
||||||
14
lib/config/app_config.dart
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/// 应用信息
|
||||||
|
class AppConfig {
|
||||||
|
// 网络配置
|
||||||
|
// static const String baseApiUrl = "http://192.168.1.4:8083";
|
||||||
|
static const String baseApiUrl = "https://cxx0822.s.3q.hair/food-api";
|
||||||
|
static const String rustfsIp = '43.248.188.28';
|
||||||
|
static const int rustfsPort = 23125;
|
||||||
|
static const String rustfsAccessKey = 'sZuAg1WCo5i4kD936Tqh';
|
||||||
|
static const String rustfsSecretKey = 'g2fEyk6bslGrHepBvjhu0OLNF4qCRziPJIx1KAS8';
|
||||||
|
static const String rustfsFileUrl = 'http://$rustfsIp:$rustfsPort';
|
||||||
|
static const String bucketName = 'food';
|
||||||
|
static const String imageBaseUrl = '$rustfsFileUrl/$bucketName/';
|
||||||
|
// static const String imageBaseUrl = '$baseApiUrl/';
|
||||||
|
}
|
||||||
105
lib/layout/app_actions.dart
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:food_hub_app/provider/food_provider.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class AppActions extends StatefulWidget {
|
||||||
|
final int pageIndex;
|
||||||
|
|
||||||
|
const AppActions({super.key, required this.pageIndex});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => AppActionsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppActionsState extends State<AppActions> {
|
||||||
|
Widget _buildBottomSheetBody(FoodProvider provider) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
final provider = context.watch<FoodProvider>();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'请选择操作',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
if (widget.pageIndex == 0)
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(Icons.book, color: colors.primary),
|
||||||
|
title: const Text('新增菜谱'),
|
||||||
|
onTap: () => _handleAddRecipe(provider),
|
||||||
|
),
|
||||||
|
if (widget.pageIndex == 0)
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(Icons.note_add, color: colors.primary),
|
||||||
|
title: const Text('新增记录'),
|
||||||
|
onTap: () => _handleAddRecord(provider),
|
||||||
|
),
|
||||||
|
if (widget.pageIndex == 2)
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(Icons.group, color: colors.primary),
|
||||||
|
title: const Text('发布朋友圈'),
|
||||||
|
onTap: () => _handleAddMoment(provider),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPressAdd(FoodProvider provider) {
|
||||||
|
if (widget.pageIndex == 0) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
),
|
||||||
|
builder: (context) => _buildBottomSheetBody(provider),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.pageIndex == 2) {
|
||||||
|
_handleAddMoment(provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleAddRecipe(FoodProvider provider) {
|
||||||
|
// provider.resetRecordForm();
|
||||||
|
Navigator.pop(context);
|
||||||
|
Navigator.pushNamed(context, "/recipeForm");
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleAddRecord(FoodProvider provider) {
|
||||||
|
provider.resetRecordForm();
|
||||||
|
provider.isEditing = false;
|
||||||
|
Navigator.pop(context);
|
||||||
|
Navigator.pushNamed(context, "/recordForm");
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleAddMoment(FoodProvider provider) {
|
||||||
|
provider.resetMomentForm();
|
||||||
|
provider.isEditing = false;
|
||||||
|
Navigator.pushNamed(context, "/momentForm");
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final provider = context.watch<FoodProvider>();
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.search, color: Colors.white),
|
||||||
|
onPressed: () {
|
||||||
|
// 搜索功能
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (widget.pageIndex == 0 || widget.pageIndex == 2)
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.add, color: Colors.white),
|
||||||
|
onPressed: () => _onPressAdd(provider),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
lib/layout/app_drawer.dart
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_common/layout/theme_layout.dart';
|
||||||
|
|
||||||
|
class AppDrawer extends StatefulWidget {
|
||||||
|
const AppDrawer({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => AppDrawerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppDrawerState extends State<AppDrawer> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// final themeProvider = Provider.of<ThemeProvider>(context);
|
||||||
|
|
||||||
|
return Drawer(
|
||||||
|
child: ListView(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxHeight: 100),
|
||||||
|
child: DrawerHeader(
|
||||||
|
decoration: BoxDecoration(color: Theme.of(context).primaryColor),
|
||||||
|
child: const Text(
|
||||||
|
'设置',
|
||||||
|
style: TextStyle(color: Colors.white, fontSize: 24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ThemeLayout(),
|
||||||
|
const Divider(),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.info),
|
||||||
|
title: const Text('关于我们'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context); // 关闭抽屉
|
||||||
|
// 跳转到关于页面
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class NavBar extends StatelessWidget {
|
|
||||||
final int currentIndex;
|
|
||||||
final Function(int) onTap;
|
|
||||||
|
|
||||||
static const List<BottomNavigationBarItem> navItems = [
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
icon: Icon(Icons.home_outlined),
|
|
||||||
activeIcon: Icon(Icons.home),
|
|
||||||
label: "记录",
|
|
||||||
),
|
|
||||||
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
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BottomNavigationBar(
|
|
||||||
currentIndex: currentIndex,
|
|
||||||
iconSize: 25,
|
|
||||||
type: BottomNavigationBarType.fixed,
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
items: navItems,
|
|
||||||
onTap: onTap,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SettingsDrawer extends StatelessWidget {
|
|
||||||
const SettingsDrawer({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
// final themeProvider = Provider.of<ThemeProvider>(context);
|
|
||||||
|
|
||||||
return Drawer(
|
|
||||||
child: ListView(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(maxHeight: 100),
|
|
||||||
child: DrawerHeader(
|
|
||||||
decoration: BoxDecoration(color: Theme.of(context).primaryColor),
|
|
||||||
child: const Text(
|
|
||||||
'设置',
|
|
||||||
style: TextStyle(color: Colors.white, fontSize: 24),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.brightness_4),
|
|
||||||
title: const Text('夜间模式'),
|
|
||||||
// trailing: Switch(
|
|
||||||
// value: themeProvider.isDarkMode,
|
|
||||||
// onChanged: (bool value) {
|
|
||||||
// themeProvider.toggleTheme();
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// onTap: () => themeProvider.toggleTheme(),
|
|
||||||
),
|
|
||||||
|
|
||||||
// 其他设置选项
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.notifications),
|
|
||||||
title: const Text('通知设置'),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context); // 关闭抽屉
|
|
||||||
// 跳转到通知设置页面
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.language),
|
|
||||||
title: const Text('语言'),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context); // 关闭抽屉
|
|
||||||
// 跳转到语言设置页面
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
// 底部关于
|
|
||||||
const Divider(),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.info),
|
|
||||||
title: const Text('关于我们'),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context); // 关闭抽屉
|
|
||||||
// 跳转到关于页面
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<StatelessWidget> homeActions() {
|
|
||||||
return [
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.search, color: Colors.white),
|
|
||||||
onPressed: () {
|
|
||||||
// 搜索功能
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Builder(
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return IconButton(
|
|
||||||
icon: Icon(Icons.settings, color: Colors.white),
|
|
||||||
onPressed: () {
|
|
||||||
Scaffold.of(context).openEndDrawer();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,48 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_common/provider/theme_provider.dart';
|
||||||
|
import 'package:flutter_common/utils/log_utils.dart';
|
||||||
|
import 'package:flutter_common/utils/sp_utils.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:food_hub_app/utils/sp_util.dart';
|
import 'package:food_hub_app/provider/app_provider.dart';
|
||||||
|
import 'package:food_hub_app/provider/food_provider.dart';
|
||||||
|
import 'package:food_hub_app/provider/user_provider.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/moment.dart';
|
||||||
|
import 'package:food_hub_app/views/moment_form.dart';
|
||||||
|
import 'package:food_hub_app/views/moment_user.dart';
|
||||||
|
import 'package:food_hub_app/views/profile_form.dart';
|
||||||
|
import 'package:food_hub_app/views/profile_user.dart';
|
||||||
|
import 'package:food_hub_app/views/recipe_form.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:liquid_glass_widgets/liquid_glass_setup.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
FormBuilderLocalizations.delegate.load(const Locale('zh', 'CN'));
|
FormBuilderLocalizations.delegate.load(const Locale('zh', 'CN'));
|
||||||
await SPUtil.init();
|
await SPUtil.init();
|
||||||
runApp(const MyApp());
|
await initLogger();
|
||||||
|
|
||||||
|
// 预编译 shader,防止首帧白闪
|
||||||
|
await LiquidGlassWidgets.initialize();
|
||||||
|
// wrap() 安装无障碍桥接、全局主题、自适应质量
|
||||||
|
runApp(
|
||||||
|
LiquidGlassWidgets.wrap(
|
||||||
|
child: MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider(create: (context) => FoodProvider()),
|
||||||
|
ChangeNotifierProvider(create: (context) => ThemeProvider()),
|
||||||
|
ChangeNotifierProvider(create: (context) => UserProvider()),
|
||||||
|
ChangeNotifierProvider(create: (context) => AppProvider()),
|
||||||
|
],
|
||||||
|
child: const MyApp(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
@@ -20,9 +51,53 @@ class MyApp extends StatelessWidget {
|
|||||||
// This widget is the root of your application.
|
// This widget is the root of your application.
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final themeProvider = context.watch<ThemeProvider>();
|
||||||
|
final brightness = MediaQuery.of(context).platformBrightness;
|
||||||
|
themeProvider.isDarkMode = brightness == Brightness.dark;
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData(fontFamily: 'CustomFont'),
|
theme: ThemeData(
|
||||||
|
scaffoldBackgroundColor: const Color(0xFFFAF6F0),
|
||||||
|
colorScheme: ColorScheme(
|
||||||
|
brightness: Brightness.light,
|
||||||
|
primary: const Color(0xFFE89F71),
|
||||||
|
secondary: const Color(0xFFE89F71),
|
||||||
|
surface: const Color(0xFFFFFFFF),
|
||||||
|
error: const Color(0xFFE76F51),
|
||||||
|
|
||||||
|
onPrimary: Colors.white,
|
||||||
|
onSecondary: Colors.white,
|
||||||
|
onSurface: const Color(0xFF3A332C),
|
||||||
|
onError: Colors.white,
|
||||||
|
),
|
||||||
|
textTheme: TextTheme(
|
||||||
|
titleLarge: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: const Color(0xFF3A332C),
|
||||||
|
),
|
||||||
|
titleMedium: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: const Color(0xFF3A332C),
|
||||||
|
),
|
||||||
|
bodyLarge: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
color: const Color(0xFF3A332C),
|
||||||
|
),
|
||||||
|
bodyMedium: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: const Color(0xFF8C8379),
|
||||||
|
),
|
||||||
|
bodySmall: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: const Color(0xFF8C8379),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
fontFamily: 'CustomFont',
|
||||||
|
useMaterial3: true,
|
||||||
|
),
|
||||||
supportedLocales: const [
|
supportedLocales: const [
|
||||||
Locale('en', 'US'), // 英语
|
Locale('en', 'US'), // 英语
|
||||||
Locale('zh', 'CN'), // 中文
|
Locale('zh', 'CN'), // 中文
|
||||||
@@ -45,7 +120,14 @@ class MyApp extends StatelessWidget {
|
|||||||
home: LoginPage(),
|
home: LoginPage(),
|
||||||
routes: {
|
routes: {
|
||||||
'/home': (context) => HomePage(),
|
'/home': (context) => HomePage(),
|
||||||
'/recordForm': (context) => RecordFormPage(),
|
'/recordForm': (context) => RecordForm(),
|
||||||
|
'/recipeForm': (context) => RecipeForm(),
|
||||||
|
'/recipeDetail': (context) => RecipeDetailPage(),
|
||||||
|
'/momentForm': (context) => MomentForm(),
|
||||||
|
'/moment': (context) => MomentPage(),
|
||||||
|
'/momentUser': (context) => MomentUserPage(),
|
||||||
|
'/profileUser': (context) => ProfileUserPage(),
|
||||||
|
'/profileForm': (context) => ProfileForm(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
15
lib/models/layout.dart
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class PageInfo {
|
||||||
|
final String label;
|
||||||
|
final IconData icon;
|
||||||
|
final IconData activeIcon;
|
||||||
|
final Widget page;
|
||||||
|
|
||||||
|
const PageInfo({
|
||||||
|
required this.label,
|
||||||
|
required this.icon,
|
||||||
|
required this.activeIcon,
|
||||||
|
required this.page,
|
||||||
|
});
|
||||||
|
}
|
||||||
82
lib/models/moment.dart
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'moment.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class Comment {
|
||||||
|
int id;
|
||||||
|
String username;
|
||||||
|
String avatar;
|
||||||
|
String content;
|
||||||
|
String date;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
int? id;
|
||||||
|
int? userId;
|
||||||
|
String? username;
|
||||||
|
String? avatar;
|
||||||
|
String content;
|
||||||
|
List<String> imageList;
|
||||||
|
String? date;
|
||||||
|
List<int>? likeList;
|
||||||
|
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);
|
||||||
|
|
||||||
|
static Moment getEmpty() {
|
||||||
|
return Moment(
|
||||||
|
id: 0,
|
||||||
|
content: '',
|
||||||
|
imageList: []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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,56 @@ part 'recipe.g.dart';
|
|||||||
|
|
||||||
/// 食材信息
|
/// 食材信息
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Material {
|
class RecipeMaterial {
|
||||||
|
int id;
|
||||||
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.id,
|
||||||
|
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 id;
|
||||||
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.id,
|
||||||
|
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,15 +61,15 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 成果信息
|
/// 成果信息
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Record {
|
class FoodRecord {
|
||||||
int? id;
|
int? id;
|
||||||
String name;
|
String name;
|
||||||
String category;
|
String category;
|
||||||
@@ -64,7 +77,7 @@ class Record {
|
|||||||
String date;
|
String date;
|
||||||
String imageUrl;
|
String imageUrl;
|
||||||
|
|
||||||
Record({
|
FoodRecord({
|
||||||
this.id,
|
this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.category,
|
required this.category,
|
||||||
@@ -73,20 +86,31 @@ class Record {
|
|||||||
required this.imageUrl,
|
required this.imageUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Record.fromJson(Map<String, dynamic> json) => _$RecordFromJson(json);
|
factory FoodRecord.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$FoodRecordFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$RecordToJson(this);
|
Map<String, dynamic> toJson() => _$FoodRecordToJson(this);
|
||||||
|
|
||||||
|
static FoodRecord getEmpty() {
|
||||||
|
return FoodRecord(
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
category: '',
|
||||||
|
person: 0,
|
||||||
|
date: '',
|
||||||
|
imageUrl: '',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class RecipeQuery {
|
class RecipeQuery {
|
||||||
String category;
|
String category;
|
||||||
|
|
||||||
RecipeQuery({
|
RecipeQuery({required this.category});
|
||||||
required this.category
|
|
||||||
});
|
|
||||||
|
|
||||||
factory RecipeQuery.fromJson(Map<String, dynamic> json) => _$RecipeQueryFromJson(json);
|
factory RecipeQuery.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$RecipeQueryFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$RecipeQueryToJson(this);
|
Map<String, dynamic> toJson() => _$RecipeQueryToJson(this);
|
||||||
}
|
}
|
||||||
@@ -94,49 +118,156 @@ 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;
|
List<RecipeMaterial> materialList;
|
||||||
List<Material>? materialList;
|
List<RecipeStep> stepList;
|
||||||
List<Step>? stepList;
|
List<FoodRecord> recordList;
|
||||||
List<Record> recordList;
|
List<int> likeList;
|
||||||
List<int>? likeList;
|
List<int> favouriteList;
|
||||||
int? likeCount;
|
List<RecipeComment> commentList;
|
||||||
List<int>? favouriteList;
|
|
||||||
int? favouriteCount;
|
|
||||||
List<Comment>? commentList;
|
|
||||||
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.materialList,
|
||||||
this.materialList,
|
required this.stepList,
|
||||||
this.stepList,
|
|
||||||
required this.recordList,
|
required this.recordList,
|
||||||
this.likeList,
|
required this.likeList,
|
||||||
this.likeCount,
|
required this.favouriteList,
|
||||||
this.favouriteList,
|
required this.commentList,
|
||||||
this.favouriteCount,
|
|
||||||
this.commentList,
|
|
||||||
this.commentCount,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Recipe.fromJson(Map<String, dynamic> json) => _$RecipeFromJson(json);
|
factory Recipe.fromJson(Map<String, dynamic> json) => _$RecipeFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$RecipeToJson(this);
|
Map<String, dynamic> toJson() => _$RecipeToJson(this);
|
||||||
|
|
||||||
|
static Recipe getEmpty() {
|
||||||
|
return Recipe(
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
category: '家常菜',
|
||||||
|
recommendRate: 3,
|
||||||
|
remark: '',
|
||||||
|
isShare: true,
|
||||||
|
userId: 0,
|
||||||
|
username: '',
|
||||||
|
materialList: [],
|
||||||
|
stepList: [],
|
||||||
|
recordList: [],
|
||||||
|
likeList: [],
|
||||||
|
favouriteList: [],
|
||||||
|
commentList: []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class RecipeSummary {
|
||||||
|
int id;
|
||||||
|
String name;
|
||||||
|
String category;
|
||||||
|
double recommendRate;
|
||||||
|
bool isShare;
|
||||||
|
List<FoodRecord> recordList;
|
||||||
|
int likeCount;
|
||||||
|
int favouriteCount;
|
||||||
|
int commentCount;
|
||||||
|
int userId;
|
||||||
|
String username;
|
||||||
|
|
||||||
|
RecipeSummary({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.category,
|
||||||
|
required this.recommendRate,
|
||||||
|
required this.isShare,
|
||||||
|
required this.userId,
|
||||||
|
required this.username,
|
||||||
|
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<FoodRecord> 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);
|
||||||
|
|
||||||
|
static RecipeDetail getEmpty() {
|
||||||
|
return RecipeDetail(
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
category: '',
|
||||||
|
recommendRate: 0,
|
||||||
|
remark: '',
|
||||||
|
isShare: false,
|
||||||
|
userId: 0,
|
||||||
|
username: '',
|
||||||
|
avatar: '',
|
||||||
|
materialList: [],
|
||||||
|
stepList: [],
|
||||||
|
recordList: [],
|
||||||
|
likeList: [],
|
||||||
|
favouriteList: [],
|
||||||
|
commentList: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ViewType { recipe, calendar, timeline }
|
enum ViewType { recipe, calendar, timeline }
|
||||||
|
|||||||
@@ -6,31 +6,39 @@ part of 'recipe.dart';
|
|||||||
// JsonSerializableGenerator
|
// JsonSerializableGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
Material _$MaterialFromJson(Map<String, dynamic> json) => Material(
|
RecipeMaterial _$RecipeMaterialFromJson(Map<String, dynamic> json) =>
|
||||||
|
RecipeMaterial(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
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>{
|
||||||
|
'id': instance.id,
|
||||||
'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(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
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>{
|
||||||
|
'id': instance.id,
|
||||||
'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 +46,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,
|
||||||
@@ -46,7 +55,7 @@ Map<String, dynamic> _$CommentToJson(Comment instance) => <String, dynamic>{
|
|||||||
'date': instance.date,
|
'date': instance.date,
|
||||||
};
|
};
|
||||||
|
|
||||||
Record _$RecordFromJson(Map<String, dynamic> json) => Record(
|
FoodRecord _$FoodRecordFromJson(Map<String, dynamic> json) => FoodRecord(
|
||||||
id: (json['id'] as num?)?.toInt(),
|
id: (json['id'] as num?)?.toInt(),
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
category: json['category'] as String,
|
category: json['category'] as String,
|
||||||
@@ -55,7 +64,8 @@ Record _$RecordFromJson(Map<String, dynamic> json) => Record(
|
|||||||
imageUrl: json['imageUrl'] as String,
|
imageUrl: json['imageUrl'] as String,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$RecordToJson(Record instance) => <String, dynamic>{
|
Map<String, dynamic> _$FoodRecordToJson(FoodRecord instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
'category': instance.category,
|
'category': instance.category,
|
||||||
@@ -71,45 +81,128 @@ 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?,
|
|
||||||
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) => FoodRecord.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(),
|
|
||||||
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(),
|
|
||||||
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(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{
|
Map<String, dynamic> _$RecipeToJson(Recipe 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,
|
||||||
|
'materialList': instance.materialList,
|
||||||
|
'stepList': instance.stepList,
|
||||||
|
'recordList': instance.recordList,
|
||||||
|
'likeList': instance.likeList,
|
||||||
|
'favouriteList': instance.favouriteList,
|
||||||
|
'commentList': instance.commentList,
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
recordList:
|
||||||
|
(json['recordList'] as List<dynamic>)
|
||||||
|
.map((e) => FoodRecord.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,
|
||||||
|
};
|
||||||
|
|
||||||
|
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) => FoodRecord.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,
|
'id': instance.id,
|
||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
'category': instance.category,
|
'category': instance.category,
|
||||||
@@ -123,9 +216,6 @@ Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{
|
|||||||
'stepList': instance.stepList,
|
'stepList': instance.stepList,
|
||||||
'recordList': instance.recordList,
|
'recordList': instance.recordList,
|
||||||
'likeList': instance.likeList,
|
'likeList': instance.likeList,
|
||||||
'likeCount': instance.likeCount,
|
|
||||||
'favouriteList': instance.favouriteList,
|
'favouriteList': instance.favouriteList,
|
||||||
'favouriteCount': instance.favouriteCount,
|
|
||||||
'commentList': instance.commentList,
|
'commentList': instance.commentList,
|
||||||
'commentCount': instance.commentCount,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,14 +30,16 @@ class SaTokenInfo {
|
|||||||
this.tag,
|
this.tag,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory SaTokenInfo.fromJson(Map<String, dynamic> json) => _$SaTokenInfoFromJson(json);
|
factory SaTokenInfo.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SaTokenInfoFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$SaTokenInfoToJson(this);
|
Map<String, dynamic> toJson() => _$SaTokenInfoToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class User {
|
class User {
|
||||||
int? id;
|
int? id;
|
||||||
String? username;
|
String username;
|
||||||
int? gender;
|
int? gender;
|
||||||
String? phoneNumber;
|
String? phoneNumber;
|
||||||
String? email;
|
String? email;
|
||||||
@@ -51,7 +53,7 @@ class User {
|
|||||||
|
|
||||||
User({
|
User({
|
||||||
this.id,
|
this.id,
|
||||||
this.username,
|
required this.username,
|
||||||
this.gender,
|
this.gender,
|
||||||
this.phoneNumber,
|
this.phoneNumber,
|
||||||
this.email,
|
this.email,
|
||||||
@@ -65,7 +67,24 @@ class User {
|
|||||||
});
|
});
|
||||||
|
|
||||||
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
|
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$UserToJson(this);
|
Map<String, dynamic> toJson() => _$UserToJson(this);
|
||||||
|
|
||||||
|
static User getEmpty() {
|
||||||
|
return User(
|
||||||
|
username: '',
|
||||||
|
gender: 0,
|
||||||
|
phoneNumber: '',
|
||||||
|
email: '',
|
||||||
|
birthDate: null,
|
||||||
|
avatar: '',
|
||||||
|
area: [],
|
||||||
|
address: '',
|
||||||
|
job: '',
|
||||||
|
tags: [],
|
||||||
|
description: '',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
@@ -75,6 +94,8 @@ class Session {
|
|||||||
|
|
||||||
Session({required this.saToken, required this.userInfo});
|
Session({required this.saToken, required this.userInfo});
|
||||||
|
|
||||||
factory Session.fromJson(Map<String, dynamic> json) => _$SessionFromJson(json);
|
factory Session.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SessionFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$SessionToJson(this);
|
Map<String, dynamic> toJson() => _$SessionToJson(this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ Map<String, dynamic> _$SaTokenInfoToJson(SaTokenInfo instance) =>
|
|||||||
|
|
||||||
User _$UserFromJson(Map<String, dynamic> json) => User(
|
User _$UserFromJson(Map<String, dynamic> json) => User(
|
||||||
id: (json['id'] as num?)?.toInt(),
|
id: (json['id'] as num?)?.toInt(),
|
||||||
username: json['username'] as String?,
|
username: json['username'] as String,
|
||||||
gender: (json['gender'] as num?)?.toInt(),
|
gender: (json['gender'] as num?)?.toInt(),
|
||||||
phoneNumber: json['phoneNumber'] as String?,
|
phoneNumber: json['phoneNumber'] as String?,
|
||||||
email: json['email'] as String?,
|
email: json['email'] as String?,
|
||||||
|
|||||||
21
lib/models/stats.dart
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
20
lib/models/stats.g.dart
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// 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,
|
||||||
|
};
|
||||||
8
lib/provider/app_provider.dart
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AppProvider with ChangeNotifier {
|
||||||
|
String appVersion = '1.0.0';
|
||||||
|
String buildNumber = '1';
|
||||||
|
|
||||||
|
String get fullVersion => '$appVersion+$buildNumber';
|
||||||
|
}
|
||||||
517
lib/provider/food_provider.dart
Normal file
@@ -0,0 +1,517 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_common/models/common_model.dart';
|
||||||
|
import 'package:food_hub_app/apis/moment.dart';
|
||||||
|
import 'package:food_hub_app/apis/recipe.dart';
|
||||||
|
import 'package:food_hub_app/apis/stats.dart';
|
||||||
|
import 'package:food_hub_app/models/moment.dart';
|
||||||
|
import 'package:food_hub_app/models/recipe.dart';
|
||||||
|
import 'package:food_hub_app/models/stats.dart';
|
||||||
|
import 'package:food_hub_app/utils/date_util.dart';
|
||||||
|
import 'package:food_hub_app/utils/index.dart';
|
||||||
|
import 'package:food_hub_app/views/record.dart';
|
||||||
|
|
||||||
|
class FoodProvider with ChangeNotifier {
|
||||||
|
late RecordTab currentRecordTab = RecordTab.recipe;
|
||||||
|
late FoodRecord recordFormItem;
|
||||||
|
late Recipe recipeFormItem;
|
||||||
|
late Moment momentFormItem;
|
||||||
|
|
||||||
|
late bool isEditing;
|
||||||
|
bool isLoading = false;
|
||||||
|
String? error;
|
||||||
|
|
||||||
|
String queryCategory = '全部菜系';
|
||||||
|
late List<String> categoryList = [];
|
||||||
|
late List<RecipeSummary> recipeSummaryList = [];
|
||||||
|
|
||||||
|
int selectYear = DateTime.now().year;
|
||||||
|
DateTime selectedDay = DateTime.now();
|
||||||
|
DateTime focusedDay = DateTime.now();
|
||||||
|
|
||||||
|
late List<FoodRecord> recordList = [];
|
||||||
|
late List<FoodRecord> selectRecordList = [];
|
||||||
|
|
||||||
|
late SummaryStats summaryStats = SummaryStats(
|
||||||
|
recipeCount: 0,
|
||||||
|
categoryCount: 0,
|
||||||
|
workCount: 0,
|
||||||
|
);
|
||||||
|
late double averageRecordCount = 0;
|
||||||
|
late List<ChartData> recordStats = [];
|
||||||
|
late List<ChartData> categoryStats = [];
|
||||||
|
late List<ChartData> rankStats = [];
|
||||||
|
|
||||||
|
List<Moment> momentList = [];
|
||||||
|
int currentPage = 1;
|
||||||
|
final int pageSize = 5;
|
||||||
|
bool hasMore = true;
|
||||||
|
|
||||||
|
void resetRecordForm() {
|
||||||
|
recordFormItem = FoodRecord.getEmpty();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void initRecordForm(FoodRecord record) {
|
||||||
|
recordFormItem = record;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetRecipeForm() {
|
||||||
|
recipeFormItem = Recipe.getEmpty();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void initRecipeForm(Recipe recipe) {
|
||||||
|
recipeFormItem = recipe;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateRecipeFormItem({
|
||||||
|
String? name,
|
||||||
|
String? category,
|
||||||
|
double? recommendRate,
|
||||||
|
String? remark,
|
||||||
|
bool? isShare,
|
||||||
|
}) {
|
||||||
|
if (name != null) recipeFormItem.name = name;
|
||||||
|
if (category != null) recipeFormItem.category = category;
|
||||||
|
if (recommendRate != null) recipeFormItem.recommendRate = recommendRate;
|
||||||
|
if (remark != null) recipeFormItem.remark = remark;
|
||||||
|
if (isShare != null) recipeFormItem.isShare = isShare;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addRecipeMaterial() {
|
||||||
|
recipeFormItem.materialList.add(
|
||||||
|
RecipeMaterial(
|
||||||
|
id: DateTime.now().microsecondsSinceEpoch,
|
||||||
|
type: '主料',
|
||||||
|
name: '',
|
||||||
|
amount: '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateRecipeMaterial(
|
||||||
|
int index, {
|
||||||
|
String? type,
|
||||||
|
String? name,
|
||||||
|
String? amount,
|
||||||
|
}) {
|
||||||
|
final material = recipeFormItem.materialList[index];
|
||||||
|
recipeFormItem.materialList[index] = RecipeMaterial(
|
||||||
|
id: material.id,
|
||||||
|
type: type ?? material.type,
|
||||||
|
name: name ?? material.name,
|
||||||
|
amount: amount ?? material.amount,
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeRecipeMaterial(int index) {
|
||||||
|
recipeFormItem.materialList.removeAt(index);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addRecipeStep() {
|
||||||
|
recipeFormItem.stepList.add(
|
||||||
|
RecipeStep(
|
||||||
|
id: DateTime.now().microsecondsSinceEpoch,
|
||||||
|
sort: recipeFormItem.stepList.length,
|
||||||
|
content: '',
|
||||||
|
imageUrl: '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateRecipeStep(int index, {String? content, String? imageUrl}) {
|
||||||
|
final step = recipeFormItem.stepList[index];
|
||||||
|
recipeFormItem.stepList[index] = RecipeStep(
|
||||||
|
id: step.id,
|
||||||
|
sort: step.sort,
|
||||||
|
content: content ?? step.content,
|
||||||
|
imageUrl: imageUrl ?? step.imageUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeRecipeStep(int index) {
|
||||||
|
recipeFormItem.stepList.removeAt(index);
|
||||||
|
|
||||||
|
for (int i = 0; i < recipeFormItem.stepList.length; i++) {
|
||||||
|
recipeFormItem.stepList[i].sort = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateRecordFormItem({String? name, String? date, String? imageUrl}) {
|
||||||
|
if (name != null) recordFormItem.name = name;
|
||||||
|
if (date != null) recordFormItem.date = date;
|
||||||
|
if (imageUrl != null) recordFormItem.imageUrl = imageUrl;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateMomentFormItem({String? content}) {
|
||||||
|
if (content != null) momentFormItem.content = content;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addMomentFormImage(List<String> imageUrls) {
|
||||||
|
momentFormItem.imageList.addAll(imageUrls);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeMomentFormImage(int index) {
|
||||||
|
momentFormItem.imageList.removeAt(index);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetMomentForm() {
|
||||||
|
momentFormItem = Moment.getEmpty();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void initMomentForm(Moment moment) {
|
||||||
|
momentFormItem = moment;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> onRecordTabChange() async {
|
||||||
|
switch (currentRecordTab) {
|
||||||
|
case RecordTab.recipe:
|
||||||
|
await refreshRecipeList();
|
||||||
|
break;
|
||||||
|
case RecordTab.calendar:
|
||||||
|
case RecordTab.timeline:
|
||||||
|
await refreshRecordList();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> queryCategoryList() async {
|
||||||
|
try {
|
||||||
|
error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
final result = await queryCategoryApi();
|
||||||
|
categoryList.clear();
|
||||||
|
categoryList.add('全部菜系');
|
||||||
|
categoryList.addAll(result);
|
||||||
|
} catch (e) {
|
||||||
|
error = '加载数据失败: $e';
|
||||||
|
debugPrint('加载数据失败: $e');
|
||||||
|
} finally {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> queryRecipeByUserId(int userId) async {
|
||||||
|
if (isLoading) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading = true;
|
||||||
|
error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
final result = await queryRecipeByUserApi(userId);
|
||||||
|
recipeSummaryList = result;
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
error = '加载数据失败: $e';
|
||||||
|
debugPrint('加载数据失败: $e');
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshRecipeList() async {
|
||||||
|
if (isLoading) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading = true;
|
||||||
|
error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
final category = queryCategory == '全部菜系' ? '' : queryCategory;
|
||||||
|
final result = await queryRecipeApi(RecipeQuery(category: category));
|
||||||
|
recipeSummaryList = result;
|
||||||
|
} catch (e) {
|
||||||
|
error = '加载数据失败: $e';
|
||||||
|
debugPrint('加载数据失败: $e');
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> handleRecipe() async {
|
||||||
|
if (isLoading) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading = true;
|
||||||
|
error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
await updateRecipeApi(recipeFormItem.id!, recipeFormItem);
|
||||||
|
} else {
|
||||||
|
await addRecipeApi(recipeFormItem);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
error = '处理数据失败: $e';
|
||||||
|
debugPrint('处理数据失败: $e');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshRecordList() async {
|
||||||
|
if (isLoading) return;
|
||||||
|
|
||||||
|
if (currentRecordTab == RecordTab.recipe) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading = true;
|
||||||
|
error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
late String startDate;
|
||||||
|
late String endDate;
|
||||||
|
|
||||||
|
switch (currentRecordTab) {
|
||||||
|
case RecordTab.calendar:
|
||||||
|
startDate = getFirstDayOfMonth(focusedDay);
|
||||||
|
endDate = getLastDayOfMonth(focusedDay);
|
||||||
|
break;
|
||||||
|
case RecordTab.timeline:
|
||||||
|
startDate = '$selectYear-01-01';
|
||||||
|
endDate = '$selectYear-12-31';
|
||||||
|
break;
|
||||||
|
case RecordTab.recipe:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await queryRecordApi(startDate, endDate);
|
||||||
|
|
||||||
|
recordList = result;
|
||||||
|
|
||||||
|
if (recordList.isNotEmpty) {
|
||||||
|
selectedDay = DateTime.parse(recordList.last.date);
|
||||||
|
} else {
|
||||||
|
selectedDay = focusedDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshSelectRecord();
|
||||||
|
} catch (e) {
|
||||||
|
error = '加载数据失败: $e';
|
||||||
|
debugPrint('加载数据失败: $e');
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> handleRecord() async {
|
||||||
|
if (isLoading) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading = true;
|
||||||
|
error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
await updateRecordApi(recordFormItem.id!, recordFormItem);
|
||||||
|
} else {
|
||||||
|
await addRecordApi(recordFormItem);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
error = '处理数据失败: $e';
|
||||||
|
debugPrint('处理数据失败: $e');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> deleteRecord() async {
|
||||||
|
if (isLoading) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading = true;
|
||||||
|
error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
await deleteRecordApi(recordFormItem.id!);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
error = '处理数据失败: $e';
|
||||||
|
debugPrint('处理数据失败: $e');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshSelectRecord() {
|
||||||
|
selectRecordList =
|
||||||
|
recordList
|
||||||
|
.where((record) => record.date == formatDateTime(selectedDay))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshFoodStats() async {
|
||||||
|
if (isLoading) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading = true;
|
||||||
|
error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
final statsResult = await queryStatsApi();
|
||||||
|
final recordStatsResult = await queryRecordStatsApi();
|
||||||
|
final categoryStatsResult = await queryCategoryStatsApi();
|
||||||
|
final rankStatsResult = await queryRankStatsApi();
|
||||||
|
|
||||||
|
summaryStats = statsResult;
|
||||||
|
recordStats = recordStatsResult;
|
||||||
|
categoryStats = categoryStatsResult;
|
||||||
|
rankStats = rankStatsResult.take(15).toList();
|
||||||
|
|
||||||
|
if (recordStats.isNotEmpty) {
|
||||||
|
double sumValue = recordStats.fold(
|
||||||
|
0.0,
|
||||||
|
(sum, item) => sum + item.value,
|
||||||
|
);
|
||||||
|
averageRecordCount = sumValue / recordStats.length;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error = '加载数据失败: $e';
|
||||||
|
debugPrint('加载数据失败: $e');
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> queryMomentByPage({bool isRefresh = true}) async {
|
||||||
|
if (isLoading) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (currentPage == 1) {
|
||||||
|
isLoading = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
// 如果是刷新,重置页码
|
||||||
|
if (isRefresh) {
|
||||||
|
currentPage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await queryMomentByPageApi(currentPage, pageSize);
|
||||||
|
|
||||||
|
// 更新数据
|
||||||
|
if (isRefresh) {
|
||||||
|
momentList = result.records;
|
||||||
|
} else {
|
||||||
|
momentList.addAll(result.records);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否还有更多数据
|
||||||
|
hasMore = result.current < result.pages;
|
||||||
|
if (hasMore) {
|
||||||
|
currentPage++;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error = '加载数据失败: $e';
|
||||||
|
debugPrint('加载数据失败: $e');
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> queryMomentByUserId(int userId) async {
|
||||||
|
if (isLoading) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading = true;
|
||||||
|
error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
final result = await queryMomentListByUserIdApi(userId);
|
||||||
|
momentList = result;
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
error = '加载数据失败: $e';
|
||||||
|
debugPrint('加载数据失败: $e');
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadMoreMomentList() async {
|
||||||
|
if (hasMore && !isLoading) {
|
||||||
|
await queryMomentByPage(isRefresh: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshMomentList() async {
|
||||||
|
await queryMomentByPage(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> handleMoment() async {
|
||||||
|
if (isLoading) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading = true;
|
||||||
|
error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
await updateMomentApi(momentFormItem.id!, momentFormItem);
|
||||||
|
} else {
|
||||||
|
await addMomentApi(momentFormItem);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
error = '处理数据失败: $e';
|
||||||
|
debugPrint('处理数据失败: $e');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> deleteMoment() async {
|
||||||
|
if (isLoading) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading = true;
|
||||||
|
error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
await deleteMomentApi(momentFormItem.id!);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
error = '处理数据失败: $e';
|
||||||
|
debugPrint('处理数据失败: $e');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
104
lib/provider/user_provider.dart
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:food_hub_app/apis/moment.dart';
|
||||||
|
import 'package:food_hub_app/apis/user.dart';
|
||||||
|
import 'package:food_hub_app/models/moment.dart';
|
||||||
|
import 'package:food_hub_app/models/session.dart';
|
||||||
|
|
||||||
|
class UserProvider with ChangeNotifier {
|
||||||
|
late User currentUser = User.getEmpty();
|
||||||
|
bool isLoading = false;
|
||||||
|
String? error = '';
|
||||||
|
List<Moment> momentList = [];
|
||||||
|
late User userFormItem;
|
||||||
|
|
||||||
|
Future<void> refreshUser(int id) async {
|
||||||
|
if (isLoading) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading = true;
|
||||||
|
error = '';
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
final result = await queryUserApi(id);
|
||||||
|
currentUser = result;
|
||||||
|
} catch (e) {
|
||||||
|
error = '加载数据失败: $e';
|
||||||
|
debugPrint('加载数据失败: $e');
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> queryMomentByUserId(int userId) async {
|
||||||
|
if (isLoading) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading = true;
|
||||||
|
error = '';
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
final result = await queryMomentListByUserIdApi(userId);
|
||||||
|
momentList = result;
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
error = '加载数据失败: $e';
|
||||||
|
debugPrint('加载数据失败: $e');
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void initUserForm(User user) {
|
||||||
|
userFormItem = user;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateUserFormItem({
|
||||||
|
String? username,
|
||||||
|
int? gender,
|
||||||
|
String? phoneNumber,
|
||||||
|
String? email,
|
||||||
|
DateTime? birthDate,
|
||||||
|
String? avatar,
|
||||||
|
List<String>? area,
|
||||||
|
String? address,
|
||||||
|
String? job,
|
||||||
|
List<String>? tags,
|
||||||
|
String? description
|
||||||
|
}) {
|
||||||
|
if (username != null) userFormItem.username = username;
|
||||||
|
if (gender != null) userFormItem.gender = gender;
|
||||||
|
if (phoneNumber != null) userFormItem.phoneNumber = phoneNumber;
|
||||||
|
if (email != null) userFormItem.email = email;
|
||||||
|
if (birthDate != null) userFormItem.birthDate = birthDate;
|
||||||
|
if (avatar != null) userFormItem.avatar = avatar;
|
||||||
|
if (area != null) userFormItem.area = area;
|
||||||
|
if (address != null) userFormItem.address = address;
|
||||||
|
if (job != null) userFormItem.job = job;
|
||||||
|
if (tags != null) userFormItem.tags = tags;
|
||||||
|
if (description != null) userFormItem.description = description;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> handleUser() async {
|
||||||
|
if (isLoading) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading = true;
|
||||||
|
error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
await updateUserApi(userFormItem.id!, userFormItem);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
error = '处理数据失败: $e';
|
||||||
|
debugPrint('处理数据失败: $e');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:food_hub_app/utils/sp_util.dart';
|
|
||||||
import 'package:food_hub_app/widgets/common/index.dart';
|
|
||||||
|
|
||||||
import 'log_util.dart';
|
|
||||||
|
|
||||||
class HttpUtil {
|
|
||||||
static final HttpUtil _instance = HttpUtil._internal();
|
|
||||||
|
|
||||||
factory HttpUtil() => _instance;
|
|
||||||
|
|
||||||
late Dio _dio;
|
|
||||||
String baseUrl = kDebugMode ? "http://172.29.101.108:8100" : "http://14.103.235.151:81";
|
|
||||||
|
|
||||||
// 请求头配置
|
|
||||||
Map<String, dynamic> headers = {
|
|
||||||
'Content-Type': 'application/json;charset=UTF-8',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
// 超时时间
|
|
||||||
final int timeout = 5;
|
|
||||||
|
|
||||||
HttpUtil._internal() {
|
|
||||||
// 初始化Dio实例
|
|
||||||
BaseOptions options = BaseOptions(
|
|
||||||
baseUrl: baseUrl,
|
|
||||||
connectTimeout: Duration(seconds: timeout),
|
|
||||||
receiveTimeout: Duration(seconds: timeout),
|
|
||||||
headers: headers,
|
|
||||||
);
|
|
||||||
|
|
||||||
_dio = Dio(options);
|
|
||||||
|
|
||||||
// 添加请求拦截器
|
|
||||||
_dio.interceptors.add(
|
|
||||||
InterceptorsWrapper(
|
|
||||||
onRequest: (options, handler) {
|
|
||||||
logger.d("请求URL: ${options.uri}");
|
|
||||||
if (options.data != null) {
|
|
||||||
logger.d("请求参数: ${options.data}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SPUtil.getString('token').isNotEmpty) {
|
|
||||||
options.headers['satoken'] = SPUtil.getString('token');
|
|
||||||
}
|
|
||||||
return handler.next(options);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 添加响应拦截器
|
|
||||||
_dio.interceptors.add(
|
|
||||||
InterceptorsWrapper(
|
|
||||||
onResponse: (response, handler) {
|
|
||||||
logger.d("响应状态码: ${response.statusCode}");
|
|
||||||
// logger.d("响应数据: ${response.data}");
|
|
||||||
return handler.next(response);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 添加错误拦截器
|
|
||||||
_dio.interceptors.add(
|
|
||||||
InterceptorsWrapper(
|
|
||||||
onError: (DioException e, handler) {
|
|
||||||
_handleError(e);
|
|
||||||
return handler.next(e);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 基础请求方法(处理所有类型的请求)
|
|
||||||
Future<T> _request<T>(
|
|
||||||
String path, {
|
|
||||||
required String method,
|
|
||||||
dynamic data,
|
|
||||||
Map<String, dynamic>? queryParameters,
|
|
||||||
T Function(dynamic data)? converter,
|
|
||||||
}) async {
|
|
||||||
try {
|
|
||||||
if (kDebugMode) {
|
|
||||||
path = path.replaceFirst('/food-service', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
Response response = await _dio.request(
|
|
||||||
path,
|
|
||||||
data: data,
|
|
||||||
queryParameters: queryParameters,
|
|
||||||
options: Options(method: method),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 处理响应数据
|
|
||||||
if (converter != null) {
|
|
||||||
return converter(response.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 没有转换器时尝试直接返回(可能不安全,建议提供转换器)
|
|
||||||
return response.data;
|
|
||||||
} catch (e) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET请求
|
|
||||||
Future<T> get<T>(
|
|
||||||
String path, {
|
|
||||||
Map<String, dynamic>? queryParameters,
|
|
||||||
T Function(dynamic data)? converter,
|
|
||||||
}) => _request(
|
|
||||||
path,
|
|
||||||
method: "GET",
|
|
||||||
queryParameters: queryParameters,
|
|
||||||
converter: converter,
|
|
||||||
);
|
|
||||||
|
|
||||||
// POST请求
|
|
||||||
Future<T> post<T>(
|
|
||||||
String path, {
|
|
||||||
dynamic data,
|
|
||||||
Map<String, dynamic>? queryParameters,
|
|
||||||
T Function(dynamic data)? converter,
|
|
||||||
}) => _request(
|
|
||||||
path,
|
|
||||||
method: "POST",
|
|
||||||
data: data,
|
|
||||||
queryParameters: queryParameters,
|
|
||||||
converter: converter,
|
|
||||||
);
|
|
||||||
|
|
||||||
// PUT请求
|
|
||||||
Future<T> put<T>(
|
|
||||||
String path, {
|
|
||||||
dynamic data,
|
|
||||||
Map<String, dynamic>? queryParameters,
|
|
||||||
T Function(dynamic data)? converter,
|
|
||||||
}) => _request(
|
|
||||||
path,
|
|
||||||
method: "PUT",
|
|
||||||
data: data,
|
|
||||||
queryParameters: queryParameters,
|
|
||||||
converter: converter,
|
|
||||||
);
|
|
||||||
|
|
||||||
// DELETE请求
|
|
||||||
Future<T> delete<T>(
|
|
||||||
String path, {
|
|
||||||
dynamic data,
|
|
||||||
Map<String, dynamic>? queryParameters,
|
|
||||||
T Function(dynamic data)? converter,
|
|
||||||
}) => _request(
|
|
||||||
path,
|
|
||||||
method: "DELETE",
|
|
||||||
data: data,
|
|
||||||
queryParameters: queryParameters,
|
|
||||||
converter: converter,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 文件上传
|
|
||||||
Future<dynamic> upload(
|
|
||||||
String path,
|
|
||||||
String filePath, {
|
|
||||||
Map<String, dynamic>? queryParameters,
|
|
||||||
}) async {
|
|
||||||
try {
|
|
||||||
FormData formData = FormData.fromMap({
|
|
||||||
'file': await MultipartFile.fromFile(filePath),
|
|
||||||
});
|
|
||||||
|
|
||||||
Response response = await _dio.post(
|
|
||||||
path,
|
|
||||||
data: formData,
|
|
||||||
queryParameters: queryParameters,
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
} catch (e) {
|
|
||||||
_handleError(e);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 错误处理
|
|
||||||
void _handleError(dynamic error) {
|
|
||||||
String errorMessage = '未知错误';
|
|
||||||
if (error is DioException) {
|
|
||||||
switch (error.type) {
|
|
||||||
case DioExceptionType.connectionTimeout:
|
|
||||||
errorMessage = '连接超时,请检查网络连接';
|
|
||||||
break;
|
|
||||||
case DioExceptionType.sendTimeout:
|
|
||||||
errorMessage = '发送超时,请检查网络连接';
|
|
||||||
break;
|
|
||||||
case DioExceptionType.receiveTimeout:
|
|
||||||
errorMessage = '接收超时,请检查网络连接';
|
|
||||||
break;
|
|
||||||
case DioExceptionType.cancel:
|
|
||||||
errorMessage = '请求已取消';
|
|
||||||
break;
|
|
||||||
case DioExceptionType.badCertificate:
|
|
||||||
errorMessage = '证书验证失败';
|
|
||||||
break;
|
|
||||||
case DioExceptionType.badResponse:
|
|
||||||
// 处理HTTP响应错误(4xx, 5xx)
|
|
||||||
final statusCode = error.response?.statusCode ?? 0;
|
|
||||||
final responseData = error.response?.data;
|
|
||||||
|
|
||||||
if (responseData is Map<String, dynamic> && responseData.containsKey('message')) {
|
|
||||||
// 服务器返回了自定义错误消息
|
|
||||||
errorMessage = responseData['message']?.toString() ?? '未知错误';
|
|
||||||
} else {
|
|
||||||
errorMessage = _getHttpErrorMessage(statusCode);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case DioExceptionType.connectionError:
|
|
||||||
errorMessage = '网络连接错误,请检查网络设置';
|
|
||||||
break;
|
|
||||||
case DioExceptionType.unknown:
|
|
||||||
if (error.error != null) {
|
|
||||||
errorMessage = '未知错误: ${error.error.toString()}';
|
|
||||||
}
|
|
||||||
errorMessage = '发生未知错误';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errorMessage = '非Dio错误: $error';
|
|
||||||
}
|
|
||||||
|
|
||||||
print(errorMessage);
|
|
||||||
showErrorToast(errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getHttpErrorMessage(int statusCode) {
|
|
||||||
// 根据HTTP状态码返回对应的错误消息
|
|
||||||
switch (statusCode) {
|
|
||||||
case 400: return '错误请求,请检查参数';
|
|
||||||
case 401: return '未授权,请登录';
|
|
||||||
case 403: return '禁止访问,权限不足';
|
|
||||||
case 404: return '资源不存在';
|
|
||||||
case 405: return '方法不允许';
|
|
||||||
case 408: return '请求超时';
|
|
||||||
case 500: return '服务器内部错误';
|
|
||||||
case 502: return '网关错误';
|
|
||||||
case 503: return '服务不可用';
|
|
||||||
case 504: return '网关超时';
|
|
||||||
default: return 'HTTP错误: $statusCode';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,10 +16,13 @@ String formatDateTime(DateTime dateTime, [String format = 'yyyy-MM-dd']) {
|
|||||||
return Intl.withLocale('zh_CN', () => formatter.format(dateTime));
|
return Intl.withLocale('zh_CN', () => formatter.format(dateTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 通用列表转换函数
|
|
||||||
List<T> convertListResponse<T>(dynamic data, T Function(Map<String, dynamic>) fromJson) {
|
|
||||||
|
List<String> convertListStringResponse(dynamic data) {
|
||||||
if (data is List) {
|
if (data is List) {
|
||||||
return data.map((item) => fromJson(item as Map<String, dynamic>)).toList();
|
return data.map((item) => item.toString()).toList();
|
||||||
}
|
}
|
||||||
throw FormatException('Expected a list of items for conversion, but got ${data.runtimeType}');
|
throw FormatException(
|
||||||
|
'Expected a list of items for conversion, but got ${data.runtimeType}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import 'package:logger/logger.dart';
|
|
||||||
|
|
||||||
// 全局日志实例,在整个项目中共享
|
|
||||||
final Logger logger = Logger(
|
|
||||||
printer: PrettyPrinter(
|
|
||||||
methodCount: 1,
|
|
||||||
colors: true,
|
|
||||||
dateTimeFormat: DateTimeFormat.dateAndTime,
|
|
||||||
),
|
|
||||||
// 可选:配置输出到文件(需配合文件操作库)
|
|
||||||
// output: FileOutput(file: File('logs/app.log')),
|
|
||||||
);
|
|
||||||
43
lib/utils/minio_utils.dart
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter_common/utils/file_utils.dart';
|
||||||
|
import 'package:food_hub_app/config/app_config.dart';
|
||||||
|
import 'package:minio/io.dart';
|
||||||
|
import 'package:minio/minio.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class MinIOHelper {
|
||||||
|
static final MinIOHelper _instance = MinIOHelper._internal();
|
||||||
|
|
||||||
|
factory MinIOHelper() => _instance;
|
||||||
|
|
||||||
|
final String ip = AppConfig.rustfsIp;
|
||||||
|
|
||||||
|
MinIOHelper._internal() {
|
||||||
|
_minio = Minio(
|
||||||
|
endPoint: AppConfig.rustfsIp,
|
||||||
|
port: AppConfig.rustfsPort,
|
||||||
|
accessKey: AppConfig.rustfsAccessKey,
|
||||||
|
secretKey: AppConfig.rustfsSecretKey,
|
||||||
|
useSSL: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
late Minio _minio;
|
||||||
|
|
||||||
|
Future<String> uploadFile({
|
||||||
|
required PlatformFile file,
|
||||||
|
required String bucketName,
|
||||||
|
Function(double)? onProgress,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
String hashName = await generateMD5HashName(file.path!);
|
||||||
|
String fileName = '$hashName${getFileExtension(file.name)}';
|
||||||
|
|
||||||
|
await _minio.fPutObject(bucketName, fileName, file.path!);
|
||||||
|
|
||||||
|
return fileName;
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('文件上传失败: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
124
lib/utils/screenshot_util.dart
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter_common/utils/toast_util.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
|
/// 截图工具类
|
||||||
|
class ScreenshotUtil {
|
||||||
|
static double pixelRatio = 3.0;
|
||||||
|
static Duration delay = const Duration(milliseconds: 50);
|
||||||
|
static String watermarkText = '来自 食光集';
|
||||||
|
|
||||||
|
// 截图功能
|
||||||
|
static Future<Uint8List?> captureImage({required GlobalKey globalKey}) async {
|
||||||
|
// 等待一帧确保UI已渲染
|
||||||
|
await Future.delayed(delay);
|
||||||
|
final RenderObject? render = globalKey.currentContext!.findRenderObject();
|
||||||
|
final RenderRepaintBoundary boundary = render as RenderRepaintBoundary;
|
||||||
|
final ui.Image image = await boundary.toImage(pixelRatio: pixelRatio);
|
||||||
|
|
||||||
|
// 添加水印
|
||||||
|
return await _addWatermarkToImage(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加水印到图片
|
||||||
|
static Future<Uint8List> _addWatermarkToImage(ui.Image image) async {
|
||||||
|
// 创建画布
|
||||||
|
final recorder = ui.PictureRecorder();
|
||||||
|
final canvas = Canvas(recorder);
|
||||||
|
|
||||||
|
// 绘制原始图片
|
||||||
|
canvas.drawImage(image, Offset.zero, Paint());
|
||||||
|
|
||||||
|
// 添加文字水印
|
||||||
|
_drawTextWatermark(canvas, image);
|
||||||
|
|
||||||
|
// 生成最终图片
|
||||||
|
final picture = recorder.endRecording();
|
||||||
|
final watermarkedImage = await picture.toImage(image.width, image.height);
|
||||||
|
final ByteData? byteData = await watermarkedImage.toByteData(
|
||||||
|
format: ui.ImageByteFormat.png,
|
||||||
|
);
|
||||||
|
|
||||||
|
return byteData!.buffer.asUint8List();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制文字水印
|
||||||
|
static void _drawTextWatermark(Canvas canvas, ui.Image image) {
|
||||||
|
final textPainter = TextPainter(
|
||||||
|
text: TextSpan(
|
||||||
|
text: watermarkText,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0x99FFFFFF),
|
||||||
|
fontSize: 28,
|
||||||
|
fontFamily: 'CustomFont',
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
);
|
||||||
|
|
||||||
|
textPainter.layout(maxWidth: image.width.toDouble());
|
||||||
|
|
||||||
|
// 计算水印位置(右下角,带边距)
|
||||||
|
final double x = image.width - textPainter.width - 40;
|
||||||
|
final double y = image.height - textPainter.height - 40;
|
||||||
|
|
||||||
|
// 绘制文字背景
|
||||||
|
final backgroundPaint =
|
||||||
|
Paint()
|
||||||
|
..color = const ui.Color(0x4D000000)
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
|
// 绘制圆角矩形背景
|
||||||
|
final backgroundRect = RRect.fromRectAndRadius(
|
||||||
|
Rect.fromLTWH(
|
||||||
|
x - 12,
|
||||||
|
y - 6,
|
||||||
|
textPainter.width + 24,
|
||||||
|
textPainter.height + 12,
|
||||||
|
),
|
||||||
|
Radius.circular(6),
|
||||||
|
);
|
||||||
|
canvas.drawRRect(backgroundRect, backgroundPaint);
|
||||||
|
|
||||||
|
// 绘制文字
|
||||||
|
textPainter.paint(canvas, Offset(x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存图片到临时文件
|
||||||
|
static Future<File> saveImageToFile(Uint8List bytes, String fileName) async {
|
||||||
|
final directory = await getTemporaryDirectory();
|
||||||
|
final File imageFile = File('${directory.path}/$fileName');
|
||||||
|
await imageFile.writeAsBytes(bytes);
|
||||||
|
return imageFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> captureAndShare({
|
||||||
|
required GlobalKey globalKey,
|
||||||
|
required String fileName,
|
||||||
|
required String shareText,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final imageBytes = await captureImage(globalKey: globalKey);
|
||||||
|
|
||||||
|
if (imageBytes != null) {
|
||||||
|
final imageFile = await saveImageToFile(imageBytes, fileName);
|
||||||
|
await SharePlus.instance.share(
|
||||||
|
ShareParams(files: [XFile(imageFile.path)], text: shareText),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 分享后删除临时文件
|
||||||
|
await imageFile.delete();
|
||||||
|
} else {
|
||||||
|
ToastUtil.error('生成图片失败,请重试');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ToastUtil.error('分享失败: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
|
||||||
class SPUtil {
|
|
||||||
static late SharedPreferences _prefs;
|
|
||||||
|
|
||||||
/// 初始化
|
|
||||||
static Future<void> init() async {
|
|
||||||
_prefs = await SharedPreferences.getInstance();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 存储数据
|
|
||||||
static Future<bool> set(String key, dynamic value) {
|
|
||||||
if (value is String) return _prefs.setString(key, value);
|
|
||||||
if (value is int) return _prefs.setInt(key, value);
|
|
||||||
if (value is bool) return _prefs.setBool(key, value);
|
|
||||||
if (value is double) return _prefs.setDouble(key, value);
|
|
||||||
if (value is List<String>) return _prefs.setStringList(key, value);
|
|
||||||
throw ArgumentError('Unsupported value type: ${value.runtimeType}');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取数据
|
|
||||||
static dynamic get(String key, [dynamic defaultValue]) {
|
|
||||||
if (!_prefs.containsKey(key)) return defaultValue;
|
|
||||||
return _prefs.get(key) ?? defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取字符串
|
|
||||||
static String getString(String key, [String defaultValue = '']) {
|
|
||||||
return _prefs.getString(key) ?? defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取布尔值
|
|
||||||
static bool getBool(String key, [bool defaultValue = false]) {
|
|
||||||
return _prefs.getBool(key) ?? defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取整数
|
|
||||||
static int getInt(String key, [int defaultValue = 0]) {
|
|
||||||
return _prefs.getInt(key) ?? defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取浮点数
|
|
||||||
static double getDouble(String key, [double defaultValue = 0.0]) {
|
|
||||||
return _prefs.getDouble(key) ?? defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取字符串列表
|
|
||||||
static List<String> getStringList(String key, [List<String> defaultValue = const []]) {
|
|
||||||
return _prefs.getStringList(key) ?? defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 删除数据
|
|
||||||
static Future<bool> remove(String key) {
|
|
||||||
return _prefs.remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 清空所有数据
|
|
||||||
static Future<bool> clear() {
|
|
||||||
return _prefs.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3063
lib/views/data.dart
@@ -1,9 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:food_hub_app/layout/index.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';
|
||||||
import 'package:food_hub_app/views/stats.dart';
|
import 'package:food_hub_app/views/stats.dart';
|
||||||
|
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
|
||||||
|
import 'package:liquid_glass_widgets/widgets/shared/glass_page.dart';
|
||||||
|
import 'package:liquid_glass_widgets/widgets/surfaces/glass_app_bar.dart';
|
||||||
|
import 'package:liquid_glass_widgets/widgets/surfaces/glass_bottom_bar.dart';
|
||||||
|
import 'package:liquid_glass_widgets/widgets/surfaces/glass_scaffold.dart';
|
||||||
|
|
||||||
|
import 'moment.dart';
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
class HomePage extends StatefulWidget {
|
||||||
const HomePage({super.key});
|
const HomePage({super.key});
|
||||||
@@ -15,59 +20,52 @@ 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 _pages = [
|
||||||
RecordPage(),
|
const RecordPage(),
|
||||||
StatsPage(),
|
const StatsPage(),
|
||||||
MomentPage(),
|
const MomentPage(),
|
||||||
ProfilePage(),
|
const ProfilePage(),
|
||||||
];
|
];
|
||||||
|
|
||||||
bool isShow(int index) {
|
|
||||||
return index == 0 || index == 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
final colors = Theme.of(context).colorScheme;
|
||||||
appBar: AppBar(
|
|
||||||
centerTitle: true,
|
return GlassScaffold(
|
||||||
title: Text('Food Hub', style: TextStyle(color: Colors.white)),
|
statusBarStyle: GlassStatusBarStyle.auto,
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
bottomBar: GlassBottomBar(
|
||||||
leading: IconButton(
|
settings: LiquidGlassSettings(
|
||||||
icon: Icon(Icons.menu, color: Colors.white),
|
glassColor: colors.surface
|
||||||
onPressed: () {
|
|
||||||
// 打开侧边栏或菜单
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
actions: homeActions(),
|
selectedIndex: _currentIndex,
|
||||||
|
onTabSelected: (i) => setState(() => _currentIndex = i),
|
||||||
|
indicatorColor: colors.primary,
|
||||||
|
selectedIconColor: colors.surface,
|
||||||
|
unselectedIconColor: Color(0xFF000000),
|
||||||
|
tabs: const [
|
||||||
|
GlassBottomBarTab(
|
||||||
|
icon: Icon(Icons.home_outlined),
|
||||||
|
activeIcon: Icon(Icons.home),
|
||||||
|
label: '记录',
|
||||||
),
|
),
|
||||||
backgroundColor: Color(0xFFF5F5F5),
|
GlassBottomBarTab(
|
||||||
endDrawer: const SettingsDrawer(),
|
icon: Icon(Icons.pie_chart_outline),
|
||||||
body: _tabPages[_currentIndex],
|
activeIcon: Icon(Icons.pie_chart),
|
||||||
floatingActionButton:
|
label: '统计',
|
||||||
isShow(_currentIndex) ? addItemButton(context) : null,
|
|
||||||
bottomNavigationBar: NavBar(
|
|
||||||
currentIndex: _currentIndex,
|
|
||||||
onTap: (index) {
|
|
||||||
setState(() {
|
|
||||||
_currentIndex = index;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
GlassBottomBarTab(
|
||||||
|
icon: Icon(Icons.group_outlined),
|
||||||
|
activeIcon: Icon(Icons.group),
|
||||||
|
label: '朋友圈',
|
||||||
|
),
|
||||||
|
GlassBottomBarTab(
|
||||||
|
icon: Icon(Icons.account_circle_outlined),
|
||||||
|
activeIcon: Icon(Icons.account_circle),
|
||||||
|
label: '我的',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: _pages[_currentIndex],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,11 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_common/utils/sp_utils.dart';
|
||||||
|
import 'package:flutter_common/utils/toast_util.dart';
|
||||||
|
import 'package:flutter_common/widget/loading_widget.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:food_hub_app/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/widgets/common/form.dart';
|
||||||
import 'package:food_hub_app/widgets/common/index.dart';
|
import 'package:food_hub_app/widgets/common/index.dart';
|
||||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||||
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
|
||||||
|
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
const LoginPage({super.key});
|
const LoginPage({super.key});
|
||||||
@@ -20,6 +22,7 @@ class _LoginPage extends State<LoginPage> {
|
|||||||
String _username = "", _password = "";
|
String _username = "", _password = "";
|
||||||
bool _isRemember = false;
|
bool _isRemember = false;
|
||||||
bool _isObscure = true;
|
bool _isObscure = true;
|
||||||
|
bool _isLoading = false;
|
||||||
Color _eyeColor = Colors.grey;
|
Color _eyeColor = Colors.grey;
|
||||||
final List _loginMethod = [
|
final List _loginMethod = [
|
||||||
{"title": "phone", "icon": Icons.phone_android},
|
{"title": "phone", "icon": Icons.phone_android},
|
||||||
@@ -34,13 +37,22 @@ class _LoginPage extends State<LoginPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadRememberState() async {
|
Future<void> _loadRememberState() async {
|
||||||
bool? isRemember = await SPUtil.getBool('isRemember');
|
bool? isRemember = SPUtil.getBool('isRemember');
|
||||||
if (isRemember) {
|
if (isRemember == true) {
|
||||||
|
final username = SPUtil.getString('username');
|
||||||
|
final password = SPUtil.getString('password');
|
||||||
|
|
||||||
|
if (username.isNotEmpty && password.isNotEmpty) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isRemember = true;
|
_isRemember = true;
|
||||||
_username = SPUtil.getString('username');
|
_username = username;
|
||||||
_password = SPUtil.getString('password');
|
_password = password;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
loginClick();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,39 +72,53 @@ class _LoginPage extends State<LoginPage> {
|
|||||||
SPUtil.set('token', token);
|
SPUtil.set('token', token);
|
||||||
}
|
}
|
||||||
|
|
||||||
void loginClick(BuildContext context) async {
|
void handleUserInfo(User user) {
|
||||||
|
SPUtil.set('userId', user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loginClick() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
// 表单校验通过才会继续执行
|
// 表单校验通过才会继续执行
|
||||||
if ((_formKey.currentState as FormState).validate()) {
|
if ((_formKey.currentState as FormState).validate()) {
|
||||||
(_formKey.currentState as FormState).save();
|
(_formKey.currentState as FormState).save();
|
||||||
Session session = await loginApi(_username, _password);
|
|
||||||
showSuccessToast('登录成功');
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
LoadingDialog.show(context, message: '登录中');
|
||||||
|
Session session = await loginApi(_username, _password);
|
||||||
|
ToastUtil.success('登录成功');
|
||||||
handleRememberState();
|
handleRememberState();
|
||||||
handleTokenState(session.saToken.tokenValue);
|
handleTokenState(session.saToken.tokenValue);
|
||||||
|
handleUserInfo(session.userInfo);
|
||||||
|
|
||||||
|
LoadingDialog.hide(context);
|
||||||
Navigator.pushNamed(context, '/home');
|
Navigator.pushNamed(context, '/home');
|
||||||
} else {
|
} catch (e) {
|
||||||
showErrorToast('请先输入信息');
|
LoadingDialog.hide(context);
|
||||||
|
ToastUtil.error(e.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.grey[100],
|
|
||||||
body: Form(
|
body: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
// autovalidateMode: AutovalidateMode.onUserInteraction,
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// 可滚动的主要内容区域
|
Expanded(child: buildLoginForm(context)),
|
||||||
Expanded(child: buildLoginForm()),
|
|
||||||
|
|
||||||
// 固定在底部的其他登录选项区域
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 50),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 50),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
buildOtherLoginText(),
|
buildOtherLoginText(context),
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
buildOtherMethod(context),
|
buildOtherMethod(context),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
@@ -114,42 +140,36 @@ class _LoginPage extends State<LoginPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildLoginForm() {
|
Widget buildLoginForm(BuildContext context) {
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: kToolbarHeight),
|
const SizedBox(height: kToolbarHeight),
|
||||||
buildTitle(),
|
buildTitle(),
|
||||||
const SizedBox(height: 60),
|
const SizedBox(height: 60),
|
||||||
buildUsernameTextField(),
|
buildUsernameTextField(context),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
buildPasswordTextField(context),
|
buildPasswordTextField(context),
|
||||||
Row(
|
const SizedBox(height: 10),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
buildRememberPasswordCheckbox(context),
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
const SizedBox(height: 20),
|
||||||
children: [
|
|
||||||
buildRememberPasswordCheckbox(context), // 记住密码(左对齐)
|
|
||||||
buildForgetPasswordText(context),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 20), // 调整与登录按钮的间距
|
|
||||||
buildLoginButton(context),
|
buildLoginButton(context),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildUsernameTextField() {
|
Widget buildUsernameTextField(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
formLabelText(labelText: "账号:"),
|
Text("账号:", style: Theme.of(context).textTheme.bodyLarge),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
FormBuilderTextField(
|
FormBuilderTextField(
|
||||||
name: 'username',
|
name: 'username',
|
||||||
initialValue: _username,
|
initialValue: _username,
|
||||||
keyboardType: TextInputType.text,
|
keyboardType: TextInputType.text,
|
||||||
decoration: formInputDecoration(
|
decoration: buildInputDecoration(
|
||||||
|
context: context,
|
||||||
hintText: "请输入账号",
|
hintText: "请输入账号",
|
||||||
prefixIcon: Icons.person,
|
prefixIcon: Icons.person,
|
||||||
),
|
),
|
||||||
@@ -164,7 +184,7 @@ class _LoginPage extends State<LoginPage> {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
formLabelText(labelText: "密码:"),
|
Text("密码:", style: Theme.of(context).textTheme.bodyLarge),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
FormBuilderTextField(
|
FormBuilderTextField(
|
||||||
name: 'password',
|
name: 'password',
|
||||||
@@ -173,22 +193,18 @@ class _LoginPage extends State<LoginPage> {
|
|||||||
obscureText: _isObscure,
|
obscureText: _isObscure,
|
||||||
onSaved: (v) => _password = v!,
|
onSaved: (v) => _password = v!,
|
||||||
validator: FormBuilderValidators.required(),
|
validator: FormBuilderValidators.required(),
|
||||||
decoration: InputDecoration(
|
decoration: buildInputDecoration(
|
||||||
prefixIcon: Icon(Icons.lock),
|
context: context,
|
||||||
hintText: "请输入密码",
|
hintText: "请输入密码",
|
||||||
hintStyle: TextStyle(color: Colors.grey),
|
prefixIcon: Icons.lock,
|
||||||
border: OutlineInputBorder(),
|
).copyWith(
|
||||||
filled: true,
|
|
||||||
fillColor: Colors.white,
|
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(Icons.remove_red_eye, color: _eyeColor),
|
icon: Icon(Icons.remove_red_eye, color: _eyeColor),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
setState(() {
|
setState(() {
|
||||||
_isObscure = !_isObscure;
|
_isObscure = !_isObscure;
|
||||||
_eyeColor =
|
_eyeColor = (_isObscure ? Colors.grey : colors.surface);
|
||||||
(_isObscure
|
|
||||||
? Colors.grey
|
|
||||||
: Theme.of(context).iconTheme.color)!;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -202,6 +218,8 @@ class _LoginPage extends State<LoginPage> {
|
|||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Checkbox(
|
Checkbox(
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
value: _isRemember,
|
value: _isRemember,
|
||||||
onChanged: (bool? value) {
|
onChanged: (bool? value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -209,48 +227,45 @@ class _LoginPage extends State<LoginPage> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Text('记住密码', style: TextStyle(color: Colors.grey, fontSize: 14)),
|
Text('记住密码', style: Theme.of(context).textTheme.bodyMedium),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildForgetPasswordText(BuildContext context) {
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
print("忘记密码");
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
"忘记密码?",
|
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildLoginButton(BuildContext context) {
|
Widget buildLoginButton(BuildContext context) {
|
||||||
return Align(
|
return SizedBox(
|
||||||
child: SizedBox(
|
|
||||||
height: 45,
|
height: 45,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: TDButton(
|
child: ElevatedButton(
|
||||||
text: '登录',
|
onPressed: _isLoading ? null : loginClick,
|
||||||
size: TDButtonSize.large,
|
style: ElevatedButton.styleFrom(
|
||||||
type: TDButtonType.fill,
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
shape: TDButtonShape.rectangle,
|
foregroundColor: Colors.white,
|
||||||
theme: TDButtonTheme.primary,
|
elevation: 0,
|
||||||
onTap: () => loginClick(context),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'登录',
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildOtherLoginText() {
|
Widget buildOtherLoginText(BuildContext context) {
|
||||||
return TDDivider(text: '其他方式登录', alignment: TextAlignment.center);
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Divider(color: Colors.grey[300], thickness: 1)),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Text('其他方式登录', style: Theme.of(context).textTheme.bodyMedium),
|
||||||
|
),
|
||||||
|
Expanded(child: Divider(color: Colors.grey[300], thickness: 1)),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildOtherMethod(context) {
|
Widget buildOtherMethod(BuildContext context) {
|
||||||
return OverflowBar(
|
return OverflowBar(
|
||||||
alignment: MainAxisAlignment.center,
|
alignment: MainAxisAlignment.center,
|
||||||
children:
|
children:
|
||||||
@@ -283,16 +298,19 @@ class _LoginPage extends State<LoginPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildRegisterText(context) {
|
Widget buildRegisterText(BuildContext context) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Text('没有账号?', style: TextStyle(fontSize: 18)),
|
Text('没有账号?', style: Theme.of(context).textTheme.bodyMedium),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
child: const Text(
|
child: Text(
|
||||||
'点击注册',
|
'点击注册',
|
||||||
style: TextStyle(fontSize: 18, color: Colors.green),
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
print("点击注册");
|
print("点击注册");
|
||||||
|
|||||||
@@ -1,15 +1,137 @@
|
|||||||
|
import 'package:easy_refresh/easy_refresh.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_common/widget/common_widget.dart';
|
||||||
|
import 'package:food_hub_app/provider/food_provider.dart';
|
||||||
|
import 'package:food_hub_app/widgets/common/easy_refresh.dart';
|
||||||
|
import 'package:food_hub_app/widgets/moment/card.dart';
|
||||||
|
import 'package:provider/provider.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> {
|
||||||
|
// 初始化EasyRefresh控制器
|
||||||
|
final EasyRefreshController _freshController = EasyRefreshController(
|
||||||
|
controlFinishRefresh: true,
|
||||||
|
controlFinishLoad: true,
|
||||||
|
);
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
bool _showScrollToTop = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_scrollController.addListener(_onScroll);
|
||||||
|
|
||||||
|
// 初始化加载数据
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
context.read<FoodProvider>().refreshMomentList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.removeListener(_onScroll);
|
||||||
|
_freshController.dispose();
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下拉刷新
|
||||||
|
Future<void> _onRefresh() async {
|
||||||
|
await context.read<FoodProvider>().refreshMomentList();
|
||||||
|
_freshController.finishRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上拉加载
|
||||||
|
Future<void> _onLoad() async {
|
||||||
|
final provider = context.read<FoodProvider>();
|
||||||
|
if (provider.hasMore) {
|
||||||
|
await provider.loadMoreMomentList();
|
||||||
|
_freshController.finishLoad(IndicatorResult.success);
|
||||||
|
} else {
|
||||||
|
_freshController.finishLoad(IndicatorResult.noMore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onScroll() {
|
||||||
|
// 当滚动距离超过时显示返回顶部按钮
|
||||||
|
if (_scrollController.offset > 100) {
|
||||||
|
if (!_showScrollToTop) {
|
||||||
|
setState(() {
|
||||||
|
_showScrollToTop = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_showScrollToTop) {
|
||||||
|
setState(() {
|
||||||
|
_showScrollToTop = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动到顶部
|
||||||
|
void _scrollToTop() => scrollToTopAnimateTo(_scrollController);
|
||||||
|
|
||||||
|
Widget _buildMomentList(FoodProvider provider) {
|
||||||
|
return ListView.separated(
|
||||||
|
padding: const EdgeInsets.only(bottom: 80),
|
||||||
|
controller: _scrollController,
|
||||||
|
itemCount: provider.momentList.length,
|
||||||
|
separatorBuilder: (context, index) => SizedBox(height: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return MomentCard(moment: provider.momentList[index], isUser: false);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MomentPage extends State<MomentPage>{
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Center(child: Text("朋友圈"));
|
final provider = context.watch<FoodProvider>();
|
||||||
|
|
||||||
|
// 空状态显示
|
||||||
|
if (provider.momentList.isEmpty) {
|
||||||
|
return buildEmptyData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有数据时显示列表
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
title: Text('朋友圈', style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
centerTitle: true,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: EdgeInsetsGeometry.all(10),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
buildEasyRefresh(
|
||||||
|
freshController: _freshController,
|
||||||
|
onRefresh: _onRefresh,
|
||||||
|
onLoad: _onLoad,
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
if (provider.isLoading)
|
||||||
|
buildLoadingIndicator()
|
||||||
|
else
|
||||||
|
_buildMomentList(provider),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 返回顶部按钮
|
||||||
|
if (_showScrollToTop)
|
||||||
|
buildScrollToTop(context: context, scrollToTop: _scrollToTop),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
227
lib/views/moment_form.dart
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_common/utils/log_utils.dart';
|
||||||
|
import 'package:flutter_common/utils/toast_util.dart';
|
||||||
|
import 'package:flutter_common/widget/dialog_widget.dart';
|
||||||
|
import 'package:flutter_common/widget/loading_widget.dart';
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:food_hub_app/config/app_config.dart';
|
||||||
|
import 'package:food_hub_app/provider/food_provider.dart';
|
||||||
|
import 'package:food_hub_app/utils/minio_utils.dart';
|
||||||
|
import 'package:food_hub_app/widgets/common/form.dart';
|
||||||
|
import 'package:food_hub_app/widgets/common/image.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class MomentForm extends StatefulWidget {
|
||||||
|
const MomentForm({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MomentForm> createState() => _MomentFormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MomentFormState extends State<MomentForm> {
|
||||||
|
final _formKey = GlobalKey<FormBuilderState>();
|
||||||
|
final _focusNode = FocusNode();
|
||||||
|
static const String _contentField = 'content';
|
||||||
|
static const int maxContentLength = 200;
|
||||||
|
static const int maxImageCount = 9; // 最大图片数量
|
||||||
|
|
||||||
|
Widget _buildContentField(FoodProvider provider) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return FormBuilderTextField(
|
||||||
|
name: _contentField,
|
||||||
|
initialValue: provider.momentFormItem.content,
|
||||||
|
focusNode: _focusNode,
|
||||||
|
maxLines: 8,
|
||||||
|
minLines: 5,
|
||||||
|
maxLength: maxContentLength,
|
||||||
|
buildCounter: (
|
||||||
|
context, {
|
||||||
|
required currentLength,
|
||||||
|
required isFocused,
|
||||||
|
maxLength,
|
||||||
|
}) {
|
||||||
|
return Text(
|
||||||
|
'$currentLength/$maxContentLength',
|
||||||
|
style: TextStyle(
|
||||||
|
color: currentLength > maxLength! ? Colors.red : colors.primary,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onChanged: (value) {
|
||||||
|
provider.updateMomentFormItem(content: value);
|
||||||
|
},
|
||||||
|
decoration: buildInputDecoration(context: context, hintText: '请输入朋友圈内容'),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '请输入朋友圈内容';
|
||||||
|
}
|
||||||
|
if (value.length > maxContentLength) {
|
||||||
|
return '内容不能超过$maxContentLength字';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImageField(FoodProvider provider) {
|
||||||
|
final imageUrls = provider.momentFormItem.imageList;
|
||||||
|
final totalItems =
|
||||||
|
imageUrls.length + (imageUrls.length < maxImageCount ? 1 : 0);
|
||||||
|
|
||||||
|
return GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
childAspectRatio: 4 / 3,
|
||||||
|
),
|
||||||
|
itemCount: totalItems,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index < imageUrls.length) {
|
||||||
|
return ImagePreview(
|
||||||
|
imageUrls: imageUrls,
|
||||||
|
index: index,
|
||||||
|
onRemoveImage: () => _removeImage(provider, index),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 上传按钮(最后一个)
|
||||||
|
else {
|
||||||
|
return buildImageUploadButton(
|
||||||
|
context: context,
|
||||||
|
onTap: () => _pickImages(provider),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFormBuilder(FoodProvider provider) {
|
||||||
|
return FormBuilder(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
buildFormLabel(context: context, text: '朋友圈内容', isRequired: true),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildContentField(provider),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
buildFormLabel(
|
||||||
|
context: context,
|
||||||
|
text: '上传图片(最多9张)',
|
||||||
|
isRequired: false,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildImageField(provider),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
buildFormButtonGroup(
|
||||||
|
context: context,
|
||||||
|
isShowDelete: provider.isEditing,
|
||||||
|
onConfirm: () => _submitForm(provider),
|
||||||
|
onDelete: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 选择多张图片
|
||||||
|
Future<void> _pickImages(FoodProvider provider) async {
|
||||||
|
final currentCount = provider.momentFormItem.imageList.length;
|
||||||
|
final remainingCount = maxImageCount - currentCount;
|
||||||
|
|
||||||
|
if (remainingCount <= 0) {
|
||||||
|
ToastUtil.error('最多只能上传$maxImageCount张图片');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||||
|
type: FileType.custom,
|
||||||
|
allowedExtensions: ['jpg', 'jpeg', 'png'],
|
||||||
|
allowMultiple: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
// 限制选择数量
|
||||||
|
final filesToUpload = result.files.take(remainingCount).toList();
|
||||||
|
LoadingDialog.show(context, message: '上传中');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final newImageUrls = <String>[];
|
||||||
|
// 逐个上传图片
|
||||||
|
for (final file in filesToUpload) {
|
||||||
|
final fileName = await MinIOHelper().uploadFile(
|
||||||
|
bucketName: AppConfig.bucketName,
|
||||||
|
file: file,
|
||||||
|
);
|
||||||
|
logger.i('图片名称:$fileName');
|
||||||
|
newImageUrls.add(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新图片列表
|
||||||
|
provider.addMomentFormImage(newImageUrls);
|
||||||
|
} catch (e) {
|
||||||
|
ToastUtil.error(e.toString());
|
||||||
|
} finally {
|
||||||
|
LoadingDialog.hide(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 移除单张图片
|
||||||
|
void _removeImage(FoodProvider provider, int index) {
|
||||||
|
provider.removeMomentFormImage(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 提交表单
|
||||||
|
void _submitForm(FoodProvider provider) async {
|
||||||
|
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||||
|
LoadingDialog.show(context, message: '提交中');
|
||||||
|
final result = await provider.handleMoment();
|
||||||
|
if (result == true) {
|
||||||
|
await provider.refreshMomentList();
|
||||||
|
LoadingDialog.hide(context);
|
||||||
|
|
||||||
|
if (provider.isEditing) {
|
||||||
|
showSuccessTip(context, '更新朋友圈成功');
|
||||||
|
} else {
|
||||||
|
showSuccessTip(context, '新增朋友圈成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future.delayed(Duration(milliseconds: 1500), () {
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
LoadingDialog.hide(context);
|
||||||
|
if (provider.isEditing) {
|
||||||
|
showErrorTip(context, '更新朋友圈失败');
|
||||||
|
} else {
|
||||||
|
showErrorTip(context, '新增朋友圈失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final provider = context.watch<FoodProvider>();
|
||||||
|
|
||||||
|
return AnimatedPadding(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
padding: MediaQuery.of(context).viewInsets,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: _buildFormBuilder(provider),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
lib/views/moment_user.dart
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_common/utils/sp_utils.dart';
|
||||||
|
import 'package:flutter_common/widget/common_widget.dart';
|
||||||
|
import 'package:food_hub_app/provider/food_provider.dart';
|
||||||
|
import 'package:food_hub_app/widgets/moment/list.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class MomentUserPage extends StatefulWidget {
|
||||||
|
const MomentUserPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MomentUserPage> createState() => _MomentUserPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MomentUserPageState extends State<MomentUserPage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
// 初始化加载数据
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
context.read<FoodProvider>().queryMomentByUserId(SPUtil.getInt('userId'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final provider = context.watch<FoodProvider>();
|
||||||
|
|
||||||
|
// 空状态显示
|
||||||
|
if (provider.momentList.isEmpty) {
|
||||||
|
return buildEmptyData();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
title: Text('我的朋友圈', style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
centerTitle: true,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body:
|
||||||
|
provider.isLoading
|
||||||
|
? buildLoadingIndicator()
|
||||||
|
: Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: MomentList(
|
||||||
|
momentList: provider.momentList,
|
||||||
|
isUser: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,182 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_common/utils/sp_utils.dart';
|
||||||
|
import 'package:flutter_common/widget/common_widget.dart';
|
||||||
|
import 'package:flutter_common/widget/dialog_widget.dart';
|
||||||
|
import 'package:food_hub_app/provider/app_provider.dart';
|
||||||
|
import 'package:food_hub_app/provider/user_provider.dart';
|
||||||
|
import 'package:food_hub_app/views/profile_form.dart';
|
||||||
|
import 'package:food_hub_app/widgets/profile/basic_info.dart';
|
||||||
|
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
|
||||||
|
import 'package:liquid_glass_widgets/widgets/containers/glass_card.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class ProfilePage extends StatefulWidget {
|
class ProfilePage extends StatefulWidget {
|
||||||
const ProfilePage({super.key});
|
const ProfilePage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ProfilePage> createState() => _ProfilePage();
|
State<ProfilePage> createState() => ProfilePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProfilePageState extends State<ProfilePage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
// 初始化加载数据
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
context.read<UserProvider>().refreshUser(SPUtil.getInt('userId'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFunctionButton({
|
||||||
|
required IconData icon,
|
||||||
|
required String text,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
leading: Icon(icon, color: colors.primary),
|
||||||
|
title: Text(text),
|
||||||
|
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: colors.primary),
|
||||||
|
onTap: onTap,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapInfo(UserProvider provider) {
|
||||||
|
provider.initUserForm(provider.currentUser);
|
||||||
|
|
||||||
|
Navigator.pushNamed(context, '/profileForm');
|
||||||
|
// showModalBottomSheet(
|
||||||
|
// context: context,
|
||||||
|
// backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
// isScrollControlled: true,
|
||||||
|
// shape: const RoundedRectangleBorder(
|
||||||
|
// borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
// ),
|
||||||
|
// builder: (context) => ProfileForm(),
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapMoment() {
|
||||||
|
Navigator.pushNamed(context, '/momentUser');
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFunctionButtons(BuildContext context) {
|
||||||
|
final provider = context.watch<UserProvider>();
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return GlassCard(
|
||||||
|
padding: EdgeInsetsGeometry.symmetric(vertical: 0, horizontal: 10),
|
||||||
|
useOwnLayer: true,
|
||||||
|
settings: LiquidGlassSettings(glassColor: colors.surface),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildFunctionButton(
|
||||||
|
icon: Icons.account_circle,
|
||||||
|
text: '基本信息',
|
||||||
|
onTap: () => _onTapInfo(provider),
|
||||||
|
),
|
||||||
|
const Divider(height: 1, thickness: 0.2),
|
||||||
|
_buildFunctionButton(
|
||||||
|
icon: Icons.group_outlined,
|
||||||
|
text: '朋友圈',
|
||||||
|
onTap: _onTapMoment,
|
||||||
|
),
|
||||||
|
const Divider(height: 1, thickness: 0.2),
|
||||||
|
_buildFunctionButton(
|
||||||
|
icon: Icons.favorite,
|
||||||
|
text: '我的收藏',
|
||||||
|
onTap: () {
|
||||||
|
// 跳转到浏览历史页面
|
||||||
|
// LogUtils.i('点击浏览历史');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(height: 1, thickness: 0.2),
|
||||||
|
_buildFunctionButton(
|
||||||
|
icon: Icons.lock,
|
||||||
|
text: '更改密码',
|
||||||
|
onTap: () {
|
||||||
|
// 跳转到帮助与反馈页面
|
||||||
|
// LogUtils.i('点击帮助与反馈');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(height: 1, thickness: 0.2),
|
||||||
|
_buildFunctionButton(icon: Icons.message, text: '反馈意见', onTap: () {}),
|
||||||
|
const Divider(height: 1, thickness: 0.2),
|
||||||
|
_buildFunctionButton(icon: Icons.refresh, text: '检查更新', onTap: () {}),
|
||||||
|
const Divider(height: 1, thickness: 0.2),
|
||||||
|
_buildFunctionButton(
|
||||||
|
icon: Icons.exit_to_app,
|
||||||
|
text: '退出登录',
|
||||||
|
onTap: _onTapLogout,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapLogout() async {
|
||||||
|
final result = await showConfirmDialog(context, '确认要退出吗?');
|
||||||
|
if (result == true) {
|
||||||
|
SPUtil.remove('token');
|
||||||
|
Navigator.pop(context);
|
||||||
|
Navigator.pushNamed(context, '/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildVersionInfo() {
|
||||||
|
final provider = context.read<AppProvider>();
|
||||||
|
|
||||||
|
return Text(
|
||||||
|
'版本 v${provider.fullVersion}',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(UserProvider provider, BuildContext context) {
|
||||||
|
final user = provider.currentUser;
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(width: double.infinity, child: ProfileInfo(user: user)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildFunctionButtons(context),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildVersionInfo(),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ProfilePage extends State<ProfilePage>{
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Center(child: Text("个人主页"));
|
final provider = context.watch<UserProvider>();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
title: Text('我的', style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
centerTitle: true,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: EdgeInsetsGeometry.all(10),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
if (provider.isLoading)
|
||||||
|
buildLoadingIndicator()
|
||||||
|
else
|
||||||
|
SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.only(bottom: 90),
|
||||||
|
child: _buildContent(provider, context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
254
lib/views/profile_form.dart
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_common/widget/dialog_widget.dart';
|
||||||
|
import 'package:flutter_common/widget/loading_widget.dart';
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:flutter_input_chips/flutter_input_chips.dart';
|
||||||
|
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||||
|
import 'package:food_hub_app/provider/user_provider.dart';
|
||||||
|
import 'package:food_hub_app/widgets/common/form.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class ProfileForm extends StatefulWidget {
|
||||||
|
const ProfileForm({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ProfileForm> createState() => _ProfileFormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProfileFormState extends State<ProfileForm> {
|
||||||
|
final _formKey = GlobalKey<FormBuilderState>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFormBuilder(UserProvider provider) {
|
||||||
|
final double sizeBoxHeight = 8;
|
||||||
|
|
||||||
|
return FormBuilder(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
buildFormLabel(context: context, text: '用户名', isRequired: true),
|
||||||
|
SizedBox(height: sizeBoxHeight),
|
||||||
|
FormBuilderTextField(
|
||||||
|
name: 'username',
|
||||||
|
initialValue: provider.userFormItem.username,
|
||||||
|
onChanged: (value) {
|
||||||
|
provider.updateUserFormItem(username: value);
|
||||||
|
},
|
||||||
|
decoration: buildInputDecoration(
|
||||||
|
context: context,
|
||||||
|
hintText: '请输入用户名',
|
||||||
|
),
|
||||||
|
validator: FormBuilderValidators.required(errorText: '请输入用户名'),
|
||||||
|
),
|
||||||
|
SizedBox(height: sizeBoxHeight),
|
||||||
|
|
||||||
|
buildFormLabel(context: context, text: '性别', isRequired: true),
|
||||||
|
SizedBox(height: sizeBoxHeight),
|
||||||
|
FormBuilderDropdown<int>(
|
||||||
|
name: 'gender',
|
||||||
|
initialValue: provider.userFormItem.gender,
|
||||||
|
decoration: buildInputDecoration(
|
||||||
|
context: context,
|
||||||
|
hintText: '请选择性别',
|
||||||
|
),
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(value: 1, child: Text('男')),
|
||||||
|
DropdownMenuItem(value: 2, child: Text('女')),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
provider.updateUserFormItem(gender: value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: sizeBoxHeight),
|
||||||
|
|
||||||
|
buildFormLabel(context: context, text: '手机号', isRequired: true),
|
||||||
|
SizedBox(height: sizeBoxHeight),
|
||||||
|
FormBuilderTextField(
|
||||||
|
name: 'phoneNumber',
|
||||||
|
initialValue: provider.userFormItem.phoneNumber,
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
decoration: buildInputDecoration(
|
||||||
|
context: context,
|
||||||
|
hintText: '请输入手机号',
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
provider.updateUserFormItem(phoneNumber: value);
|
||||||
|
},
|
||||||
|
validator: FormBuilderValidators.compose([
|
||||||
|
FormBuilderValidators.required(errorText: '请输入手机号'),
|
||||||
|
FormBuilderValidators.match(
|
||||||
|
RegExp(r'^1[3-9]\d{9}$'),
|
||||||
|
errorText: '请输入正确的11位手机号',
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
SizedBox(height: sizeBoxHeight),
|
||||||
|
|
||||||
|
buildFormLabel(context: context, text: '邮箱', isRequired: true),
|
||||||
|
SizedBox(height: sizeBoxHeight),
|
||||||
|
FormBuilderTextField(
|
||||||
|
name: 'email',
|
||||||
|
initialValue: provider.userFormItem.email,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
decoration: buildInputDecoration(
|
||||||
|
context: context,
|
||||||
|
hintText: '请输入邮箱',
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
provider.updateUserFormItem(email: value);
|
||||||
|
},
|
||||||
|
validator: FormBuilderValidators.compose([
|
||||||
|
FormBuilderValidators.email(errorText: '邮箱格式不正确'),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
SizedBox(height: sizeBoxHeight),
|
||||||
|
|
||||||
|
buildFormLabel(context: context, text: '出生日期', isRequired: true),
|
||||||
|
SizedBox(height: sizeBoxHeight),
|
||||||
|
FormBuilderDateTimePicker(
|
||||||
|
name: 'birthDate',
|
||||||
|
initialValue: provider.userFormItem.birthDate,
|
||||||
|
inputType: InputType.date,
|
||||||
|
format: DateFormat('yyyy-MM-dd'),
|
||||||
|
firstDate: DateTime(1900),
|
||||||
|
lastDate: DateTime.now(),
|
||||||
|
decoration: buildInputDecoration(
|
||||||
|
context: context,
|
||||||
|
hintText: '请选择出生日期',
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
provider.updateUserFormItem(birthDate: value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: sizeBoxHeight),
|
||||||
|
|
||||||
|
buildFormLabel(context: context, text: '详细地址', isRequired: false),
|
||||||
|
SizedBox(height: sizeBoxHeight),
|
||||||
|
FormBuilderTextField(
|
||||||
|
name: 'address',
|
||||||
|
initialValue: provider.userFormItem.address,
|
||||||
|
decoration: buildInputDecoration(
|
||||||
|
context: context,
|
||||||
|
hintText: '请输入详细地址',
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
provider.updateUserFormItem(address: value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: sizeBoxHeight),
|
||||||
|
|
||||||
|
buildFormLabel(context: context, text: '职业', isRequired: false),
|
||||||
|
SizedBox(height: sizeBoxHeight),
|
||||||
|
FormBuilderTextField(
|
||||||
|
name: 'job',
|
||||||
|
initialValue: provider.userFormItem.job,
|
||||||
|
decoration: buildInputDecoration(
|
||||||
|
context: context,
|
||||||
|
hintText: '请输入职业',
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
provider.updateUserFormItem(job: value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: sizeBoxHeight),
|
||||||
|
|
||||||
|
buildFormLabel(context: context, text: '标签', isRequired: false),
|
||||||
|
SizedBox(height: sizeBoxHeight),
|
||||||
|
FormBuilderField<List<String>>(
|
||||||
|
name: 'tags',
|
||||||
|
initialValue: provider.userFormItem.tags,
|
||||||
|
builder: (FormFieldState<List<String>> field) {
|
||||||
|
return FlutterInputChips(
|
||||||
|
padding: EdgeInsets.all(0),
|
||||||
|
initialValue: field.value ?? [],
|
||||||
|
onChanged: (value) {
|
||||||
|
provider.updateUserFormItem(tags: value);
|
||||||
|
},
|
||||||
|
inputDecoration: buildInputDecoration(
|
||||||
|
context: context,
|
||||||
|
hintText: '请输入标签,多个标签用回车键隔开',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: sizeBoxHeight),
|
||||||
|
|
||||||
|
buildFormLabel(context: context, text: '简介', isRequired: false),
|
||||||
|
SizedBox(height: sizeBoxHeight),
|
||||||
|
FormBuilderTextField(
|
||||||
|
name: 'description',
|
||||||
|
initialValue: provider.userFormItem.description,
|
||||||
|
decoration: buildInputDecoration(
|
||||||
|
context: context,
|
||||||
|
hintText: '请输入简介',
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
provider.updateUserFormItem(description: value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: sizeBoxHeight),
|
||||||
|
|
||||||
|
buildFormButtonGroup(
|
||||||
|
context: context,
|
||||||
|
isShowDelete: false,
|
||||||
|
onConfirm: () => _submitForm(provider),
|
||||||
|
onDelete: () => {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 提交表单
|
||||||
|
void _submitForm(UserProvider provider) async {
|
||||||
|
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||||
|
LoadingDialog.show(context, message: '提交中');
|
||||||
|
|
||||||
|
final result = await provider.handleUser();
|
||||||
|
if (result == true) {
|
||||||
|
LoadingDialog.hide(context);
|
||||||
|
|
||||||
|
showSuccessTip(context, '更新信息成功');
|
||||||
|
|
||||||
|
Future.delayed(Duration(milliseconds: 1500), () {
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
LoadingDialog.hide(context);
|
||||||
|
showErrorTip(context, '更新信息失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final provider = context.watch<UserProvider>();
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('基本信息', style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: Icon(Icons.arrow_back, color: colors.primary),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: _buildFormBuilder(provider),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
109
lib/views/profile_user.dart
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_common/widget/common_widget.dart';
|
||||||
|
import 'package:food_hub_app/provider/user_provider.dart';
|
||||||
|
import 'package:food_hub_app/widgets/common/index.dart';
|
||||||
|
import 'package:food_hub_app/widgets/moment/list.dart';
|
||||||
|
import 'package:food_hub_app/widgets/profile/basic_info.dart';
|
||||||
|
import 'package:food_hub_app/widgets/recipe/list.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
enum ProfileTab {
|
||||||
|
profile('基础信息'),
|
||||||
|
moment('朋友圈'),
|
||||||
|
recipe('菜谱');
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
const ProfileTab(this.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProfileUserPage extends StatefulWidget {
|
||||||
|
const ProfileUserPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ProfileUserPage> createState() => ProfileUserPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProfileUserPageState extends State<ProfileUserPage> {
|
||||||
|
late ProfileTab currentTab = ProfileTab.profile;
|
||||||
|
|
||||||
|
void _onTabChange(ProfileTab tab) {
|
||||||
|
setState(() {
|
||||||
|
currentTab = tab;
|
||||||
|
});
|
||||||
|
|
||||||
|
// provider.onRecordTabChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTabs({
|
||||||
|
required BuildContext context,
|
||||||
|
required ProfileTab currentTab,
|
||||||
|
required ValueChanged<ProfileTab> onTabChanged,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(8),
|
||||||
|
child: buildToggleSwitch(
|
||||||
|
context: context,
|
||||||
|
currentTab: currentTab,
|
||||||
|
tabValues: ProfileTab.values,
|
||||||
|
labels: ProfileTab.values.map((e) => e.label).toList(),
|
||||||
|
onTabChanged: onTabChanged,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(UserProvider provider) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildTabs(
|
||||||
|
context: context,
|
||||||
|
currentTab: currentTab,
|
||||||
|
onTabChanged: (tab) => _onTabChange(tab),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: IndexedStack(
|
||||||
|
index: currentTab.index,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity, child: ProfileInfo(user: provider.currentUser)
|
||||||
|
),
|
||||||
|
MomentList(momentList: provider.momentList, isUser: false),
|
||||||
|
RecipeList(userId: provider.currentUser.id!),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final provider = context.watch<UserProvider>();
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
'${provider.currentUser.username}的个人信息',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: Icon(Icons.arrow_back, color: colors.primary),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
if (provider.isLoading)
|
||||||
|
buildLoadingIndicator()
|
||||||
|
else
|
||||||
|
_buildContent(provider),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
291
lib/views/recipe_detail.dart
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_common/widget/common_widget.dart';
|
||||||
|
import 'package:food_hub_app/apis/recipe.dart';
|
||||||
|
import 'package:food_hub_app/models/recipe.dart';
|
||||||
|
import 'package:food_hub_app/provider/food_provider.dart';
|
||||||
|
import 'package:food_hub_app/utils/screenshot_util.dart';
|
||||||
|
import 'package:food_hub_app/views/recipe_form.dart';
|
||||||
|
import 'package:food_hub_app/widgets/common/index.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class RecipeDetailPage extends StatefulWidget {
|
||||||
|
const RecipeDetailPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RecipeDetailPage> createState() => _RecipeDetailState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||||
|
Recipe recipe = Recipe.getEmpty();
|
||||||
|
final GlobalKey _recipeDetailKey = GlobalKey();
|
||||||
|
bool _isSharing = false;
|
||||||
|
|
||||||
|
List<RecipeMaterial> materialList = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
refreshRecipeDetail();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshRecipeDetail() async {
|
||||||
|
// 获取参数
|
||||||
|
final args = ModalRoute.of(context)?.settings.arguments as Map;
|
||||||
|
final result = await queryRecipeByIdApi(args['id']);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
recipe = result;
|
||||||
|
getRecipeMaterial();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void getRecipeMaterial() {
|
||||||
|
materialList.clear();
|
||||||
|
|
||||||
|
for (var material in recipe.materialList) {
|
||||||
|
materialList.add(material);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleEditRecipe(FoodProvider provider) {
|
||||||
|
provider.initRecipeForm(recipe);
|
||||||
|
provider.isEditing = true;
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
constraints: BoxConstraints(minHeight: 600),
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
isScrollControlled: true,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
),
|
||||||
|
builder: (context) => RecipeForm(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分享图片
|
||||||
|
Future<void> _shareRecipeAsImage() async {
|
||||||
|
if (_isSharing) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isSharing = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
final imageName = '${recipe.name}_$timestamp.png';
|
||||||
|
await ScreenshotUtil.captureAndShare(
|
||||||
|
globalKey: _recipeDetailKey,
|
||||||
|
fileName: imageName,
|
||||||
|
shareText: '分享菜谱: ${recipe.name}',
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isSharing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
final provider = context.watch<FoodProvider>();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('菜谱信息', style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: Icon(Icons.arrow_back, color: colors.primary),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// 用RepaintBoundary包裹要截图的部分
|
||||||
|
RepaintBoundary(
|
||||||
|
key: _recipeDetailKey,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(10),
|
||||||
|
child: _buildRecipeDetail(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildButtons(provider),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRecipeDetail() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_buildTitleSection(),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
_buildInfoCard(
|
||||||
|
context,
|
||||||
|
icon: Icons.info,
|
||||||
|
title: "基础信息",
|
||||||
|
content: Text(recipe.category),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
_buildInfoCard(
|
||||||
|
context,
|
||||||
|
icon: Icons.star,
|
||||||
|
title: "推荐指数",
|
||||||
|
content: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: List.generate(5, (index) {
|
||||||
|
return Icon(
|
||||||
|
index < recipe.recommendRate ? Icons.star : Icons.star_border,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
size: 20,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
_buildInfoCard(
|
||||||
|
context,
|
||||||
|
icon: Icons.shopping_cart,
|
||||||
|
title: "食材信息",
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [_buildMaterialSection(materialList)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
_buildInfoCard(
|
||||||
|
context,
|
||||||
|
icon: Icons.list,
|
||||||
|
title: "步骤信息",
|
||||||
|
content: Column(children: [_buildStepSection(recipe.stepList)]),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
_buildInfoCard(
|
||||||
|
context,
|
||||||
|
icon: Icons.note,
|
||||||
|
title: "其他信息",
|
||||||
|
content: Text(
|
||||||
|
'备注:${recipe.remark.isEmpty ? '无' : recipe.remark}',
|
||||||
|
style: const TextStyle(height: 1.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTitleSection() {
|
||||||
|
return Text(
|
||||||
|
recipe.name,
|
||||||
|
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMaterialSection(List<RecipeMaterial> materials) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (materials.isEmpty)
|
||||||
|
buildTag(context, '无')
|
||||||
|
else
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 8.0,
|
||||||
|
children: materials.map((m) => _buildMaterialItem(m)).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMaterialItem(RecipeMaterial material) {
|
||||||
|
return Text('${material.name} ${material.amount}');
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStepSection(List<RecipeStep> steps) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (steps.isEmpty)
|
||||||
|
buildTag(context, '无')
|
||||||
|
else
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 8.0,
|
||||||
|
children: steps.map((m) => _buildStepItem(m)).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStepItem(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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoCard(
|
||||||
|
BuildContext context, {
|
||||||
|
required IconData icon,
|
||||||
|
required String title,
|
||||||
|
required Widget content,
|
||||||
|
}) {
|
||||||
|
return CommonCard(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: Theme.of(context).colorScheme.primary),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 5),
|
||||||
|
content,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildButtons(FoodProvider provider) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
circleIconButton(
|
||||||
|
context: context,
|
||||||
|
icon: Icons.edit,
|
||||||
|
onPressed: () => _handleEditRecipe(provider),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
circleIconButton(
|
||||||
|
context: context,
|
||||||
|
icon: _isSharing ? Icons.hourglass_top : Icons.share,
|
||||||
|
onPressed: _shareRecipeAsImage,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
569
lib/views/recipe_form.dart
Normal file
@@ -0,0 +1,569 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_common/widget/dialog_widget.dart';
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:food_hub_app/models/recipe.dart';
|
||||||
|
import 'package:food_hub_app/provider/food_provider.dart';
|
||||||
|
import 'package:food_hub_app/widgets/common/form.dart';
|
||||||
|
import 'package:food_hub_app/widgets/common/index.dart';
|
||||||
|
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||||
|
import 'package:flutter_common/utils/toast_util.dart';
|
||||||
|
import 'package:flutter_common/widget/loading_widget.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class RecipeForm extends StatefulWidget {
|
||||||
|
const RecipeForm({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RecipeForm> createState() => _RecipeFormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecipeFormState extends State<RecipeForm> {
|
||||||
|
final _formKey = GlobalKey<FormBuilderState>();
|
||||||
|
int _currentStep = 0;
|
||||||
|
|
||||||
|
// 表单字段名称常量
|
||||||
|
static const String _nameField = 'name';
|
||||||
|
static const String _categoryField = 'category';
|
||||||
|
static const String _recommendRateField = 'recommendRate';
|
||||||
|
static const String _remarkField = 'remark';
|
||||||
|
static const String _isShareField = 'isShare';
|
||||||
|
|
||||||
|
// 分类选项
|
||||||
|
final List<String> _categoryOptions = [
|
||||||
|
'家常菜',
|
||||||
|
'川菜',
|
||||||
|
'粤菜',
|
||||||
|
'湘菜',
|
||||||
|
'西餐',
|
||||||
|
'甜品',
|
||||||
|
'汤羹',
|
||||||
|
'其他',
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建步骤内容
|
||||||
|
Widget _buildStepContent(FoodProvider provider) {
|
||||||
|
switch (_currentStep) {
|
||||||
|
case 0:
|
||||||
|
return _buildInfo(provider);
|
||||||
|
case 1:
|
||||||
|
return _buildMaterials(provider);
|
||||||
|
case 2:
|
||||||
|
return _buildSteps(provider);
|
||||||
|
default:
|
||||||
|
return _buildInfo(provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第一步:基本信息
|
||||||
|
Widget _buildInfo(FoodProvider provider) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
buildFormLabel(context: context, text: '菜谱名称', isRequired: true),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
FormBuilderTextField(
|
||||||
|
name: _nameField,
|
||||||
|
initialValue: provider.recipeFormItem.name,
|
||||||
|
decoration: buildInputDecoration(
|
||||||
|
context: context,
|
||||||
|
hintText: '请输入菜谱名称',
|
||||||
|
prefixIcon: Icons.restaurant_menu,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
provider.updateRecipeFormItem(name: value ?? '');
|
||||||
|
},
|
||||||
|
validator: FormBuilderValidators.compose([
|
||||||
|
FormBuilderValidators.required(errorText: '菜谱名称不能为空'),
|
||||||
|
FormBuilderValidators.maxLength(50, errorText: '名称不能超过50个字符'),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
buildFormLabel(context: context, text: '分类', isRequired: true),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
FormBuilderDropdown(
|
||||||
|
name: _categoryField,
|
||||||
|
initialValue: provider.recipeFormItem.category,
|
||||||
|
decoration: buildInputDecoration(
|
||||||
|
context: context,
|
||||||
|
hintText: '请选择分类',
|
||||||
|
prefixIcon: Icons.widgets,
|
||||||
|
),
|
||||||
|
items:
|
||||||
|
_categoryOptions
|
||||||
|
.map(
|
||||||
|
(category) => DropdownMenuItem(
|
||||||
|
value: category,
|
||||||
|
child: Text(category),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onChanged: (value) {
|
||||||
|
provider.updateRecipeFormItem(category: value ?? '');
|
||||||
|
},
|
||||||
|
validator: FormBuilderValidators.required(errorText: '请选择分类'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
buildFormLabel(context: context, text: '推荐评分', isRequired: false),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
FormBuilderSlider(
|
||||||
|
name: _recommendRateField,
|
||||||
|
initialValue: provider.recipeFormItem.recommendRate,
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
divisions: 4,
|
||||||
|
activeColor: Theme.of(context).colorScheme.primary,
|
||||||
|
decoration: const InputDecoration(border: InputBorder.none),
|
||||||
|
onChanged: (value) {
|
||||||
|
provider.updateRecipeFormItem(recommendRate: value ?? 0);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
buildFormLabel(context: context, text: '备注', isRequired: false),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
FormBuilderTextField(
|
||||||
|
name: _remarkField,
|
||||||
|
initialValue: provider.recipeFormItem.remark,
|
||||||
|
maxLines: 3,
|
||||||
|
decoration: buildInputDecoration(
|
||||||
|
context: context,
|
||||||
|
hintText: '请输入菜谱的特别说明或小贴士',
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
provider.updateRecipeFormItem(remark: value ?? '');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
FormBuilderCheckbox(
|
||||||
|
name: _isShareField,
|
||||||
|
initialValue: provider.recipeFormItem.isShare,
|
||||||
|
title: const Text('公开分享此菜谱'),
|
||||||
|
onChanged: (value) {
|
||||||
|
provider.updateRecipeFormItem(isShare: value ?? false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第二步:食材清单
|
||||||
|
Widget _buildMaterials(FoodProvider provider) {
|
||||||
|
final materials = provider.recipeFormItem.materialList;
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text('食材清单', style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.add,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
onPressed: () => provider.addRecipeMaterial(),
|
||||||
|
tooltip: '添加食材',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (materials.isEmpty)
|
||||||
|
Container(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
margin: EdgeInsets.only(bottom: 10),
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.inventory_2, size: 48, color: Colors.grey[400]),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'暂无食材,请点击添加按钮添加食材',
|
||||||
|
style: TextStyle(color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Column(
|
||||||
|
children:
|
||||||
|
materials.asMap().entries.map((entry) {
|
||||||
|
final index = entry.key;
|
||||||
|
final material = entry.value;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildMaterialItem(index, material, provider),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMaterialItem(
|
||||||
|
int index,
|
||||||
|
RecipeMaterial material,
|
||||||
|
FoodProvider provider,
|
||||||
|
) {
|
||||||
|
return Column(
|
||||||
|
key: ValueKey(material.id),
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
initialValue: material.name,
|
||||||
|
decoration: buildInputDecoration(
|
||||||
|
context: context,
|
||||||
|
hintText: '请输入名称',
|
||||||
|
prefixIcon: Icons.content_paste,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
provider.updateRecipeMaterial(index, name: value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
initialValue: material.amount,
|
||||||
|
decoration: buildInputDecoration(
|
||||||
|
context: context,
|
||||||
|
hintText: '请输入用量',
|
||||||
|
prefixIcon: Icons.scale,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
provider.updateRecipeMaterial(index, amount: value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete, color: Colors.red, size: 20),
|
||||||
|
onPressed: () => provider.removeRecipeMaterial(index),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第三步:制作步骤
|
||||||
|
Widget _buildSteps(FoodProvider provider) {
|
||||||
|
final steps = provider.recipeFormItem.stepList;
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text('制作步骤', style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.add,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
onPressed: () => provider.addRecipeStep(),
|
||||||
|
tooltip: '添加步骤',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (steps.isEmpty)
|
||||||
|
Container(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
margin: EdgeInsets.only(bottom: 10),
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.list_alt, size: 48, color: Colors.grey[400]),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'暂无步骤,请点击添加按钮添加制作步骤',
|
||||||
|
style: TextStyle(color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Column(
|
||||||
|
children:
|
||||||
|
steps.asMap().entries.map((entry) {
|
||||||
|
final index = entry.key;
|
||||||
|
final step = entry.value;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildStepItem(index, step, provider),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStepItem(int index, RecipeStep step, FoodProvider provider) {
|
||||||
|
return Row(
|
||||||
|
key: ValueKey(step.id),
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// 步骤编号
|
||||||
|
Container(
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'${index + 1}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
|
// 步骤内容
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
initialValue: step.content,
|
||||||
|
decoration: buildInputDecoration(
|
||||||
|
context: context,
|
||||||
|
hintText: '请输入内容',
|
||||||
|
prefixIcon: Icons.content_paste,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
provider.updateRecipeStep(index, content: value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 删除按钮
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete, color: Colors.red),
|
||||||
|
onPressed: () => provider.removeRecipeStep(index),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步骤指示器
|
||||||
|
Widget _buildStepIndicator() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_buildStepCircle(0),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
_buildStepConnector(0),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
_buildStepCircle(1),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
_buildStepConnector(1),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
_buildStepCircle(2),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(top: 8),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_buildStepTitle(0, '基本信息'),
|
||||||
|
SizedBox(width: 40),
|
||||||
|
_buildStepTitle(1, '食材清单'),
|
||||||
|
SizedBox(width: 40),
|
||||||
|
_buildStepTitle(2, '制作步骤'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只构建圆圈,不包含标题文字
|
||||||
|
Widget _buildStepCircle(int stepIndex) {
|
||||||
|
final isActive = _currentStep == stepIndex;
|
||||||
|
final isCompleted = _currentStep > stepIndex;
|
||||||
|
final boxColor =
|
||||||
|
isActive || isCompleted
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: Colors.grey;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
decoration: BoxDecoration(color: boxColor, shape: BoxShape.circle),
|
||||||
|
child: Center(
|
||||||
|
child:
|
||||||
|
isCompleted
|
||||||
|
? const Icon(Icons.check, color: Colors.white, size: 16)
|
||||||
|
: Text(
|
||||||
|
'${stepIndex + 1}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 箭头连接器
|
||||||
|
Widget _buildStepConnector(int stepIndex) {
|
||||||
|
final isActive = _currentStep > stepIndex;
|
||||||
|
return SizedBox(
|
||||||
|
width: 40,
|
||||||
|
child: Icon(
|
||||||
|
Icons.arrow_forward,
|
||||||
|
size: 20,
|
||||||
|
color: isActive ? Theme.of(context).colorScheme.primary : Colors.grey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStepTitle(int stepIndex, String title) {
|
||||||
|
final isActive = _currentStep == stepIndex;
|
||||||
|
final isCompleted = _currentStep > stepIndex;
|
||||||
|
final textColor =
|
||||||
|
isActive || isCompleted
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: Colors.grey;
|
||||||
|
|
||||||
|
return Text(
|
||||||
|
title,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
|
||||||
|
color: textColor,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导航按钮
|
||||||
|
Widget _buildNavigationButtons(FoodProvider provider) {
|
||||||
|
final isLastStep = _currentStep == 2;
|
||||||
|
final isFirstStep = _currentStep == 0;
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
if (!isFirstStep)
|
||||||
|
Expanded(
|
||||||
|
child: buildInfoButton(
|
||||||
|
context: context,
|
||||||
|
text: '上一步',
|
||||||
|
onPressed: _previousStep,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!isFirstStep) const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: buildPrimaryButton(
|
||||||
|
context: context,
|
||||||
|
text: isLastStep ? '提交' : '下一步',
|
||||||
|
onPressed: () => _nextStep(provider),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _nextStep(FoodProvider provider) {
|
||||||
|
if (_currentStep < 2) {
|
||||||
|
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||||
|
setState(() {
|
||||||
|
_currentStep++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_submitForm(provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _previousStep() {
|
||||||
|
if (_currentStep > 0) {
|
||||||
|
setState(() {
|
||||||
|
_currentStep--;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _submitForm(FoodProvider provider) async {
|
||||||
|
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||||
|
LoadingDialog.show(context, message: '提交中');
|
||||||
|
final result = await provider.handleRecipe();
|
||||||
|
if (result == true) {
|
||||||
|
LoadingDialog.hide(context);
|
||||||
|
|
||||||
|
if (provider.isEditing) {
|
||||||
|
showSuccessTip(context, '更新菜谱成功');
|
||||||
|
} else {
|
||||||
|
showSuccessTip(context, '新增菜谱成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future.delayed(Duration(milliseconds: 1500), () {
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
LoadingDialog.hide(context);
|
||||||
|
if (provider.isEditing) {
|
||||||
|
showErrorTip(context, '更新菜谱失败');
|
||||||
|
} else {
|
||||||
|
showErrorTip(context, '新增菜谱失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ToastUtil.error('请检查表单填写是否正确');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final provider = context.watch<FoodProvider>();
|
||||||
|
|
||||||
|
return AnimatedPadding(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
padding: MediaQuery.of(context).viewInsets,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildStepIndicator(),
|
||||||
|
FormBuilder(key: _formKey, child: _buildStepContent(provider)),
|
||||||
|
_buildNavigationButtons(provider),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,23 @@
|
|||||||
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/provider/food_provider.dart';
|
||||||
import 'package:food_hub_app/widgets/recipe/recipe_list.dart';
|
import 'package:food_hub_app/views/moment_form.dart';
|
||||||
import 'package:food_hub_app/widgets/recipe/recipe_timeline.dart';
|
import 'package:food_hub_app/views/recipe_form.dart';
|
||||||
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
import 'package:food_hub_app/views/record_form.dart';
|
||||||
|
import 'package:food_hub_app/widgets/common/index.dart';
|
||||||
|
import 'package:food_hub_app/widgets/recipe/calendar.dart';
|
||||||
|
import 'package:food_hub_app/widgets/recipe/list.dart';
|
||||||
|
import 'package:food_hub_app/widgets/recipe/timeline.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
enum RecordTab {
|
||||||
|
recipe('菜谱'),
|
||||||
|
calendar('日历'),
|
||||||
|
timeline('时间轴');
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
const RecordTab(this.label);
|
||||||
|
}
|
||||||
|
|
||||||
class RecordPage extends StatefulWidget {
|
class RecordPage extends StatefulWidget {
|
||||||
const RecordPage({super.key});
|
const RecordPage({super.key});
|
||||||
@@ -12,18 +26,11 @@ class RecordPage extends StatefulWidget {
|
|||||||
State<StatefulWidget> createState() => _RecordPageState();
|
State<StatefulWidget> createState() => _RecordPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RecordPageState extends State<RecordPage>
|
class _RecordPageState extends State<RecordPage> {
|
||||||
with SingleTickerProviderStateMixin {
|
final List<Widget> _tabPages = [
|
||||||
|
RecipeList(userId: 0),
|
||||||
late final TabController _tabController = TabController(
|
RecipeCalendar(),
|
||||||
length: 3,
|
RecipeTimeline(),
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
|
|
||||||
final List<TDTab> tabs = [
|
|
||||||
const TDTab(text: '菜谱', icon: Icon(Icons.book)),
|
|
||||||
const TDTab(text: '日历', icon: Icon(Icons.calendar_month)),
|
|
||||||
const TDTab(text: '时间轴', icon: Icon(Icons.timeline)),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -33,37 +40,176 @@ class _RecordPageState extends State<RecordPage>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_tabController.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildTabs({
|
||||||
|
required BuildContext context,
|
||||||
|
required RecordTab currentTab,
|
||||||
|
required ValueChanged<RecordTab> onTabChanged,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(8),
|
||||||
|
child: buildToggleSwitch(
|
||||||
|
context: context,
|
||||||
|
currentTab: currentTab,
|
||||||
|
tabValues: RecordTab.values,
|
||||||
|
labels: RecordTab.values.map((e) => e.label).toList(),
|
||||||
|
onTabChanged: onTabChanged,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTabChange(RecordTab tab, FoodProvider provider) {
|
||||||
|
provider.currentRecordTab = tab;
|
||||||
|
provider.onRecordTabChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleAddRecord(FoodProvider provider) {
|
||||||
|
provider.resetRecordForm();
|
||||||
|
provider.isEditing = false;
|
||||||
|
Navigator.pop(context);
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
isScrollControlled: true,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
),
|
||||||
|
builder: (context) => RecordForm(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleAddRecipe(FoodProvider provider) {
|
||||||
|
provider.resetRecipeForm();
|
||||||
|
provider.isEditing = false;
|
||||||
|
Navigator.pop(context);
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
isScrollControlled: true,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
),
|
||||||
|
builder: (context) => RecipeForm(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleAddMoment(FoodProvider provider) {
|
||||||
|
provider.resetMomentForm();
|
||||||
|
provider.isEditing = false;
|
||||||
|
Navigator.pop(context);
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
isScrollControlled: true,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
),
|
||||||
|
builder: (context) => MomentForm(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPressAdd(FoodProvider provider) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
),
|
||||||
|
builder:
|
||||||
|
(context) => Padding(
|
||||||
|
padding: EdgeInsetsGeometry.symmetric(horizontal: 0, vertical: 8),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text('请选择操作', style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.book,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'新增菜谱',
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
onTap: () => _handleAddRecipe(provider),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.note_add,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'新增记录',
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
onTap: () => _handleAddRecord(provider),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.group,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'发布朋友圈',
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
onTap: () => _handleAddMoment(provider),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
final provider = context.watch<FoodProvider>();
|
||||||
padding: EdgeInsets.all(0),
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
appBar: AppBar(
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
title: Text('记录', style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
centerTitle: true,
|
||||||
|
leading: Builder(
|
||||||
|
builder:
|
||||||
|
(context) => IconButton(
|
||||||
|
icon: Icon(Icons.menu, color: Theme.of(context).primaryColor),
|
||||||
|
onPressed: () => {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.search, color: Theme.of(context).primaryColor),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.add, color: Theme.of(context).primaryColor),
|
||||||
|
onPressed: () => _onPressAdd(provider),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
TDTabBar(
|
_buildTabs(
|
||||||
tabs: tabs,
|
context: context,
|
||||||
controller: _tabController,
|
currentTab: provider.currentRecordTab,
|
||||||
backgroundColor: Colors.white,
|
onTabChanged: (tab) => _onTabChange(tab, provider),
|
||||||
showIndicator: true,
|
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(child: _tabPages[provider.currentRecordTab.index]),
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(5),
|
|
||||||
child: TabBarView(
|
|
||||||
controller: _tabController,
|
|
||||||
children: [
|
|
||||||
RecipeList(),
|
|
||||||
RecipeCalendar(),
|
|
||||||
RecipeTimeline(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
346
lib/views/record_form.dart
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_common/utils/toast_util.dart';
|
||||||
|
import 'package:flutter_common/widget/dialog_widget.dart';
|
||||||
|
import 'package:flutter_common/widget/loading_widget.dart';
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:food_hub_app/apis/recipe.dart';
|
||||||
|
import 'package:food_hub_app/config/app_config.dart';
|
||||||
|
import 'package:food_hub_app/provider/food_provider.dart';
|
||||||
|
import 'package:food_hub_app/utils/minio_utils.dart';
|
||||||
|
import 'package:food_hub_app/widgets/common/form.dart';
|
||||||
|
import 'package:food_hub_app/widgets/common/image.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class RecordForm extends StatefulWidget {
|
||||||
|
const RecordForm({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RecordForm> createState() => _RecordFormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecordFormState extends State<RecordForm> {
|
||||||
|
final _formKey = GlobalKey<FormBuilderState>();
|
||||||
|
static const String _nameField = 'name';
|
||||||
|
static const String _dateField = 'date';
|
||||||
|
List<String> _foodNameList = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
refreshCategoryList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshCategoryList() async {
|
||||||
|
final result = await queryFoodNameListApi();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_foodNameList = result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFormBuilder(FoodProvider provider) {
|
||||||
|
return FormBuilder(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
buildFormLabel(context: context, text: '菜谱名称', isRequired: true),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildRecipeField(provider),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
buildFormLabel(context: context, text: '完成时间', isRequired: true),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildDateField(provider),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
buildFormLabel(context: context, text: '上传图片', isRequired: true),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildImageField(provider),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
buildFormButtonGroup(
|
||||||
|
context: context,
|
||||||
|
isShowDelete: provider.isEditing,
|
||||||
|
onConfirm: () => _submitForm(provider),
|
||||||
|
onDelete: () => _deleteForm(provider),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRecipeField(FoodProvider provider) {
|
||||||
|
return Autocomplete<String>(
|
||||||
|
initialValue: TextEditingValue(text: provider.recordFormItem.name),
|
||||||
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||||
|
if (textEditingValue.text.isEmpty || _foodNameList.isEmpty) {
|
||||||
|
return const Iterable<String>.empty();
|
||||||
|
}
|
||||||
|
return _foodNameList.where(
|
||||||
|
(option) => option.toLowerCase().contains(
|
||||||
|
textEditingValue.text.toLowerCase(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
onSelected: (String value) {
|
||||||
|
provider.updateRecordFormItem(name: value);
|
||||||
|
},
|
||||||
|
|
||||||
|
optionsViewBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
AutocompleteOnSelected<String> onSelected,
|
||||||
|
Iterable<String> options,
|
||||||
|
) {
|
||||||
|
Widget buildOptionItem(String option) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => onSelected(option),
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||||
|
child: Text(
|
||||||
|
option,
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.grey.shade800),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: Material(
|
||||||
|
elevation: 2,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: 200,
|
||||||
|
maxWidth: MediaQuery.of(context).size.width - 58,
|
||||||
|
),
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: options.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
return buildOptionItem(options.elementAt(index));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
fieldViewBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
TextEditingController controller,
|
||||||
|
FocusNode focusNode,
|
||||||
|
VoidCallback onFieldSubmitted,
|
||||||
|
) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (provider.recordFormItem.name.isNotEmpty &&
|
||||||
|
controller.text.isEmpty) {
|
||||||
|
controller.text = provider.recordFormItem.name;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
void onClearRecipeName() {
|
||||||
|
controller.clear();
|
||||||
|
provider.updateRecordFormItem(name: '');
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget? buildSuffixIcon() {
|
||||||
|
final isShow = controller.text.isNotEmpty && !provider.isEditing;
|
||||||
|
|
||||||
|
return isShow
|
||||||
|
? IconButton(
|
||||||
|
icon: Icon(Icons.clear, size: 18),
|
||||||
|
onPressed: onClearRecipeName,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FormBuilderTextField(
|
||||||
|
name: _nameField,
|
||||||
|
controller: controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
enabled: !provider.isEditing,
|
||||||
|
onChanged: (value) {
|
||||||
|
provider.updateRecordFormItem(name: value ?? '');
|
||||||
|
},
|
||||||
|
decoration: buildInputDecoration(
|
||||||
|
context: context,
|
||||||
|
hintText: '请输入菜谱名称',
|
||||||
|
prefixIcon: Icons.restaurant_menu,
|
||||||
|
).copyWith(suffixIcon: buildSuffixIcon()),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '请输入菜谱名称';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDateField(FoodProvider provider) {
|
||||||
|
return FormBuilderTextField(
|
||||||
|
name: _dateField,
|
||||||
|
readOnly: true,
|
||||||
|
initialValue: provider.recordFormItem.date,
|
||||||
|
decoration: buildInputDecoration(
|
||||||
|
context: context,
|
||||||
|
hintText: '请选择完成时间',
|
||||||
|
prefixIcon: Icons.calendar_month,
|
||||||
|
),
|
||||||
|
onTap: () => _onSelectDate(provider),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '请选择完成时间';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 选择日期
|
||||||
|
Future<void> _onSelectDate(FoodProvider provider) async {
|
||||||
|
final DateTime? picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: DateTime.now(),
|
||||||
|
firstDate: DateTime(2000),
|
||||||
|
lastDate: DateTime(2100),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (picked != null) {
|
||||||
|
final String date = DateFormat('yyyy-MM-dd').format(picked);
|
||||||
|
_formKey.currentState?.fields[_dateField]?.didChange(date);
|
||||||
|
provider.updateRecordFormItem(date: date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImageField(FoodProvider provider) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (provider.recordFormItem.imageUrl.isEmpty)
|
||||||
|
buildImageUploadButton(
|
||||||
|
context: context,
|
||||||
|
onTap: () => _pickImage(provider),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
ImagePreview(
|
||||||
|
imageUrls: [provider.recordFormItem.imageUrl],
|
||||||
|
index: 0,
|
||||||
|
onRemoveImage: () => _removeImage(provider),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 选择图片(限制单张)
|
||||||
|
Future<void> _pickImage(FoodProvider provider) async {
|
||||||
|
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||||
|
type: FileType.custom,
|
||||||
|
allowedExtensions: ['jpg', 'jpeg', 'png'],
|
||||||
|
allowMultiple: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
PlatformFile file = result.files.first;
|
||||||
|
LoadingDialog.show(context, message: '上传中');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final fileName = await MinIOHelper().uploadFile(
|
||||||
|
bucketName: AppConfig.bucketName,
|
||||||
|
file: file,
|
||||||
|
);
|
||||||
|
ToastUtil.success('上传成功');
|
||||||
|
provider.updateRecordFormItem(imageUrl: fileName);
|
||||||
|
} catch (e) {
|
||||||
|
ToastUtil.error(e.toString());
|
||||||
|
} finally {
|
||||||
|
LoadingDialog.hide(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 移除图片
|
||||||
|
void _removeImage(FoodProvider provider) {
|
||||||
|
provider.updateRecordFormItem(imageUrl: '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 提交表单
|
||||||
|
void _submitForm(FoodProvider provider) async {
|
||||||
|
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||||
|
if (provider.recordFormItem.imageUrl.isEmpty) {
|
||||||
|
ToastUtil.error('请上传图片');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadingDialog.show(context, message: '提交中');
|
||||||
|
final result = await provider.handleRecord();
|
||||||
|
if (result == true) {
|
||||||
|
await provider.onRecordTabChange();
|
||||||
|
LoadingDialog.hide(context);
|
||||||
|
|
||||||
|
if (provider.isEditing) {
|
||||||
|
showSuccessTip(context, '更新记录成功');
|
||||||
|
} else {
|
||||||
|
showSuccessTip(context, '新增记录成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future.delayed(Duration(milliseconds: 1500), () {
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
LoadingDialog.hide(context);
|
||||||
|
if (provider.isEditing) {
|
||||||
|
showErrorTip(context, '更新记录失败');
|
||||||
|
} else {
|
||||||
|
showErrorTip(context, '新增记录失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _deleteForm(FoodProvider provider) async {
|
||||||
|
final result = await showConfirmDialog(context, '确认删除该记录?');
|
||||||
|
if (result == true) {
|
||||||
|
LoadingDialog.show(context, message: '删除中');
|
||||||
|
final isDelete = await provider.deleteRecord();
|
||||||
|
if (isDelete) {
|
||||||
|
await provider.onRecordTabChange();
|
||||||
|
LoadingDialog.hide(context);
|
||||||
|
showSuccessTip(context, '删除记录成功');
|
||||||
|
|
||||||
|
Future.delayed(Duration(milliseconds: 1500), () {
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final provider = context.watch<FoodProvider>();
|
||||||
|
|
||||||
|
return AnimatedPadding(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
padding: MediaQuery.of(context).viewInsets,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: _buildFormBuilder(provider),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:graphic/graphic.dart';
|
import 'package:flutter_common/widget/chart.dart';
|
||||||
|
import 'package:flutter_common/widget/common_widget.dart';
|
||||||
import 'data.dart';
|
import 'package:food_hub_app/provider/food_provider.dart';
|
||||||
|
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class StatsPage extends StatefulWidget {
|
class StatsPage extends StatefulWidget {
|
||||||
const StatsPage({super.key});
|
const StatsPage({super.key});
|
||||||
@@ -11,87 +13,144 @@ class StatsPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _StatsPage extends State<StatsPage> {
|
class _StatsPage extends State<StatsPage> {
|
||||||
var _smooth = false;
|
final double chartHeight = 400;
|
||||||
var _stepped = false;
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
// 初始化加载数据
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
context.read<FoodProvider>().refreshFoodStats();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSummaryStats(BuildContext context, FoodProvider provider) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return GridView.count(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
// 禁用网格自身滚动
|
||||||
|
crossAxisCount: 2,
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
childAspectRatio: 2,
|
||||||
|
children: [
|
||||||
|
GlassCard(
|
||||||
|
useOwnLayer: true,
|
||||||
|
settings: LiquidGlassSettings(glassColor: colors.surface),
|
||||||
|
padding: EdgeInsetsGeometry.all(10),
|
||||||
|
child: StatsCard(
|
||||||
|
icon: Icons.restaurant_menu,
|
||||||
|
title: '菜谱总数',
|
||||||
|
value: provider.summaryStats.recipeCount.toString(),
|
||||||
|
unit: '个',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GlassCard(
|
||||||
|
useOwnLayer: true,
|
||||||
|
settings: LiquidGlassSettings(glassColor: colors.surface),
|
||||||
|
padding: EdgeInsetsGeometry.all(10),
|
||||||
|
child: StatsCard(
|
||||||
|
icon: Icons.grid_view_rounded,
|
||||||
|
title: '菜谱类别',
|
||||||
|
value: provider.summaryStats.categoryCount.toString(),
|
||||||
|
unit: '种',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GlassCard(
|
||||||
|
useOwnLayer: true,
|
||||||
|
settings: LiquidGlassSettings(glassColor: colors.surface),
|
||||||
|
padding: EdgeInsetsGeometry.all(10),
|
||||||
|
child: StatsCard(
|
||||||
|
icon: Icons.flag,
|
||||||
|
title: '做菜次数',
|
||||||
|
value: provider.summaryStats.workCount.toString(),
|
||||||
|
unit: '个',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GlassCard(
|
||||||
|
useOwnLayer: true,
|
||||||
|
settings: LiquidGlassSettings(glassColor: colors.surface),
|
||||||
|
padding: EdgeInsetsGeometry.all(10),
|
||||||
|
child: StatsCard(
|
||||||
|
icon: Icons.show_chart,
|
||||||
|
title: '平均次数',
|
||||||
|
value: provider.averageRecordCount.toStringAsFixed(2),
|
||||||
|
unit: '次/月',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(BuildContext context, FoodProvider provider) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildSummaryStats(context, provider),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
SizedBox(
|
||||||
|
height: chartHeight,
|
||||||
|
child: GlassCard(
|
||||||
|
useOwnLayer: true,
|
||||||
|
settings: LiquidGlassSettings(glassColor: colors.surface),
|
||||||
|
child: LineChart(
|
||||||
|
title: '记录统计',
|
||||||
|
xAxisName: '日期',
|
||||||
|
yAxisName: '次数',
|
||||||
|
unit: '次',
|
||||||
|
data: provider.recordStats,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
SizedBox(
|
||||||
|
height: chartHeight,
|
||||||
|
child: GlassCard(
|
||||||
|
useOwnLayer: true,
|
||||||
|
settings: LiquidGlassSettings(glassColor: colors.surface),
|
||||||
|
child: PieChart(
|
||||||
|
title: '菜谱统计',
|
||||||
|
unit: '个',
|
||||||
|
data: provider.categoryStats,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
GlassCard(
|
||||||
|
useOwnLayer: true,
|
||||||
|
settings: LiquidGlassSettings(glassColor: colors.surface),
|
||||||
|
child: RankChart(title: '排行榜', data: provider.rankStats, unit: '次'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SingleChildScrollView(
|
final provider = context.watch<FoodProvider>();
|
||||||
child: Center(
|
|
||||||
child: Column(
|
return Scaffold(
|
||||||
children: <Widget>[
|
backgroundColor: Colors.transparent,
|
||||||
Container(
|
appBar: AppBar(
|
||||||
margin: const EdgeInsets.only(top: 10),
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
width: 350,
|
title: Text('统计', style: Theme.of(context).textTheme.titleLarge),
|
||||||
height: 300,
|
centerTitle: true,
|
||||||
child: Chart(
|
automaticallyImplyLeading: false,
|
||||||
data: basicData,
|
|
||||||
variables: {
|
|
||||||
'genre': Variable(
|
|
||||||
accessor: (Map map) => map['genre'] as String,
|
|
||||||
),
|
|
||||||
'sold': Variable(accessor: (Map map) => map['sold'] as num),
|
|
||||||
},
|
|
||||||
marks: [
|
|
||||||
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],
|
|
||||||
selections: {'tap': PointSelection(dim: Dim.x)},
|
|
||||||
tooltip: TooltipGuide(),
|
|
||||||
crosshair: CrosshairGuide(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.fromLTRB(20, 40, 20, 5),
|
|
||||||
child: const Text(
|
|
||||||
'Transposed Bar Chart',
|
|
||||||
style: TextStyle(fontSize: 20),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.only(top: 10),
|
|
||||||
width: 350,
|
|
||||||
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),
|
|
||||||
),
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: EdgeInsetsGeometry.all(10),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
if (provider.isLoading)
|
||||||
|
buildLoadingIndicator()
|
||||||
|
else
|
||||||
|
SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.only(bottom: 90),
|
||||||
|
child: _buildContent(context, provider),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
67
lib/widgets/common/easy_refresh.dart
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import 'package:easy_refresh/easy_refresh.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
Widget buildEasyRefresh({
|
||||||
|
required EasyRefreshController freshController,
|
||||||
|
required Future<void> Function()? onRefresh,
|
||||||
|
required Future<void> Function()? onLoad,
|
||||||
|
required Widget body,
|
||||||
|
}) {
|
||||||
|
return 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: body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildScrollToTop({
|
||||||
|
required BuildContext context,
|
||||||
|
required VoidCallback scrollToTop,
|
||||||
|
}) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return Positioned(
|
||||||
|
right: 0,
|
||||||
|
bottom: 80,
|
||||||
|
child: FloatingActionButton(
|
||||||
|
onPressed: scrollToTop,
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
elevation: 2,
|
||||||
|
mini: true,
|
||||||
|
child: Icon(Icons.arrow_upward, color: colors.surface),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void scrollToTopAnimateTo(ScrollController controller) {
|
||||||
|
controller.animateTo(
|
||||||
|
0,
|
||||||
|
duration: const Duration(milliseconds: 500), // 滚动动画时长
|
||||||
|
curve: Curves.easeInOut, // 滚动动画曲线
|
||||||
|
);
|
||||||
|
}
|
||||||
107
lib/widgets/common/form.dart
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'index.dart';
|
||||||
|
|
||||||
|
/// 通用输入框样式
|
||||||
|
InputDecoration buildInputDecoration({
|
||||||
|
required BuildContext context,
|
||||||
|
required String hintText,
|
||||||
|
IconData? prefixIcon,
|
||||||
|
}) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return InputDecoration(
|
||||||
|
hintText: hintText,
|
||||||
|
hintStyle: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
filled: true,
|
||||||
|
fillColor: colors.surface,
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: colors.primary, width: 2),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderSide: const BorderSide(color: Colors.red, width: 2),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 14),
|
||||||
|
errorStyle: const TextStyle(fontSize: 12, height: 1, color: Colors.red),
|
||||||
|
prefixIcon:
|
||||||
|
prefixIcon != null
|
||||||
|
? Icon(prefixIcon, size: 20, color: colors.primary)
|
||||||
|
: null,
|
||||||
|
prefixIconConstraints: const BoxConstraints(minWidth: 40),
|
||||||
|
isDense: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildFormLabel({
|
||||||
|
required BuildContext context,
|
||||||
|
required String text,
|
||||||
|
required bool isRequired,
|
||||||
|
}) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
text: text,
|
||||||
|
style: TextStyle(
|
||||||
|
color: colors.onSurface,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontFamily: 'CustomFont',
|
||||||
|
height: 1.2,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
if (isRequired)
|
||||||
|
const TextSpan(
|
||||||
|
text: ' *',
|
||||||
|
style: TextStyle(color: Colors.red, fontSize: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 表单底部按钮组组件
|
||||||
|
Widget buildFormButtonGroup({
|
||||||
|
required BuildContext context,
|
||||||
|
required bool isShowDelete,
|
||||||
|
required VoidCallback onConfirm,
|
||||||
|
required VoidCallback onDelete,
|
||||||
|
}) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildCancelButton(context)),
|
||||||
|
if (isShowDelete) ...[
|
||||||
|
SizedBox(width: 16),
|
||||||
|
Expanded(child: _buildDeleteButton(context, onDelete)),
|
||||||
|
],
|
||||||
|
SizedBox(width: 16),
|
||||||
|
Expanded(child: _buildSubmitButton(context, onConfirm)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 取消按钮
|
||||||
|
Widget _buildCancelButton(BuildContext context) {
|
||||||
|
return buildInfoButton(
|
||||||
|
context: context,
|
||||||
|
text: '取消',
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 提交按钮
|
||||||
|
Widget _buildSubmitButton(BuildContext context, VoidCallback onConfirm) {
|
||||||
|
return buildPrimaryButton(context: context, text: '提交', onPressed: onConfirm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除按钮
|
||||||
|
Widget _buildDeleteButton(BuildContext context, VoidCallback onDelete) {
|
||||||
|
return buildErrorButton(text: '删除', onPressed: onDelete);
|
||||||
|
}
|
||||||
220
lib/widgets/common/image.dart
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:food_hub_app/config/app_config.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) {
|
||||||
|
final index = '${_currentIndex + 1}/${widget.images.length}';
|
||||||
|
|
||||||
|
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(index, style: const TextStyle(color: Colors.white)),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: PhotoViewGallery(
|
||||||
|
pageOptions:
|
||||||
|
widget.images.map((url) {
|
||||||
|
return PhotoViewGalleryPageOptions(
|
||||||
|
imageProvider: NetworkImage('${AppConfig.imageBaseUrl}$url'),
|
||||||
|
minScale: PhotoViewComputedScale.contained,
|
||||||
|
maxScale: PhotoViewComputedScale.covered * 2,
|
||||||
|
// 点击空白处关闭预览
|
||||||
|
onTapDown: (context, details, controllerValue) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
pageController: _pageController,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CommonImage extends StatelessWidget {
|
||||||
|
final List<String> imageUrls;
|
||||||
|
final int index;
|
||||||
|
|
||||||
|
const CommonImage({super.key, required this.imageUrls, required this.index});
|
||||||
|
|
||||||
|
Widget _buildErrorImage() {
|
||||||
|
return Container(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
child: const Icon(Icons.image, color: Colors.grey, size: 30),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _navigateToImagePreview(
|
||||||
|
BuildContext context,
|
||||||
|
List<String> imageUrls,
|
||||||
|
int index,
|
||||||
|
) {
|
||||||
|
final route = MaterialPageRoute(
|
||||||
|
builder:
|
||||||
|
(context) => ImagePreviewPage(images: imageUrls, initialIndex: index),
|
||||||
|
);
|
||||||
|
|
||||||
|
Navigator.push(context, route);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AspectRatio(
|
||||||
|
aspectRatio: 4 / 3,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
_navigateToImagePreview(context, imageUrls, index);
|
||||||
|
},
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: '${AppConfig.imageBaseUrl}${imageUrls[index]}',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
progressIndicatorBuilder:
|
||||||
|
(context, url, downloadProgress) => CircularProgressIndicator(
|
||||||
|
strokeWidth: 1.5,
|
||||||
|
padding: EdgeInsetsGeometry.all(20),
|
||||||
|
value: downloadProgress.progress,
|
||||||
|
),
|
||||||
|
errorWidget: (context, error, stackTrace) => _buildErrorImage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImagePreview extends StatelessWidget {
|
||||||
|
final List<String> imageUrls;
|
||||||
|
final int index;
|
||||||
|
final VoidCallback onRemoveImage;
|
||||||
|
|
||||||
|
const ImagePreview({
|
||||||
|
super.key,
|
||||||
|
required this.imageUrls,
|
||||||
|
required this.index,
|
||||||
|
required this.onRemoveImage,
|
||||||
|
});
|
||||||
|
|
||||||
|
Widget _buildDeleteImage() {
|
||||||
|
return Positioned(
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: onRemoveImage,
|
||||||
|
child: Container(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.red,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.close, color: Colors.white, size: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
CommonImage(imageUrls: imageUrls, index: index),
|
||||||
|
_buildDeleteImage(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildImageUploadButton({
|
||||||
|
required BuildContext context,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 180,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surface,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: colors.surface),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.camera_alt, color: colors.onSurface.withAlpha(100)),
|
||||||
|
SizedBox(height: 6),
|
||||||
|
Text('点击上传美食图片', style: Theme.of(context).textTheme.bodyMedium),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:toggle_switch/toggle_switch.dart';
|
||||||
|
|
||||||
Widget formLabelText({required String labelText, bool isRequired = false}) {
|
Widget formLabelText({required String labelText, bool isRequired = false}) {
|
||||||
return Text.rich(
|
return Text.rich(
|
||||||
@@ -14,7 +14,7 @@ Widget formLabelText({required String labelText, bool isRequired = false}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
InputDecoration formInputDecoration({
|
InputDecoration formInputDecoration({
|
||||||
required String hintText, // 使用命名参数并标记为必填
|
required String hintText,
|
||||||
IconData? prefixIcon,
|
IconData? prefixIcon,
|
||||||
}) {
|
}) {
|
||||||
return InputDecoration(
|
return InputDecoration(
|
||||||
@@ -27,67 +27,124 @@ InputDecoration formInputDecoration({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 确认按钮样式
|
Widget buildToggleSwitch<T extends Enum>({
|
||||||
ButtonStyle primaryButtonStyle() {
|
required BuildContext context,
|
||||||
return ButtonStyle(
|
required T currentTab,
|
||||||
backgroundColor: WidgetStateProperty.all(Colors.green),
|
required List<T> tabValues,
|
||||||
shape: WidgetStateProperty.all(
|
required List<String> labels,
|
||||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
required ValueChanged<T> onTabChanged,
|
||||||
),
|
}) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return ToggleSwitch(
|
||||||
|
minWidth: 90.0,
|
||||||
|
minHeight: 40.0,
|
||||||
|
initialLabelIndex: tabValues.indexOf(currentTab),
|
||||||
|
totalSwitches: tabValues.length,
|
||||||
|
labels: labels,
|
||||||
|
activeBgColor: [colors.primary],
|
||||||
|
activeFgColor: colors.surface,
|
||||||
|
inactiveBgColor: colors.surface,
|
||||||
|
inactiveFgColor: colors.onSurface,
|
||||||
|
cornerRadius: 12.0,
|
||||||
|
customTextStyles: [TextStyle(fontSize: 12, fontWeight: FontWeight.w500)],
|
||||||
|
onToggle: (index) {
|
||||||
|
if (index != null && index < tabValues.length) {
|
||||||
|
onTabChanged(tabValues[index]);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 取消按钮样式
|
/// 圆形图标按钮
|
||||||
ButtonStyle cancelButtonStyle() {
|
Widget circleIconButton({
|
||||||
return ButtonStyle(
|
required IconData icon,
|
||||||
backgroundColor: WidgetStateProperty.all(Colors.grey),
|
required VoidCallback onPressed,
|
||||||
shape: WidgetStateProperty.all(
|
required BuildContext context,
|
||||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
bool? isSmall,
|
||||||
|
}) {
|
||||||
|
final double size = isSmall == true ? 24 : 36;
|
||||||
|
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
fixedSize: Size(size, size),
|
||||||
|
shape: CircleBorder(),
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
minimumSize: const Size(0, 0),
|
||||||
),
|
),
|
||||||
|
child: Icon(icon, color: Colors.white),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Text buttonText({required String text}) {
|
Widget buildTag(BuildContext context, String title) {
|
||||||
return Text(text, style: TextStyle(color: Colors.white));
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.primary,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Text(title, style: TextStyle(color: colors.surface)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildPrimaryButton({
|
||||||
|
required BuildContext context,
|
||||||
|
required String text,
|
||||||
|
required VoidCallback onPressed,
|
||||||
|
}) {
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: () => onPressed(),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: Text(text, style: TextStyle(color: Colors.white)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildErrorButton({
|
||||||
|
required String text,
|
||||||
|
required VoidCallback onPressed,
|
||||||
|
}) {
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: () => onPressed(),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: Text(text, style: TextStyle(color: Colors.white)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildInfoButton({
|
||||||
|
required BuildContext context,
|
||||||
|
required String text,
|
||||||
|
required VoidCallback onPressed,
|
||||||
|
}) {
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: () => onPressed(),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: Text(text),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 图片错误显示
|
||||||
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)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 成功消息
|
|
||||||
void showSuccessToast(String message) {
|
|
||||||
Fluttertoast.showToast(
|
|
||||||
msg: message,
|
|
||||||
toastLength: Toast.LENGTH_SHORT,
|
|
||||||
gravity: ToastGravity.CENTER,
|
|
||||||
timeInSecForIosWeb: 1,
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
textColor: Colors.white,
|
|
||||||
fontSize: 16.0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 错误消息
|
|
||||||
void showErrorToast(String message) {
|
|
||||||
Fluttertoast.showToast(
|
|
||||||
msg: message,
|
|
||||||
toastLength: Toast.LENGTH_SHORT,
|
|
||||||
gravity: ToastGravity.CENTER,
|
|
||||||
timeInSecForIosWeb: 1,
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
textColor: Colors.white,
|
|
||||||
fontSize: 16.0,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
263
lib/widgets/moment/card.dart
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_common/flutter_common.dart';
|
||||||
|
import 'package:flutter_common/widget/common_widget.dart';
|
||||||
|
import 'package:food_hub_app/config/app_config.dart';
|
||||||
|
import 'package:food_hub_app/models/moment.dart';
|
||||||
|
import 'package:food_hub_app/provider/food_provider.dart';
|
||||||
|
import 'package:food_hub_app/provider/user_provider.dart';
|
||||||
|
import 'package:food_hub_app/widgets/common/image.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class MomentCard extends StatefulWidget {
|
||||||
|
final Moment moment;
|
||||||
|
final bool isUser;
|
||||||
|
|
||||||
|
const MomentCard({super.key, required this.moment, required this.isUser});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => MomentCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class MomentCardState extends State<MomentCard> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CommonCard(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
onTap: () => _onTapUser(context),
|
||||||
|
child: _buildAvatar(context),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildNickname(context),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
_buildContent(context),
|
||||||
|
if (widget.moment.imageList.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildPostImages(),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_buildTime(context),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
_buildActionButtons(),
|
||||||
|
if (widget.moment.commentList!.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildCommentList(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapUser(BuildContext context) async {
|
||||||
|
final provider = context.read<UserProvider>();
|
||||||
|
|
||||||
|
if (widget.moment.userId != SPUtil.getInt('userId')) {
|
||||||
|
await provider.refreshUser(widget.moment.userId!);
|
||||||
|
await provider.queryMomentByUserId(widget.moment.userId!);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.pushNamed(context, '/profileUser');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建头像组件
|
||||||
|
Widget _buildAvatar(BuildContext context) {
|
||||||
|
if (widget.moment.avatar!.isEmpty) {
|
||||||
|
return CircleAvatar(
|
||||||
|
radius: 18,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
|
foregroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
child: Text(widget.moment.username![0], style: TextStyle(fontSize: 14)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return CircleAvatar(
|
||||||
|
radius: 18,
|
||||||
|
backgroundImage: NetworkImage('${AppConfig.imageBaseUrl}/${widget.moment.avatar}')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建昵称组件
|
||||||
|
Widget _buildNickname(BuildContext context) {
|
||||||
|
return Text(
|
||||||
|
widget.moment.username ?? "",
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建内容组件
|
||||||
|
Widget _buildContent(BuildContext context) {
|
||||||
|
return Text(
|
||||||
|
widget.moment.content,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建朋友圈图片列表(网格布局)
|
||||||
|
Widget _buildPostImages() {
|
||||||
|
final imageCount = widget.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 =
|
||||||
|
widget.moment.imageList.map((path) => path).toList();
|
||||||
|
|
||||||
|
return GridView.count(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
crossAxisCount: crossAxisCount,
|
||||||
|
crossAxisSpacing: 4,
|
||||||
|
mainAxisSpacing: 4,
|
||||||
|
childAspectRatio: itemAspectRatio,
|
||||||
|
children: List.generate(imageCount, (index) {
|
||||||
|
return CommonImage(imageUrls: imageUrls, index: index);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建时间组件
|
||||||
|
Widget _buildTime(BuildContext context) {
|
||||||
|
return Text(
|
||||||
|
widget.moment.date ?? "",
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapEdit() {
|
||||||
|
final provider = Provider.of<FoodProvider>(context, listen: false);
|
||||||
|
provider.isEditing = true;
|
||||||
|
provider.momentFormItem = widget.moment;
|
||||||
|
Navigator.pushNamed(context, '/momentForm');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建点赞和评论按钮
|
||||||
|
Widget _buildActionButtons() {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
if (widget.isUser)
|
||||||
|
SizedBox(
|
||||||
|
width: 42,
|
||||||
|
height: 36,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.bottomLeft,
|
||||||
|
children: [
|
||||||
|
InkWell(onTap: () => _onTapEdit(), child: Icon(Icons.edit)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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: (widget.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: (widget.moment.commentList?.length ?? 0).toString(),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCommentItem(Comment comment) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: comment.username,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'CustomFont',
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const TextSpan(text: ': ', style: TextStyle(color: Colors.black)),
|
||||||
|
TextSpan(
|
||||||
|
text: comment.content,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'CustomFont',
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCommentList() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: ListView.separated(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: widget.moment.commentList!.length,
|
||||||
|
separatorBuilder: (context, index) => SizedBox(height: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return _buildCommentItem(widget.moment.commentList![index]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
lib/widgets/moment/list.dart
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:food_hub_app/models/moment.dart';
|
||||||
|
import 'package:food_hub_app/widgets/moment/card.dart';
|
||||||
|
|
||||||
|
class MomentList extends StatelessWidget {
|
||||||
|
final List<Moment> momentList;
|
||||||
|
final bool isUser;
|
||||||
|
|
||||||
|
const MomentList({super.key, required this.momentList, required this.isUser});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView.separated(
|
||||||
|
itemCount: momentList.length,
|
||||||
|
separatorBuilder: (context, index) => SizedBox(height: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return MomentCard(moment: momentList[index], isUser: isUser);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
117
lib/widgets/profile/basic_info.dart
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_common/utils/date_utils.dart';
|
||||||
|
import 'package:food_hub_app/config/app_config.dart';
|
||||||
|
import 'package:food_hub_app/models/session.dart';
|
||||||
|
import 'package:food_hub_app/widgets/common/index.dart';
|
||||||
|
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
|
||||||
|
import 'package:liquid_glass_widgets/widgets/containers/glass_card.dart';
|
||||||
|
|
||||||
|
class ProfileInfo extends StatelessWidget {
|
||||||
|
final User user;
|
||||||
|
|
||||||
|
const ProfileInfo({super.key, required this.user});
|
||||||
|
|
||||||
|
Widget _buildAvatar(BuildContext context, User user) {
|
||||||
|
if (user.avatar == null) {
|
||||||
|
return CircleAvatar(
|
||||||
|
radius: 24,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
|
foregroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
child: Text(
|
||||||
|
user.username.isNotEmpty ? user.username[0] : '?',
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return CircleAvatar(
|
||||||
|
radius: 24,
|
||||||
|
backgroundImage: NetworkImage(
|
||||||
|
'${AppConfig.imageBaseUrl}/${user.avatar}',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoItem({
|
||||||
|
required BuildContext context,
|
||||||
|
required IconData icon,
|
||||||
|
required String text,
|
||||||
|
}) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 16, color: colors.primary),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(text, style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return GlassCard(
|
||||||
|
padding: EdgeInsetsGeometry.all(10),
|
||||||
|
useOwnLayer: true,
|
||||||
|
settings: LiquidGlassSettings(glassColor: colors.surface),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildAvatar(context, user),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(user.username, style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (user.tags != null && user.tags!.isNotEmpty)
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children:
|
||||||
|
user.tags!.map((tag) => buildTag(context, tag)).toList(),
|
||||||
|
),
|
||||||
|
if (user.tags != null && user.tags!.isNotEmpty)
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
if (user.area != null && user.area!.length >= 2)
|
||||||
|
_buildInfoItem(
|
||||||
|
context: context,
|
||||||
|
icon: Icons.location_on,
|
||||||
|
text: '${user.area![0]}-${user.area![1]}',
|
||||||
|
),
|
||||||
|
if (user.birthDate != null)
|
||||||
|
_buildInfoItem(
|
||||||
|
context: context,
|
||||||
|
icon: Icons.cake,
|
||||||
|
text: formatDate(user.birthDate!),
|
||||||
|
),
|
||||||
|
if (user.job != null && user.job!.isNotEmpty)
|
||||||
|
_buildInfoItem(
|
||||||
|
context: context,
|
||||||
|
icon: Icons.work,
|
||||||
|
text: user.job!,
|
||||||
|
),
|
||||||
|
if (user.phoneNumber != null && user.phoneNumber!.isNotEmpty)
|
||||||
|
_buildInfoItem(
|
||||||
|
context: context,
|
||||||
|
icon: Icons.phone_android,
|
||||||
|
text: user.phoneNumber!,
|
||||||
|
),
|
||||||
|
if (user.email != null && user.email!.isNotEmpty)
|
||||||
|
_buildInfoItem(
|
||||||
|
context: context,
|
||||||
|
icon: Icons.email,
|
||||||
|
text: user.email!,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (user.description != null && user.description!.isNotEmpty)
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (user.description != null && user.description!.isNotEmpty)
|
||||||
|
Text(user.description!),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
226
lib/widgets/recipe/calendar.dart
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_common/widget/common_widget.dart';
|
||||||
|
import 'package:food_hub_app/models/recipe.dart';
|
||||||
|
import 'package:food_hub_app/provider/food_provider.dart';
|
||||||
|
import 'package:food_hub_app/utils/index.dart';
|
||||||
|
import 'package:food_hub_app/views/record_form.dart';
|
||||||
|
import 'package:food_hub_app/widgets/common/index.dart';
|
||||||
|
import 'package:table_calendar/table_calendar.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class RecipeCalendar extends StatefulWidget {
|
||||||
|
const RecipeCalendar({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RecipeCalendar> createState() => _RecipeCalendarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecipeCalendarState extends State<RecipeCalendar> {
|
||||||
|
TableCalendar _recipeCalendar(FoodProvider provider) {
|
||||||
|
return TableCalendar(
|
||||||
|
locale: 'zh_CN',
|
||||||
|
headerStyle: const HeaderStyle(
|
||||||
|
headerPadding: EdgeInsets.symmetric(vertical: 0),
|
||||||
|
formatButtonVisible: false,
|
||||||
|
titleCentered: true,
|
||||||
|
),
|
||||||
|
firstDay: DateTime.utc(2010, 1, 1),
|
||||||
|
lastDay: DateTime.utc(2100, 12, 31),
|
||||||
|
focusedDay: provider.focusedDay,
|
||||||
|
selectedDayPredicate: (day) => isSameDay(provider.selectedDay, day),
|
||||||
|
onDaySelected: (selectedDay, focusedDay) {
|
||||||
|
if (!isSameDay(provider.selectedDay, selectedDay)) {
|
||||||
|
setState(() {
|
||||||
|
provider.selectedDay = selectedDay;
|
||||||
|
provider.focusedDay = focusedDay;
|
||||||
|
provider.refreshSelectRecord();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPageChanged: (focusedDay) {
|
||||||
|
provider.focusedDay = focusedDay;
|
||||||
|
provider.refreshRecordList();
|
||||||
|
},
|
||||||
|
// 自定义日期单元格构建器
|
||||||
|
calendarBuilders: CalendarBuilders(
|
||||||
|
defaultBuilder:
|
||||||
|
(context, day, focusedDay) =>
|
||||||
|
_buildDateItem(day, false, false, provider),
|
||||||
|
selectedBuilder:
|
||||||
|
(context, day, focusedDay) =>
|
||||||
|
_buildDateItem(day, true, false, provider),
|
||||||
|
todayBuilder:
|
||||||
|
(context, day, focusedDay) =>
|
||||||
|
_buildDateItem(day, false, true, provider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _dailyItem(BuildContext context, FoodProvider provider) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(formatDateTime(provider.selectedDay, 'MM月dd日 EEEE')),
|
||||||
|
SizedBox(height: 4),
|
||||||
|
if (provider.selectRecordList.isEmpty)
|
||||||
|
SizedBox(height: 100, child: buildEmptyData())
|
||||||
|
else
|
||||||
|
Column(
|
||||||
|
children:
|
||||||
|
provider.selectRecordList.map((record) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: _buildRecordItem(context, record),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRecordItemAvatar(String name, Color color) {
|
||||||
|
return CircleAvatar(
|
||||||
|
radius: 20,
|
||||||
|
backgroundColor: color,
|
||||||
|
child: Text(
|
||||||
|
name,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRecordItemBody(String name, String category) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(name, style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
SizedBox(height: 5),
|
||||||
|
Text(category, style: TextStyle(color: Colors.grey)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleEditRecord(BuildContext context, FoodRecord record) {
|
||||||
|
final provider = context.read<FoodProvider>();
|
||||||
|
|
||||||
|
provider.initRecordForm(record);
|
||||||
|
provider.isEditing = true;
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
),
|
||||||
|
builder: (context) => RecordForm(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRecordItem(BuildContext context, FoodRecord record) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
elevation: 0,
|
||||||
|
color: colors.primary.withAlpha(50),
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(10),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_buildRecordItemAvatar(record.category[0], colors.primary),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
Expanded(child: _buildRecordItemBody(record.name, record.category)),
|
||||||
|
circleIconButton(
|
||||||
|
context: context,
|
||||||
|
icon: Icons.chevron_right,
|
||||||
|
onPressed: () => _handleEditRecord(context, record),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDateItem(
|
||||||
|
DateTime day,
|
||||||
|
bool isSelected,
|
||||||
|
bool isToday,
|
||||||
|
FoodProvider provider,
|
||||||
|
) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
// 检查当前日期是否在需要显示红点的列表中
|
||||||
|
bool shouldShowRedDot = provider.recordList.any(
|
||||||
|
(item) => item.date == formatDateTime(day),
|
||||||
|
);
|
||||||
|
|
||||||
|
late Color boxColor;
|
||||||
|
if (isSelected) {
|
||||||
|
boxColor = colors.secondary;
|
||||||
|
} else {
|
||||||
|
boxColor = isToday ? colors.primary : Colors.transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
late Color textColor;
|
||||||
|
if (isSelected) {
|
||||||
|
textColor = colors.surface;
|
||||||
|
} else {
|
||||||
|
textColor = isToday ? colors.surface : colors.onSurface;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(color: boxColor, shape: BoxShape.circle),
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
// 日期数字始终居中
|
||||||
|
Text(day.day.toString(), style: TextStyle(color: textColor)),
|
||||||
|
|
||||||
|
// 红点
|
||||||
|
if (shouldShowRedDot)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment(0, 0.8),
|
||||||
|
child: Container(
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(FoodProvider provider) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.only(bottom: 80),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
CommonCard(child: _recipeCalendar(provider)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
CommonCard(child: _dailyItem(context, provider)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final provider = context.watch<FoodProvider>();
|
||||||
|
|
||||||
|
if (provider.isLoading) {
|
||||||
|
return buildLoadingIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _buildContent(provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
103
lib/widgets/recipe/card.dart
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_common/widget/common_widget.dart';
|
||||||
|
import 'package:food_hub_app/models/recipe.dart';
|
||||||
|
import 'package:food_hub_app/widgets/common/image.dart';
|
||||||
|
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
|
||||||
|
|
||||||
|
class RecipeCard extends StatelessWidget {
|
||||||
|
final RecipeSummary recipe;
|
||||||
|
|
||||||
|
const RecipeCard({super.key, required this.recipe});
|
||||||
|
|
||||||
|
void navigatorToRecipeDetail(BuildContext context) {
|
||||||
|
Navigator.pushNamed(context, '/recipeDetail', arguments: {'id': recipe.id});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final List<String> imageUrls =
|
||||||
|
recipe.recordList.reversed.map((record) => record.imageUrl).toList();
|
||||||
|
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return GlassCard(
|
||||||
|
padding: EdgeInsetsGeometry.all(10),
|
||||||
|
useOwnLayer: true,
|
||||||
|
settings: LiquidGlassSettings(glassColor: colors.surface),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CommonImage(imageUrls: imageUrls, index: 0),
|
||||||
|
InkWell(
|
||||||
|
onTap: () => navigatorToRecipeDetail(context),
|
||||||
|
child: _buildRecipeContent(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRecipeContent(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
recipe.name,
|
||||||
|
style: textTheme.titleMedium,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
_buildIconText(
|
||||||
|
icon: Icons.star,
|
||||||
|
text: recipe.recommendRate.toString(),
|
||||||
|
color: colors.primary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 5),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
_buildIconText(
|
||||||
|
icon: Icons.food_bank,
|
||||||
|
text: recipe.category,
|
||||||
|
color: colors.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_buildIconText(
|
||||||
|
icon: Icons.note_add,
|
||||||
|
text: '${recipe.recordList.length.toString()}次',
|
||||||
|
color: colors.primary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用图标文本组件
|
||||||
|
Widget _buildIconText({
|
||||||
|
required IconData icon,
|
||||||
|
required String text,
|
||||||
|
Color color = Colors.grey,
|
||||||
|
double iconSize = 16,
|
||||||
|
double textSize = 16,
|
||||||
|
double spacing = 2,
|
||||||
|
}) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: iconSize, color: color),
|
||||||
|
SizedBox(width: spacing),
|
||||||
|
Text(text, style: TextStyle(fontSize: textSize, color: color)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
lib/widgets/recipe/list.dart
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_common/widget/common_widget.dart';
|
||||||
|
import 'package:food_hub_app/provider/food_provider.dart';
|
||||||
|
import 'package:food_hub_app/widgets/recipe/card.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class RecipeList extends StatefulWidget {
|
||||||
|
final int userId;
|
||||||
|
|
||||||
|
const RecipeList({super.key, required this.userId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RecipeList> createState() => _RecipeListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecipeListState extends State<RecipeList> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
// 初始化加载数据
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (widget.userId == 0) {
|
||||||
|
context.read<FoodProvider>().refreshRecipeList();
|
||||||
|
} else {
|
||||||
|
context.read<FoodProvider>().queryRecipeByUserId(widget.userId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRecipeList(FoodProvider provider) {
|
||||||
|
return GridView.builder(
|
||||||
|
padding: const EdgeInsets.only(bottom: 80),
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
childAspectRatio: 0.85,
|
||||||
|
),
|
||||||
|
itemCount: provider.recipeSummaryList.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return RecipeCard(recipe: provider.recipeSummaryList[index]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(FoodProvider provider) {
|
||||||
|
if (provider.recipeSummaryList.isEmpty) {
|
||||||
|
return buildEmptyData();
|
||||||
|
} else {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildRecipeList(provider)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final provider = context.watch<FoodProvider>();
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
if (provider.isLoading)
|
||||||
|
buildLoadingIndicator()
|
||||||
|
else
|
||||||
|
_buildContent(provider),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,215 +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/utils/date_util.dart';
|
|
||||||
import 'package:food_hub_app/utils/index.dart';
|
|
||||||
import 'package:table_calendar/table_calendar.dart';
|
|
||||||
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
|
||||||
|
|
||||||
class RecipeCalendar extends StatefulWidget {
|
|
||||||
const RecipeCalendar({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<RecipeCalendar> createState() => _RecipeCalendarState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RecipeCalendarState extends State<RecipeCalendar> {
|
|
||||||
DateTime _selectedDay = DateTime.now();
|
|
||||||
DateTime _focusedDay = DateTime.now();
|
|
||||||
|
|
||||||
List<Record> recordList = [];
|
|
||||||
List<Record> selectRecordList = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
refreshRecord();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> refreshRecord() async {
|
|
||||||
final result = await queryRecordApi(
|
|
||||||
getFirstDayOfMonth(_focusedDay),
|
|
||||||
getLastDayOfMonth(_focusedDay),
|
|
||||||
);
|
|
||||||
setState(() {
|
|
||||||
recordList = result;
|
|
||||||
|
|
||||||
if (recordList.isNotEmpty) {
|
|
||||||
_selectedDay = DateTime.parse(recordList.last.date);
|
|
||||||
} else {
|
|
||||||
_selectedDay = _focusedDay;
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshSelectRecord();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void refreshSelectRecord() {
|
|
||||||
setState(() {
|
|
||||||
selectRecordList =
|
|
||||||
recordList
|
|
||||||
.where((record) => record.date == formatDateTime(_selectedDay))
|
|
||||||
.toList();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
TableCalendar recipeCalendar() {
|
|
||||||
return TableCalendar(
|
|
||||||
locale: 'zh_CN',
|
|
||||||
headerStyle: const HeaderStyle(
|
|
||||||
formatButtonVisible: false,
|
|
||||||
titleCentered: true,
|
|
||||||
),
|
|
||||||
firstDay: DateTime.utc(2010, 1, 1),
|
|
||||||
lastDay: DateTime.utc(2100, 12, 31),
|
|
||||||
focusedDay: _focusedDay,
|
|
||||||
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
|
|
||||||
onDaySelected: (selectedDay, focusedDay) {
|
|
||||||
if (!isSameDay(_selectedDay, selectedDay)) {
|
|
||||||
setState(() {
|
|
||||||
_selectedDay = selectedDay;
|
|
||||||
_focusedDay = focusedDay;
|
|
||||||
refreshSelectRecord();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onPageChanged: (focusedDay) {
|
|
||||||
_focusedDay = focusedDay;
|
|
||||||
refreshRecord();
|
|
||||||
},
|
|
||||||
// 自定义日期单元格构建器
|
|
||||||
calendarBuilders: CalendarBuilders(
|
|
||||||
defaultBuilder:
|
|
||||||
(context, day, focusedDay) => _buildDateWidget(day, false, false),
|
|
||||||
selectedBuilder:
|
|
||||||
(context, day, focusedDay) => _buildDateWidget(day, true, false),
|
|
||||||
todayBuilder:
|
|
||||||
(context, day, focusedDay) => _buildDateWidget(day, false, true),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget dailyItem() {
|
|
||||||
return Card(
|
|
||||||
elevation: 0,
|
|
||||||
color: Colors.white,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(5),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(formatDateTime(_selectedDay, 'MM月dd日 EEEE')),
|
|
||||||
if (selectRecordList.isEmpty)
|
|
||||||
TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据')
|
|
||||||
else
|
|
||||||
ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: NeverScrollableScrollPhysics(),
|
|
||||||
itemCount: selectRecordList.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return recipeRecordItem(selectRecordList[index]);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget recipeRecordItem(Record record) {
|
|
||||||
return Card(
|
|
||||||
elevation: 0,
|
|
||||||
color: Color(0xFFF5F5DC),
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(10),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
TDAvatar(
|
|
||||||
size: TDAvatarSize.medium,
|
|
||||||
type: TDAvatarType.customText,
|
|
||||||
text: record.category[0],
|
|
||||||
),
|
|
||||||
SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
// 使用Expanded让文本区域占据剩余空间
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
record.name,
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
SizedBox(height: 5),
|
|
||||||
Text(record.category, style: TextStyle(color: Colors.grey)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 10),
|
|
||||||
TDButton(
|
|
||||||
icon: TDIcons.arrow_right,
|
|
||||||
type: TDButtonType.fill,
|
|
||||||
shape: TDButtonShape.circle,
|
|
||||||
theme: TDButtonTheme.primary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDateWidget(DateTime day, bool isSelected, bool isToday) {
|
|
||||||
// 检查当前日期是否在需要显示红点的列表中
|
|
||||||
bool shouldShowRedDot = recordList.any(
|
|
||||||
(item) => item.date == formatDateTime(day),
|
|
||||||
);
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
margin: EdgeInsets.all(2),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color:
|
|
||||||
isSelected
|
|
||||||
? Colors.blue
|
|
||||||
: isToday
|
|
||||||
? Colors.grey[200]
|
|
||||||
: Colors.transparent,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
// 日期数字
|
|
||||||
Text(
|
|
||||||
day.day.toString(),
|
|
||||||
style: TextStyle(color: isSelected ? Colors.white : Colors.black87),
|
|
||||||
),
|
|
||||||
// 底部红点 - 只在指定日期显示
|
|
||||||
if (shouldShowRedDot)
|
|
||||||
Container(
|
|
||||||
margin: EdgeInsets.only(top: 2),
|
|
||||||
width: 6,
|
|
||||||
height: 6,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.red,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Card(elevation: 0, color: Colors.white, child: recipeCalendar()),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Expanded(child: dailyItem()),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
import 'package:flutter/material.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';
|
|
||||||
|
|
||||||
class RecipeCard extends StatelessWidget {
|
|
||||||
final Recipe recipe;
|
|
||||||
|
|
||||||
const RecipeCard({super.key, required this.recipe});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Card(
|
|
||||||
elevation: 0,
|
|
||||||
color: Colors.white,
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Image.network(
|
|
||||||
'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(
|
|
||||||
height: 1,
|
|
||||||
thickness: 1,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
indent: 0,
|
|
||||||
endIndent: 0,
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
child: _buildRecipeContent(context),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildRecipeContent(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
recipe.name,
|
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
||||||
maxLines: 1, // 限制单行,避免挤压按钮
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
TDButton(
|
|
||||||
icon: Icons.edit,
|
|
||||||
size: TDButtonSize.small,
|
|
||||||
type: TDButtonType.fill,
|
|
||||||
shape: TDButtonShape.circle,
|
|
||||||
theme: TDButtonTheme.primary,
|
|
||||||
onTap: () => {},
|
|
||||||
),
|
|
||||||
SizedBox(width: 10),
|
|
||||||
TDButton(
|
|
||||||
icon: Icons.book,
|
|
||||||
size: TDButtonSize.small,
|
|
||||||
type: TDButtonType.fill,
|
|
||||||
shape: TDButtonShape.circle,
|
|
||||||
theme: TDButtonTheme.primary,
|
|
||||||
onTap: () => {},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 5),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
_buildIconText(
|
|
||||||
icon: Icons.food_bank,
|
|
||||||
text: recipe.category,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
_buildIconText(
|
|
||||||
icon: Icons.thumb_up_alt_outlined,
|
|
||||||
text: recipe.likeCount.toString(),
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
_buildIconText(
|
|
||||||
icon: Icons.star_outline,
|
|
||||||
text: recipe.favouriteCount.toString(),
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
_buildIconText(
|
|
||||||
icon: Icons.comment_outlined,
|
|
||||||
text: recipe.commentCount.toString(),
|
|
||||||
color: Colors.deepOrange,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
_buildIconText(
|
|
||||||
icon: Icons.date_range,
|
|
||||||
text: recipe.recordList[0].date,
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通用图标文本组件
|
|
||||||
Widget _buildIconText({
|
|
||||||
required IconData icon,
|
|
||||||
required String text,
|
|
||||||
Color color = Colors.grey, // 默认灰色
|
|
||||||
double iconSize = 16,
|
|
||||||
double textSize = 16,
|
|
||||||
double spacing = 2, // 图标与文本间距
|
|
||||||
}) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon, size: iconSize, color: color),
|
|
||||||
SizedBox(width: spacing),
|
|
||||||
Text(text, style: TextStyle(fontSize: textSize, color: color)),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +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/recipe/recipe_card.dart';
|
|
||||||
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
|
||||||
|
|
||||||
class RecipeList extends StatefulWidget {
|
|
||||||
const RecipeList({super.key});
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<RecipeList> createState() => _RecipeListState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RecipeListState extends State<RecipeList> {
|
|
||||||
List<Recipe> recipeList = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
refreshRecipeList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> refreshRecipeList() async {
|
|
||||||
final result = await queryRecipeApi(RecipeQuery(category: ""));
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
recipeList = result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (recipeList.isEmpty) {
|
|
||||||
return const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据');
|
|
||||||
} else {
|
|
||||||
return ListView.separated(
|
|
||||||
itemCount: recipeList.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return RecipeCard(recipe: recipeList[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(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
115
lib/widgets/recipe/timeline.dart
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_common/widget/common_widget.dart';
|
||||||
|
import 'package:flutter_common/widget/year_selector.dart';
|
||||||
|
import 'package:food_hub_app/models/recipe.dart';
|
||||||
|
import 'package:food_hub_app/provider/food_provider.dart';
|
||||||
|
import 'package:food_hub_app/widgets/common/image.dart';
|
||||||
|
import 'package:timelines_plus/timelines_plus.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class RecipeTimeline extends StatefulWidget {
|
||||||
|
const RecipeTimeline({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _RecipeTimeline();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecipeTimeline extends State<RecipeTimeline> {
|
||||||
|
Widget buildTimelineCard(BuildContext context, FoodRecord record) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.date_range,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
SizedBox(width: 5),
|
||||||
|
Text(
|
||||||
|
record.date,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
CommonCard(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
record.name,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
CommonImage(imageUrls: [record.imageUrl], index: 0),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildTimeline(BuildContext context, List<FoodRecord> recordList) {
|
||||||
|
if (recordList.isEmpty) {
|
||||||
|
return buildEmptyData();
|
||||||
|
} else {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return Timeline.tileBuilder(
|
||||||
|
padding: const EdgeInsets.only(bottom: 80),
|
||||||
|
theme: TimelineThemeData(nodePosition: 0, indicatorPosition: 0),
|
||||||
|
builder: TimelineTileBuilder.connected(
|
||||||
|
itemCount: recordList.length,
|
||||||
|
connectorBuilder:
|
||||||
|
(context, index, type) =>
|
||||||
|
Connector.solidLine(thickness: 2, color: colors.primary),
|
||||||
|
indicatorBuilder: (context, index) {
|
||||||
|
return Indicator.dot(size: 12.0, color: colors.primary);
|
||||||
|
},
|
||||||
|
contentsBuilder: (context, index) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 8),
|
||||||
|
child: buildTimelineCard(context, recordList[index]),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(FoodProvider provider) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
YearSelector(
|
||||||
|
currentYear: provider.selectYear,
|
||||||
|
onYearChanged: (year) {
|
||||||
|
provider.selectYear = year;
|
||||||
|
provider.refreshRecordList();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 0, horizontal: 10),
|
||||||
|
child: buildTimeline(context, provider.recordList),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final provider = context.watch<FoodProvider>();
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
if (provider.isLoading)
|
||||||
|
buildLoadingIndicator()
|
||||||
|
else
|
||||||
|
_buildContent(provider),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,10 +6,14 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <file_selector_linux/file_selector_plugin.h>
|
#include <rive_native/rive_native_plugin.h>
|
||||||
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
g_autoptr(FlPluginRegistrar) rive_native_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "RiveNativePlugin");
|
||||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
rive_native_plugin_register_with_registrar(rive_native_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
file_selector_linux
|
rive_native
|
||||||
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
@@ -5,10 +5,20 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import file_selector_macos
|
import file_picker
|
||||||
|
import flutter_image_compress_macos
|
||||||
|
import path_provider_foundation
|
||||||
|
import rive_native
|
||||||
|
import share_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
import sqflite_darwin
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
|
FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
|
||||||
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
|
RiveNativePlugin.register(with: registry.registrar(forPlugin: "RiveNativePlugin"))
|
||||||
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
511
pubspec.lock
@@ -33,6 +33,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.12.0"
|
version: "2.12.0"
|
||||||
|
awesome_dialog:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: awesome_dialog
|
||||||
|
sha256: "4c5821a0a637ceee022084e78c1b8237dd4b8bfca4dd24ac2484662a56707338"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.3.0"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -41,6 +49,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
|
buffer:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: buffer
|
||||||
|
sha256: "389da2ec2c16283c8787e0adaede82b1842102f8c8aae2f49003a766c5c6b3d1"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.3"
|
||||||
build:
|
build:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -105,6 +121,30 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.10.1"
|
version: "8.10.1"
|
||||||
|
cached_network_image:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cached_network_image
|
||||||
|
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.1"
|
||||||
|
cached_network_image_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_platform_interface
|
||||||
|
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.1"
|
||||||
|
cached_network_image_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_web
|
||||||
|
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.1"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -165,10 +205,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.6"
|
version: "3.0.7"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -185,38 +225,54 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.0"
|
||||||
dio:
|
dbus:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: dio
|
name: dbus
|
||||||
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
|
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.8.0+1"
|
version: "0.7.11"
|
||||||
|
dio:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dio
|
||||||
|
sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "5.9.2"
|
||||||
dio_web_adapter:
|
dio_web_adapter:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: dio_web_adapter
|
name: dio_web_adapter
|
||||||
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
|
sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.2"
|
||||||
easy_refresh:
|
easy_refresh:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: easy_refresh
|
name: easy_refresh
|
||||||
sha256: "486e30abfcaae66c0f2c2798a10de2298eb9dc5e0bb7e1dba9328308968cae0c"
|
sha256: "486e30abfcaae66c0f2c2798a10de2298eb9dc5e0bb7e1dba9328308968cae0c"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.4.0"
|
version: "3.4.0"
|
||||||
|
equatable:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: equatable
|
||||||
|
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.8"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: fake_async
|
name: fake_async
|
||||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.2"
|
version: "1.3.3"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -233,6 +289,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.0.1"
|
||||||
|
file_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: file_picker
|
||||||
|
sha256: f8f4ea435f791ab1f817b4e338ed958cb3d04ba43d6736ffc39958d950754967
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "10.3.6"
|
||||||
file_selector_linux:
|
file_selector_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -278,6 +342,29 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_cache_manager:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_cache_manager
|
||||||
|
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.1"
|
||||||
|
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_common:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "../flutter_common"
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "1.0.0+1"
|
||||||
flutter_form_builder:
|
flutter_form_builder:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -286,6 +373,62 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.1"
|
version: "10.0.1"
|
||||||
|
flutter_image_compress:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_image_compress
|
||||||
|
sha256: "51d23be39efc2185e72e290042a0da41aed70b14ef97db362a6b5368d0523b27"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
|
flutter_image_compress_common:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_image_compress_common
|
||||||
|
sha256: c5c5d50c15e97dd7dc72ff96bd7077b9f791932f2076c5c5b6c43f2c88607bfb
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.6"
|
||||||
|
flutter_image_compress_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_image_compress_macos
|
||||||
|
sha256: "20019719b71b743aba0ef874ed29c50747461e5e8438980dfa5c2031898f7337"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.3"
|
||||||
|
flutter_image_compress_ohos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_image_compress_ohos
|
||||||
|
sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.3"
|
||||||
|
flutter_image_compress_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_image_compress_platform_interface
|
||||||
|
sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.5"
|
||||||
|
flutter_image_compress_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_image_compress_web
|
||||||
|
sha256: b9b141ac7c686a2ce7bb9a98176321e1182c9074650e47bb140741a44b6f5a96
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.5"
|
||||||
|
flutter_input_chips:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_input_chips
|
||||||
|
sha256: "4b45df0c8b80a23db86850d8f892144dc796f3f6eb13ebd8bcb2d242ec921e22"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -307,22 +450,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.28"
|
version: "2.0.28"
|
||||||
flutter_slidable:
|
flutter_shaders:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_slidable
|
name: flutter_shaders
|
||||||
sha256: a857de7ea701f276fd6a6c4c67ae885b60729a3449e42766bb0e655171042801
|
sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "0.1.3"
|
||||||
flutter_swiper_null_safety:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_swiper_null_safety
|
|
||||||
sha256: "5a855e0080d035c08e82f8b7fd2f106344943a30c9ab483b2584860a2f22eaaf"
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.2"
|
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -334,13 +469,13 @@ packages:
|
|||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
fluttertoast:
|
fluttertoast:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: fluttertoast
|
name: fluttertoast
|
||||||
sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1"
|
sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.2.12"
|
version: "8.2.14"
|
||||||
form_builder_validators:
|
form_builder_validators:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -365,14 +500,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:
|
||||||
@@ -473,10 +600,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.19.0"
|
version: "0.20.2"
|
||||||
io:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -513,26 +640,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.8"
|
version: "11.0.2"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.9"
|
version: "3.0.10"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_testing
|
name: leak_tracker_testing
|
||||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.2"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -541,6 +668,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.1"
|
version: "5.1.1"
|
||||||
|
liquid_glass_widgets:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: liquid_glass_widgets
|
||||||
|
sha256: "776adcdb7d48af0b935642425936813074355830eb0a47ecca8b227549d5b057"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.16.3"
|
||||||
logger:
|
logger:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -577,10 +712,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.17.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -589,6 +724,30 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
minio:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: minio
|
||||||
|
sha256: ee2ce47766e46c7d164f960f2f5ed6a9a82844d877f6b82574f6876ec50c56d1
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.5.8"
|
||||||
|
nested:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: nested
|
||||||
|
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
octo_image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: octo_image
|
||||||
|
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -621,6 +780,30 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
path_provider:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: path_provider
|
||||||
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.5"
|
||||||
|
path_provider_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_android
|
||||||
|
sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.19"
|
||||||
|
path_provider_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_foundation
|
||||||
|
sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -645,6 +828,22 @@ 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"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.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:
|
||||||
@@ -669,6 +868,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.1"
|
version: "1.5.1"
|
||||||
|
provider:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: provider
|
||||||
|
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.5+1"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -685,30 +892,70 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
version: "1.5.0"
|
||||||
shared_preferences:
|
rive:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: rive
|
||||||
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
|
sha256: "84a640f48122679e48397935ed1b971db19ef2e44bfcbae60e42fb5ce29a0a4e"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.3"
|
version: "0.14.9"
|
||||||
|
rive_native:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rive_native
|
||||||
|
sha256: "85dc8413398f7540eab9b73c6d704ecd5051ab9c57e539eed85ccd2a69f119aa"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.9"
|
||||||
|
rxdart:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rxdart
|
||||||
|
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.28.0"
|
||||||
|
share_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: share_plus
|
||||||
|
sha256: d7dc0630a923883c6328ca31b89aa682bacbf2f8304162d29f7c6aaff03a27a1
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "11.1.0"
|
||||||
|
share_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: share_plus_platform_interface
|
||||||
|
sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.0"
|
||||||
|
shared_preferences:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences
|
||||||
|
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.5"
|
||||||
shared_preferences_android:
|
shared_preferences_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_android
|
name: shared_preferences_android
|
||||||
sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac"
|
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.10"
|
version: "2.4.23"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_foundation
|
name: shared_preferences_foundation
|
||||||
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
|
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.4"
|
version: "2.5.6"
|
||||||
shared_preferences_linux:
|
shared_preferences_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -721,10 +968,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_platform_interface
|
name: shared_preferences_platform_interface
|
||||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.1"
|
version: "2.4.2"
|
||||||
shared_preferences_web:
|
shared_preferences_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -794,6 +1041,46 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.1"
|
version: "1.10.1"
|
||||||
|
sqflite:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite
|
||||||
|
sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2+1"
|
||||||
|
sqflite_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_android
|
||||||
|
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2+3"
|
||||||
|
sqflite_common:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_common
|
||||||
|
sha256: "1581ffbf7a0e333b380d6a30737d78516b826cb35beb7fb0bf8a3ea0c678b465"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.8"
|
||||||
|
sqflite_darwin:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_darwin
|
||||||
|
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
|
sqflite_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_platform_interface
|
||||||
|
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -826,22 +1113,38 @@ 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: transitive
|
||||||
|
description:
|
||||||
|
name: syncfusion_flutter_charts
|
||||||
|
sha256: "68fdb029dad34a46e4c9cfad8ad66fe29db7b303bd96849261ab2b23a168d0e8"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "30.2.7"
|
||||||
|
syncfusion_flutter_core:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: syncfusion_flutter_core
|
||||||
|
sha256: bfd026c0f9822b49ff26fed11cd3334519acb6a6ad4b0c81d9cd18df6af1c4c0
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "30.2.7"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.0"
|
||||||
table_calendar:
|
table_calendar:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: table_calendar
|
name: table_calendar
|
||||||
sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63
|
sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.2.0"
|
||||||
tdesign_flutter:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: tdesign_flutter
|
|
||||||
sha256: b36b6f939f7a585184665202b6b3acf1922728962312b65182dbbff64c3b62f2
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.3"
|
|
||||||
tdesign_flutter_adaptation:
|
tdesign_flutter_adaptation:
|
||||||
dependency: "direct overridden"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
@@ -862,10 +1165,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.4"
|
version: "0.7.7"
|
||||||
timelines_plus:
|
timelines_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -882,6 +1185,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
version: "1.0.2"
|
||||||
|
toggle_switch:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: toggle_switch
|
||||||
|
sha256: dca04512d7c23ed320d6c5ede1211a404f177d54d353bf785b07d15546a86ce5
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -890,14 +1201,54 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
url_launcher_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_linux
|
||||||
|
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.1"
|
||||||
|
url_launcher_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_platform_interface
|
||||||
|
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
url_launcher_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_web
|
||||||
|
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
url_launcher_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_windows
|
||||||
|
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.4"
|
||||||
|
uuid:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.2"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_math
|
name: vector_math
|
||||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.2.0"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -938,6 +1289,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.3"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "5.13.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -946,6 +1305,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.0"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -955,5 +1322,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.7.0 <4.0.0"
|
dart: ">=3.10.0 <4.0.0"
|
||||||
flutter: ">=3.29.0"
|
flutter: ">=3.38.0"
|
||||||
|
|||||||
114
pubspec.yaml
@@ -1,113 +1 @@
|
|||||||
name: food_hub_app
|
name: food_hub_app
|
||||||
description: "A new Flutter project."
|
|
||||||
# The following line prevents the package from being accidentally published to
|
|
||||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
|
||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|
||||||
|
|
||||||
# The following defines the version and build number for your application.
|
|
||||||
# A version number is three numbers separated by dots, like 1.2.43
|
|
||||||
# followed by an optional build number separated by a +.
|
|
||||||
# Both the version and the builder number may be overridden in flutter
|
|
||||||
# build by specifying --build-name and --build-number, respectively.
|
|
||||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
|
||||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
|
||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
|
||||||
# Read more about iOS versioning at
|
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
|
||||||
version: 1.0.0+1
|
|
||||||
|
|
||||||
environment:
|
|
||||||
sdk: ^3.7.0
|
|
||||||
|
|
||||||
# Dependencies specify other packages that your package needs in order to work.
|
|
||||||
# To automatically upgrade your package dependencies to the latest versions
|
|
||||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
|
||||||
# dependencies can be manually updated by changing the version numbers below to
|
|
||||||
# the latest version available on pub.dev. To see which dependencies have newer
|
|
||||||
# versions available, run `flutter pub outdated`.
|
|
||||||
dependencies:
|
|
||||||
flutter:
|
|
||||||
sdk: flutter
|
|
||||||
flutter_localizations:
|
|
||||||
sdk: flutter
|
|
||||||
# The following adds the Cupertino Icons fonts to your application.
|
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
|
||||||
cupertino_icons: ^1.0.8
|
|
||||||
dio: ^5.7.0
|
|
||||||
timelines_plus: ^1.0.7
|
|
||||||
table_calendar: ^3.1.3
|
|
||||||
flutter_form_builder: ^10.0.0
|
|
||||||
form_builder_validators: ^11.1.2
|
|
||||||
graphic: ^2.6.0
|
|
||||||
intl: ^0.19.0
|
|
||||||
tdesign_flutter: ^0.2.3
|
|
||||||
json_annotation: ^4.9.0
|
|
||||||
fluttertoast: ^8.2.0
|
|
||||||
shared_preferences: ^2.3.0
|
|
||||||
logger: ^2.6.0
|
|
||||||
|
|
||||||
dependency_overrides:
|
|
||||||
tdesign_flutter_adaptation: 3.16.0
|
|
||||||
image_picker: 1.0.8
|
|
||||||
|
|
||||||
dev_dependencies:
|
|
||||||
flutter_test:
|
|
||||||
sdk: flutter
|
|
||||||
|
|
||||||
# The "flutter_lints" package below contains a set of recommended lints to
|
|
||||||
# encourage good coding practices. The lint set provided by the package is
|
|
||||||
# activated in the `analysis_options.yaml` file located at the root of your
|
|
||||||
# package. See that file for information about deactivating specific lint
|
|
||||||
# rules and activating additional ones.
|
|
||||||
flutter_lints: ^5.0.0
|
|
||||||
# flutter pub run build_runner build
|
|
||||||
build_runner: ^2.4.5
|
|
||||||
json_serializable: ^6.7.1
|
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
|
||||||
|
|
||||||
# The following section is specific to Flutter packages.
|
|
||||||
flutter:
|
|
||||||
|
|
||||||
# The following line ensures that the Material Icons fonts is
|
|
||||||
# included with your application, so that you can use the icons in
|
|
||||||
# the material Icons class.
|
|
||||||
uses-material-design: true
|
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
|
||||||
# assets:
|
|
||||||
# - images/a_dot_burr.jpeg
|
|
||||||
# - images/a_dot_ham.jpeg
|
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
|
||||||
# https://flutter.dev/to/resolution-aware-images
|
|
||||||
|
|
||||||
# For details regarding adding assets from package dependencies, see
|
|
||||||
# https://flutter.dev/to/asset-from-package
|
|
||||||
|
|
||||||
# To add custom fonts to your application, add a fonts section here,
|
|
||||||
# in this "flutter" section. Each entry in this list should have a
|
|
||||||
# "family" key with the fonts family name, and a "fonts" key with a
|
|
||||||
# list giving the asset and other descriptors for the fonts. For
|
|
||||||
# example:
|
|
||||||
# fonts:
|
|
||||||
# - family: Schyler
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/Schyler-Regular.ttf
|
|
||||||
# - asset: fonts/Schyler-Italic.ttf
|
|
||||||
# style: italic
|
|
||||||
# - family: Trajan Pro
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/TrajanPro.ttf
|
|
||||||
# - asset: fonts/TrajanPro_Bold.ttf
|
|
||||||
# weight: 700
|
|
||||||
#
|
|
||||||
# For details regarding fonts from package dependencies,
|
|
||||||
# see https://flutter.dev/to/font-from-package
|
|
||||||
fonts:
|
|
||||||
- family: CustomFont
|
|
||||||
fonts:
|
|
||||||
- asset: fonts/custom.ttf
|
|
||||||
@@ -6,9 +6,15 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <rive_native/rive_native_plugin.h>
|
||||||
|
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||||
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
FileSelectorWindowsRegisterWithRegistrar(
|
RiveNativePluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("RiveNativePlugin"));
|
||||||
|
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||||
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
file_selector_windows
|
rive_native
|
||||||
|
share_plus
|
||||||
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||