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