feat:更新美食记录表单

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

View File

@@ -1,28 +1,31 @@
import 'package:flutter_common/utils/convert_utils.dart';
import 'package:flutter_common/utils/http_utils.dart';
import 'package:food_hub_app/config/app_config.dart';
import 'package:food_hub_app/models/moment.dart';
import 'package:food_hub_app/utils/http_util.dart';
import 'package:food_hub_app/utils/index.dart';
final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl);
Future<bool> addMomentApi(Moment moment) {
return HttpUtil().post<bool>("/moment", data: moment);
return httpUtil.post<bool>("/moment", data: moment);
}
Future<bool> updateMomentApi(int id, Moment moment) {
return HttpUtil().put<bool>("/moment/$id", data: moment);
return httpUtil.put<bool>("/moment/$id", data: moment);
}
Future<bool> deleteMomentApi(int id) {
return HttpUtil().delete<bool>("/moment/$id");
return httpUtil.delete<bool>("/moment/$id");
}
Future<List<Moment>> queryMomentListApi() {
return HttpUtil().get<List<Moment>>(
return httpUtil.get<List<Moment>>(
"/moment",
converter: (data) => convertListResponse<Moment>(data, Moment.fromJson),
converter: (data) => convertList<Moment>(data, Moment.fromJson),
);
}
Future<PageMoment> queryMomentByPageApi(int currentPage, int pageSize) {
return HttpUtil().get<PageMoment>(
return httpUtil.get<PageMoment>(
"/moment/page",
queryParameters: {"currentPage": currentPage, "pageSize": pageSize},
converter: (data) => PageMoment.fromJson(data),
@@ -30,13 +33,13 @@ Future<PageMoment> queryMomentByPageApi(int currentPage, int pageSize) {
}
Future<bool> addMomentCommentApi(int id, String content) {
return HttpUtil().post<bool>("/moment/$id/comment", data: content);
return httpUtil.post<bool>("/moment/$id/comment", data: content);
}
Future<bool> addMomentLikeApi(int id) {
return HttpUtil().post<bool>("/food/moment/$id/like");
return httpUtil.post<bool>("/food/moment/$id/like");
}
Future<bool> deleteMomentLikeApi(int id) {
return HttpUtil().delete<bool>("/food/moment/$id/like");
return httpUtil.delete<bool>("/food/moment/$id/like");
}

View File

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

View File

@@ -1,10 +1,13 @@
import 'package:flutter_common/utils/http_utils.dart';
import 'package:food_hub_app/config/app_config.dart';
import 'package:food_hub_app/models/session.dart';
import 'package:food_hub_app/utils/http_util.dart';
final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl);
Future<Session> loginApi(String username, String password) {
return HttpUtil().post<Session>(
return httpUtil.post<Session>(
"/session",
queryParameters: {"username": username, "password": password},
converter: (data) => Session.fromJson(data),
);
}
}

View File

@@ -1,31 +1,35 @@
import 'package:flutter_common/models/common_model.dart';
import 'package:flutter_common/utils/convert_utils.dart';
import 'package:flutter_common/utils/http_utils.dart';
import 'package:food_hub_app/config/app_config.dart';
import 'package:food_hub_app/models/stats.dart';
import 'package:food_hub_app/utils/http_util.dart';
import 'package:food_hub_app/utils/index.dart';
final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl);
Future<SummaryStats> queryStatsApi() {
return HttpUtil().get<SummaryStats>(
return httpUtil.get<SummaryStats>(
"/food/stats",
converter: (data) => SummaryStats.fromJson(data),
);
}
Future<List<ChartData>> queryRecordStatsApi() {
return HttpUtil().get<List<ChartData>>(
return httpUtil.get<List<ChartData>>(
"/food/stats/record",
converter: (data) => convertListResponse<ChartData>(data, ChartData.fromJson),
converter: (data) => convertList<ChartData>(data, ChartData.fromJson),
);
}
Future<List<ChartData>> queryCategoryStatsApi() {
return HttpUtil().get<List<ChartData>>(
return httpUtil.get<List<ChartData>>(
"/food/stats/category",
converter: (data) => convertListResponse<ChartData>(data, ChartData.fromJson),
converter: (data) => convertList<ChartData>(data, ChartData.fromJson),
);
}
Future<List<ChartData>> queryRankStatsApi() {
return HttpUtil().get<List<ChartData>>(
return httpUtil.get<List<ChartData>>(
"/food/stats/rank",
converter: (data) => convertListResponse<ChartData>(data, ChartData.fromJson),
converter: (data) => convertList<ChartData>(data, ChartData.fromJson),
);
}
}

View File

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

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/layout/theme_layout.dart';
import 'package:flutter_common/layout/theme_layout.dart';
class AppDrawer extends StatefulWidget {
const AppDrawer({super.key});

View File

@@ -1,128 +0,0 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/provider/theme_provider.dart';
import 'package:food_hub_app/utils/theme_utils.dart';
import 'package:provider/provider.dart';
class ThemeLayout extends StatelessWidget {
final Widget? child;
const ThemeLayout({super.key, this.child});
Widget _buildThemeColorList(BuildContext context, ThemeProvider provider) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1.2,
),
itemCount: provider.availableThemes.length,
itemBuilder: (context, index) {
final themeColor = provider.availableThemes[index];
final isSelected = provider.currentTheme == themeColor;
return _buildThemeColorItem(
themeColor: themeColor,
isSelected: isSelected,
onTap: () => provider.changeTheme(themeColor),
);
},
),
);
}
Widget _buildThemeColorItem({
required ThemeColor themeColor,
required bool isSelected,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
color:
isSelected
? themeColor.primaryColor.withAlpha(50)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? themeColor.primaryColor : Colors.grey.shade300,
width: isSelected ? 2 : 1,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 颜色圆点
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: themeColor.primaryColor,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(10),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
),
const SizedBox(height: 4),
Text(
themeColor.name,
style: TextStyle(
fontSize: 10,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color:
isSelected ? themeColor.primaryColor : Colors.grey.shade600,
),
),
],
),
),
);
}
Widget _buildDarkModeSection(BuildContext context, ThemeProvider provider) {
return ListTile(
leading: Icon(
provider.isDarkMode ? Icons.dark_mode : Icons.light_mode,
color: Theme.of(context).colorScheme.primary,
),
title: const Text('暗黑模式'),
trailing: Switch(
value: provider.isDarkMode,
onChanged: (value) {
provider.toggleDarkMode(value);
},
),
onTap: () {
provider.toggleDarkMode(!provider.isDarkMode);
},
);
}
@override
Widget build(BuildContext context) {
final provider = context.watch<ThemeProvider>();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text('主题颜色', style: Theme.of(context).textTheme.titleMedium),
),
_buildDarkModeSection(context, provider),
_buildThemeColorList(context, provider),
],
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/models/common_model.dart';
import 'package:flutter_common/utils/toast_util.dart';
import 'package:food_hub_app/apis/moment.dart';
import 'package:food_hub_app/apis/recipe.dart';
import 'package:food_hub_app/apis/stats.dart';
@@ -7,8 +9,10 @@ import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/models/stats.dart';
import 'package:food_hub_app/utils/date_util.dart';
import 'package:food_hub_app/utils/index.dart';
import 'package:food_hub_app/views/record.dart';
class FoodProvider with ChangeNotifier {
late RecordTab currentRecordTab = RecordTab.recipe;
late FoodRecord recordFormItem;
late Moment momentFormItem;
@@ -20,6 +24,7 @@ class FoodProvider with ChangeNotifier {
late List<String> categoryList = [];
late List<RecipeSummary> recipeSummaryList = [];
int selectYear = DateTime.now().year;
DateTime selectedDay = DateTime.now();
DateTime focusedDay = DateTime.now();
@@ -61,6 +66,18 @@ class FoodProvider with ChangeNotifier {
notifyListeners();
}
Future<void> onRecordTabChange() async {
switch (currentRecordTab) {
case RecordTab.recipe:
await refreshRecipeList();
break;
case RecordTab.calendar:
case RecordTab.timeline:
await refreshRecordList();
break;
}
}
Future<void> queryCategoryList() async {
try {
error = null;
@@ -98,16 +115,31 @@ class FoodProvider with ChangeNotifier {
}
}
Future<void> refreshRecordList(String? startDate, String? endDate) async {
Future<void> refreshRecordList() async {
if (isLoading) return;
if (currentRecordTab == RecordTab.recipe) return;
try {
isLoading = true;
error = null;
notifyListeners();
startDate ??= getFirstDayOfMonth(focusedDay);
endDate ??= getLastDayOfMonth(focusedDay);
late String startDate;
late String endDate;
switch (currentRecordTab) {
case RecordTab.calendar:
startDate = getFirstDayOfMonth(focusedDay);
endDate = getLastDayOfMonth(focusedDay);
break;
case RecordTab.timeline:
startDate = '$selectYear-01-01';
endDate = '$selectYear-12-31';
break;
case RecordTab.recipe:
break;
}
final result = await queryRecordApi(startDate, endDate);
@@ -129,6 +161,30 @@ class FoodProvider with ChangeNotifier {
}
}
Future<void> handleRecord() async {
if (isLoading) return;
try {
isLoading = true;
error = null;
notifyListeners();
if (isEditing) {
await updateRecordApi(recordFormItem.id!, recordFormItem);
ToastUtil.success('更新记录成功');
} else {
await addRecordApi(recordFormItem);
ToastUtil.success('上传记录成功');
}
} catch (e) {
error = '处理数据失败: $e';
debugPrint('处理数据失败: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
void refreshSelectRecord() {
selectRecordList =
recordList
@@ -136,7 +192,7 @@ class FoodProvider with ChangeNotifier {
.toList();
}
Future<void> refreshBlogStats() async {
Future<void> refreshFoodStats() async {
if (isLoading) return;
try {

View File

@@ -1,76 +0,0 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/utils/theme_utils.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ThemeProvider with ChangeNotifier {
static const String _themeKey = 'selected_theme';
static const String _darkModeKey = 'is_dark_mode';
bool isDarkMode = false;
ThemeColor currentTheme = defaultThemes[0];
late SharedPreferences _prefs;
bool isInitialized = false;
ThemeProvider() {
_initPreferences();
}
// 初始化 SharedPreferences
Future<void> _initPreferences() async {
_prefs = await SharedPreferences.getInstance();
_loadPreferences();
isInitialized = true;
notifyListeners();
}
// 加载存储的设置
void _loadPreferences() {
isDarkMode = _prefs.getBool(_darkModeKey) ?? false;
// 加载主题色
final themeName = _prefs.getString(_themeKey);
if (themeName != null) {
currentTheme = getThemeColor(themeName);
}
}
// 保存主题色
Future<void> _saveTheme() async {
await _prefs.setString(_themeKey, currentTheme.name);
}
// 保存暗黑模式
Future<void> _saveDarkMode() async {
await _prefs.setBool(_darkModeKey, isDarkMode);
}
List<ThemeColor> get availableThemes => defaultThemes;
// 切换明暗模式
void toggleDarkMode(bool value) {
isDarkMode = value;
_saveDarkMode();
notifyListeners();
}
// 更改主题色
void changeTheme(ThemeColor theme) {
currentTheme = theme;
_saveTheme();
notifyListeners();
}
ThemeData get currentThemeData {
return ThemeData(
primarySwatch: currentTheme.materialColor,
colorScheme: ColorScheme.fromSeed(
seedColor: currentTheme.primaryColor,
brightness: isDarkMode ? Brightness.dark : Brightness.light,
),
useMaterial3: true,
fontFamily: 'CustomFont',
);
}
}

View File

@@ -1,245 +0,0 @@
import 'package:dio/dio.dart';
import 'package:food_hub_app/config/app_config.dart';
import 'package:food_hub_app/utils/sp_util.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'log_util.dart';
class HttpUtil {
static final HttpUtil _instance = HttpUtil._internal();
factory HttpUtil() => _instance;
late Dio _dio;
// 请求头配置
Map<String, dynamic> headers = {
'Content-Type': 'application/json;charset=UTF-8',
'Accept': 'application/json',
};
// 超时时间
final int timeout = 5;
HttpUtil._internal() {
// 初始化Dio实例
BaseOptions options = BaseOptions(
baseUrl: AppConfig.baseApiUrl,
connectTimeout: Duration(seconds: timeout),
receiveTimeout: Duration(seconds: timeout),
headers: headers,
);
_dio = Dio(options);
// 添加请求拦截器
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
logger.d("请求URL: ${options.uri}");
if (options.data != null) {
logger.d("请求参数: ${options.data}");
}
if (SPUtil.getString('token').isNotEmpty) {
options.headers['satoken'] = SPUtil.getString('token');
}
return handler.next(options);
},
),
);
// 添加响应拦截器
_dio.interceptors.add(
InterceptorsWrapper(
onResponse: (response, handler) {
logger.d("响应状态码: ${response.statusCode}");
// logger.d("响应数据: ${response.data}");
return handler.next(response);
},
),
);
// 添加错误拦截器
_dio.interceptors.add(
InterceptorsWrapper(
onError: (DioException e, handler) {
_handleError(e);
return handler.next(e);
},
),
);
}
// 基础请求方法(处理所有类型的请求)
Future<T> _request<T>(
String path, {
required String method,
dynamic data,
Map<String, dynamic>? queryParameters,
T Function(dynamic data)? converter,
}) async {
try {
Response response = await _dio.request(
path,
data: data,
queryParameters: queryParameters,
options: Options(method: method),
);
// 处理响应数据
if (converter != null) {
return converter(response.data);
}
// 没有转换器时尝试直接返回(可能不安全,建议提供转换器)
return response.data;
} catch (e) {
rethrow;
}
}
// GET请求
Future<T> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
T Function(dynamic data)? converter,
}) => _request(
path,
method: "GET",
queryParameters: queryParameters,
converter: converter,
);
// POST请求
Future<T> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
T Function(dynamic data)? converter,
}) => _request(
path,
method: "POST",
data: data,
queryParameters: queryParameters,
converter: converter,
);
// PUT请求
Future<T> put<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
T Function(dynamic data)? converter,
}) => _request(
path,
method: "PUT",
data: data,
queryParameters: queryParameters,
converter: converter,
);
// DELETE请求
Future<T> delete<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
T Function(dynamic data)? converter,
}) => _request(
path,
method: "DELETE",
data: data,
queryParameters: queryParameters,
converter: converter,
);
// 文件上传
Future<dynamic> upload(
String path,
String filePath, {
Map<String, dynamic>? queryParameters,
}) async {
try {
FormData formData = FormData.fromMap({
'file': await MultipartFile.fromFile(filePath),
});
Response response = await _dio.post(
path,
data: formData,
queryParameters: queryParameters,
);
return response.data;
} catch (e) {
_handleError(e);
rethrow;
}
}
// 错误处理
void _handleError(dynamic error) {
String errorMessage = '未知错误';
if (error is DioException) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
errorMessage = '连接超时,请检查网络连接';
break;
case DioExceptionType.sendTimeout:
errorMessage = '发送超时,请检查网络连接';
break;
case DioExceptionType.receiveTimeout:
errorMessage = '接收超时,请检查网络连接';
break;
case DioExceptionType.cancel:
errorMessage = '请求已取消';
break;
case DioExceptionType.badCertificate:
errorMessage = '证书验证失败';
break;
case DioExceptionType.badResponse:
// 处理HTTP响应错误4xx, 5xx
final statusCode = error.response?.statusCode ?? 0;
final responseData = error.response?.data;
if (responseData is Map<String, dynamic> && responseData.containsKey('message')) {
// 服务器返回了自定义错误消息
errorMessage = responseData['message']?.toString() ?? '未知错误';
} else {
errorMessage = _getHttpErrorMessage(statusCode);
}
break;
case DioExceptionType.connectionError:
errorMessage = '网络连接错误,请检查网络设置';
break;
case DioExceptionType.unknown:
if (error.error != null) {
errorMessage = '未知错误: ${error.error.toString()}';
}
errorMessage = '发生未知错误';
break;
}
} else {
errorMessage = '非Dio错误: $error';
}
print(errorMessage);
showErrorToast(errorMessage);
}
String _getHttpErrorMessage(int statusCode) {
// 根据HTTP状态码返回对应的错误消息
switch (statusCode) {
case 400: return '错误请求,请检查参数';
case 401: return '未授权,请登录';
case 403: return '禁止访问,权限不足';
case 404: return '资源不存在';
case 405: return '方法不允许';
case 408: return '请求超时';
case 500: return '服务器内部错误';
case 502: return '网关错误';
case 503: return '服务不可用';
case 504: return '网关超时';
default: return 'HTTP错误: $statusCode';
}
}
}

View File

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

View File

@@ -1,12 +0,0 @@
import 'package:logger/logger.dart';
// 全局日志实例,在整个项目中共享
final Logger logger = Logger(
printer: PrettyPrinter(
methodCount: 1,
colors: true,
dateTimeFormat: DateTimeFormat.dateAndTime,
),
// 可选:配置输出到文件(需配合文件操作库)
// output: FileOutput(file: File('logs/app.log')),
);

View File

@@ -1,54 +0,0 @@
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:file_picker/file_picker.dart';
import 'package:food_hub_app/config/app_config.dart';
import 'package:minio/io.dart';
import 'package:minio/minio.dart';
class MinIOHelper {
static final MinIOHelper _instance = MinIOHelper._internal();
factory MinIOHelper() => _instance;
MinIOHelper._internal() {
_minio = Minio(
endPoint: AppConfig.rustfsIp,
port: 9100,
accessKey: "tHSFfcDW8qpCzKa2Xg6Y",
secretKey: "oq79EeYJ4jdczRp2IHUMCnbKtSw58NgDlG3sOkvX",
useSSL: false,
);
}
late Minio _minio;
Future<String> uploadFile({
required PlatformFile file,
Function(double)? onProgress,
}) async {
try {
String hashName = await _generateMD5HashName(file.path!);
String fileName = '$hashName${_getFileExtension(file.name)}';
await _minio.fPutObject(AppConfig.bucketName, fileName, file.path!);
return fileName;
} catch (e) {
throw Exception('文件上传失败: $e');
}
}
String _getFileExtension(String fileName) {
if (fileName.contains('.')) {
return '.${fileName.split('.').last.toLowerCase()}';
}
return '';
}
Future<String> _generateMD5HashName(String filePath) async {
final file = File(filePath);
final bytes = await file.readAsBytes();
final hash = md5.convert(bytes);
return hash.toString();
}
}

View File

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

View File

@@ -1,61 +0,0 @@
import 'package:shared_preferences/shared_preferences.dart';
class SPUtil {
static late SharedPreferences _prefs;
/// 初始化
static Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
/// 存储数据
static Future<bool> set(String key, dynamic value) {
if (value is String) return _prefs.setString(key, value);
if (value is int) return _prefs.setInt(key, value);
if (value is bool) return _prefs.setBool(key, value);
if (value is double) return _prefs.setDouble(key, value);
if (value is List<String>) return _prefs.setStringList(key, value);
throw ArgumentError('Unsupported value type: ${value.runtimeType}');
}
/// 获取数据
static dynamic get(String key, [dynamic defaultValue]) {
if (!_prefs.containsKey(key)) return defaultValue;
return _prefs.get(key) ?? defaultValue;
}
/// 获取字符串
static String getString(String key, [String defaultValue = '']) {
return _prefs.getString(key) ?? defaultValue;
}
/// 获取布尔值
static bool getBool(String key, [bool defaultValue = false]) {
return _prefs.getBool(key) ?? defaultValue;
}
/// 获取整数
static int getInt(String key, [int defaultValue = 0]) {
return _prefs.getInt(key) ?? defaultValue;
}
/// 获取浮点数
static double getDouble(String key, [double defaultValue = 0.0]) {
return _prefs.getDouble(key) ?? defaultValue;
}
/// 获取字符串列表
static List<String> getStringList(String key, [List<String> defaultValue = const []]) {
return _prefs.getStringList(key) ?? defaultValue;
}
/// 删除数据
static Future<bool> remove(String key) {
return _prefs.remove(key);
}
/// 清空所有数据
static Future<bool> clear() {
return _prefs.clear();
}
}

View File

@@ -1,87 +0,0 @@
import 'package:flutter/material.dart';
class ThemeColor {
final String name;
final Color primaryColor;
final MaterialColor materialColor;
ThemeColor({
required this.name,
required this.primaryColor,
required this.materialColor,
});
}
final List<ThemeColor> defaultThemes = [
ThemeColor(
name: '梦幻紫',
primaryColor: Color(0xFF8B5CF6),
materialColor: MaterialColor(0xFF8B5CF6, {
50: Color(0xFFF5F3FF),
100: Color(0xFFEDE9FE),
200: Color(0xFFDDD6FE),
300: Color(0xFFC4B5FD),
400: Color(0xFFA78BFA),
500: Color(0xFF8B5CF6),
600: Color(0xFF7C3AED),
700: Color(0xFF6D28D9),
800: Color(0xFF5B21B6),
900: Color(0xFF4C1D95),
}),
),
ThemeColor(
name: '活力橙',
primaryColor: Color(0xFFF59E0B),
materialColor: MaterialColor(0xFFF59E0B, {
50: Color(0xFFFFFBEB),
100: Color(0xFFFEF3C7),
200: Color(0xFFFDE68A),
300: Color(0xFFFCD34D),
400: Color(0xFFFBBF24),
500: Color(0xFFF59E0B),
600: Color(0xFFD97706),
700: Color(0xFFB45309),
800: Color(0xFF92400E),
900: Color(0xFF78350F),
}),
),
ThemeColor(
name: '浪漫粉',
primaryColor: Color(0xFFEC4899),
materialColor: MaterialColor(0xFFEC4899, {
50: Color(0xFFFDF2F8),
100: Color(0xFFFCE7F3),
200: Color(0xFFFBCFE8),
300: Color(0xFFF9A8D4),
400: Color(0xFFF472B6),
500: Color(0xFFEC4899),
600: Color(0xFFDB2777),
700: Color(0xFFBE185D),
800: Color(0xFF9D174D),
900: Color(0xFF831843),
}),
),
ThemeColor(
name: '清新青',
primaryColor: Color(0xFF06B6D4),
materialColor: MaterialColor(0xFF06B6D4, {
50: Color(0xFFF0FDFA),
100: Color(0xFFCCFBF1),
200: Color(0xFF99F6E4),
300: Color(0xFF5EEAD4),
400: Color(0xFF2DD4BF),
500: Color(0xFF06B6D4),
600: Color(0xFF0891B2),
700: Color(0xFF0E7490),
800: Color(0xFF155E75),
900: Color(0xFF164E63),
}),
),
];
ThemeColor getThemeColor(String themeName) {
return defaultThemes.firstWhere(
(theme) => theme.name == themeName,
orElse: () => defaultThemes[0],
);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,6 @@ class RecordPage extends StatefulWidget {
}
class _RecordPageState extends State<RecordPage> {
RecordTab _currentTab = RecordTab.recipe;
final List<Widget> _tabPages = [
RecipeList(),
RecipeCalendar(),
@@ -49,22 +48,8 @@ class _RecordPageState extends State<RecordPage> {
}
void _onTabChange(RecordTab tab, FoodProvider provider) {
setState(() {
_currentTab = tab;
});
switch (tab) {
case RecordTab.recipe:
provider.refreshRecipeList();
break;
case RecordTab.calendar:
provider.refreshRecordList(null, null);
break;
case RecordTab.timeline:
final year = DateTime.now().year;
provider.refreshRecordList("$year-01-01", "$year-12-31");
break;
}
provider.currentRecordTab = tab;
provider.onRecordTabChange();
}
@override
@@ -75,11 +60,14 @@ class _RecordPageState extends State<RecordPage> {
children: [
_buildTabs(
context: context,
currentTab: _currentTab,
currentTab: provider.currentRecordTab,
onTabChanged: (tab) => _onTabChange(tab, provider),
),
Expanded(
child: IndexedStack(index: _currentTab.index, children: _tabPages),
child: IndexedStack(
index: provider.currentRecordTab.index,
children: _tabPages,
),
),
],
);

View File

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

View File

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

View File

@@ -1,253 +0,0 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/models/stats.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
Widget lineChart({
required BuildContext context,
required String xAxisName,
required String yAxisName,
required String unit,
required List<ChartData> data,
}) {
return SfCartesianChart(
// 图表标题
// title: ChartTitle(text: '2023年上半年销售额万元'),
// X轴配置类别轴
primaryXAxis: CategoryAxis(majorGridLines: MajorGridLines(width: 0)),
// Y轴配置数值轴
// primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName$unit')),
// 启用图例
// legend: const Legend(isVisible: true),
// 启用交互提示(点击数据点显示详情)
tooltipBehavior: TooltipBehavior(
enable: true,
format: 'point.x: point.y $unit',
),
// 折线图数据系列
series: [
LineSeries<ChartData, String>(
dataSource: data,
// X轴数据映射
xValueMapper: (ChartData chart, _) => chart.name,
// Y轴数据映射
yValueMapper: (ChartData chart, _) => chart.value,
// 线条颜色
color: Theme.of(context).primaryColor,
// 线条宽度
width: 3,
// 数据点样式
markerSettings: const MarkerSettings(
isVisible: true,
color: Colors.white,
shape: DataMarkerType.circle,
height: 6,
width: 6,
),
// 折线名称(会显示在图例中)
name: yAxisName,
// 启用数据标签(直接显示数值)
dataLabelSettings: const DataLabelSettings(
isVisible: true,
color: Colors.white,
opacity: 0,
),
// 动画效果
animationDuration: 2000, // 动画时长(毫秒)
),
],
);
}
Widget barChart({
required BuildContext context,
required String xAxisName,
required String yAxisName,
required String unit,
required List<ChartData> data,
}) {
return SfCartesianChart(
// X轴配置类别轴
primaryXAxis: CategoryAxis(
majorGridLines: MajorGridLines(width: 0),
title: AxisTitle(text: xAxisName),
),
// Y轴配置数值轴
primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName$unit')),
// 启用交互提示
tooltipBehavior: TooltipBehavior(
enable: true,
format: 'point.x: point.y $unit',
),
// 柱状图数据系列
series: [
ColumnSeries<ChartData, String>(
dataSource: data,
// X轴数据映射
xValueMapper: (ChartData chart, _) => chart.name,
// Y轴数据映射
yValueMapper: (ChartData chart, _) => chart.value,
// 名称
name: yAxisName,
// 柱子颜色
color: Theme.of(context).primaryColor,
// 柱子宽度0-1之间1表示占满类别间隔
width: 0.6,
// 柱子边框
borderWidth: 1,
borderColor: Colors.black12,
// 数据标签
dataLabelSettings: const DataLabelSettings(
isVisible: true,
color: Colors.white,
opacity: 0,
alignment: ChartAlignment.center,
),
// 动画效果
animationDuration: 2000,
),
],
);
}
Widget pieChart({
required BuildContext context,
required String unit,
required List<ChartData> data,
}) {
// 计算 value 的总和
double sumValue = data.fold(0.0, (sum, item) => sum + item.value);
return SfCircularChart(
// 饼图标题
// title: ChartTitle(text: '菜谱类别占比分布'),
// 启用图例
legend: const Legend(isVisible: true, position: LegendPosition.right),
// 启用交互提示(点击扇区显示详情)
tooltipBehavior: TooltipBehavior(
enable: true,
format: 'point.x: point.y $unit',
),
// 饼图系列配置
series: [
PieSeries<ChartData, String>(
dataSource: data,
// 类别映射(饼图扇区名称)
xValueMapper: (ChartData data, _) => data.name,
// 数值映射(扇区大小占比)
yValueMapper: (ChartData data, _) => data.value,
// 扇区半径0-1之间1表示充满容器
// radius: '50%',
// 启用扇区分离效果
explode: true,
// 指定分离的扇区索引(这里分离第一个扇区)
explodeIndex: 0,
// 分离距离
explodeOffset: '5%',
dataLabelMapper: (ChartData data, _) {
final percentage = (data.value / sumValue * 100).toStringAsFixed(0);
return '$percentage%';
},
// 数据标签(显示在扇区上的文本)
dataLabelSettings: DataLabelSettings(isVisible: true),
// 动画效果
animationDuration: 2000,
),
],
);
}
Widget buildChartTitle({required BuildContext context, required String title}) {
return Row(
children: [
Icon(Icons.insert_chart, color: Theme.of(context).colorScheme.primary),
Text(title),
],
);
}
Widget buildChartDivider({required BuildContext context}) {
return Divider(
height: 1,
thickness: 1,
color: Theme.of(context).colorScheme.primary,
indent: 0,
endIndent: 0,
);
}
Color _getRankColor(int index) {
switch (index) {
case 0: // 第1名
return Colors.amber; // 金色
case 1: // 第2名
return Colors.grey; // 银色
case 2: // 第3名
return Colors.orange[700]!; // 铜色
default:
return Colors.black; // 普通颜色
}
}
Widget rankChart({required String unit, required List<ChartData> data}) {
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
final item = data[index];
final rank = index + 1;
return SizedBox(
height: 35,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
width: 25,
alignment: Alignment.center,
child: Text(
'$rank.',
style: TextStyle(
fontWeight: FontWeight.bold,
color: _getRankColor(index),
),
),
),
Text(item.name, style: TextStyle(color: _getRankColor(index))),
],
),
Text(
'${item.value.toStringAsFixed(0)} $unit',
style: TextStyle(color: _getRankColor(index)),
),
],
),
);
},
);
}

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:food_hub_app/config/app_config.dart';
import 'package:photo_view/photo_view.dart';
@@ -91,138 +89,123 @@ class _ImagePreviewPageState extends State<ImagePreviewPage> {
}
}
Widget buildNetworkImage(
BuildContext context,
List<String> imageUrls,
int index,
) {
return AspectRatio(
aspectRatio: 4 / 3,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: GestureDetector(
onTap: () {
showFullScreenImage(context, imageUrls, index);
},
child: Image.network(
'${AppConfig.imageBaseUrl}${imageUrls[index]}',
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return buildImageLoadingIndicator(loadingProgress);
},
errorBuilder: (context, error, stackTrace) => buildErrorImage(),
),
class CommonImage extends StatelessWidget {
final List<String> imageUrls;
final int index;
const CommonImage({super.key, required this.imageUrls, required this.index});
Widget _buildImageLoadingIndicator(ImageChunkEvent? progress) {
final value =
progress?.expectedTotalBytes != null
? progress!.cumulativeBytesLoaded / progress.expectedTotalBytes!
: null;
return Center(
child: SizedBox(
width: 30,
height: 30,
child: CircularProgressIndicator(value: value),
),
),
);
}
);
}
Widget buildImagePreviewItem({
required BuildContext context,
required List<String> imageUrls,
required int index,
required VoidCallback onRemoveImage,
}) {
return Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Stack(
children: [
buildNetworkImage(context, imageUrls, index),
buildDeleteImage(onRemoveImage: onRemoveImage),
],
),
),
);
}
Widget _buildErrorImage() {
return Container(
color: Colors.grey[200],
child: const Icon(Icons.image, color: Colors.grey, size: 30),
);
}
Widget buildImageLoadingIndicator(ImageChunkEvent? loadingProgress) {
return Center(
child: SizedBox(
width: 30,
height: 30,
child: CircularProgressIndicator(
value:
loadingProgress?.expectedTotalBytes != null
? loadingProgress!.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
),
);
}
Widget buildErrorImage() {
return Container(
color: Colors.grey[200],
child: const Icon(Icons.image, color: Colors.grey, size: 30),
);
}
Widget _buildPhotoView(ImageProvider imageProvider) {
return PhotoView(
imageProvider: imageProvider,
backgroundDecoration: const BoxDecoration(color: Colors.transparent),
minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 2,
initialScale: PhotoViewComputedScale.contained,
loadingBuilder: (context, event) => buildImageLoadingIndicator(event),
errorBuilder: (context, error, stackTrace) => buildErrorImage(),
);
}
Widget _buildCloseImage(BuildContext context) {
return Positioned(
top: MediaQuery.of(context).padding.top + 10,
right: 20,
child: IconButton(
icon: Icon(Icons.close, color: Colors.white, size: 30),
onPressed: () => Navigator.of(context).pop(),
),
);
}
void showFullScreenImage(
BuildContext context,
List<String> imageUrls,
int index,
) {
Navigator.push(
context,
MaterialPageRoute(
void _navigateToImagePreview(
BuildContext context,
List<String> imageUrls,
int index,
) {
final route = MaterialPageRoute(
builder:
(context) => ImagePreviewPage(images: imageUrls, initialIndex: index),
),
);
// Navigator.of(context).push(
// PageRouteBuilder(
// opaque: false,
// pageBuilder: (
// BuildContext context,
// Animation<double> animation,
// Animation<double> secondaryAnimation,
// ) {
// return Scaffold(
// backgroundColor: Colors.black.withAlpha(200),
// body: Stack(
// children: [
// // 可缩放图片
// Positioned.fill(child: _buildPhotoView(imageProvider)),
// // 关闭按钮
// _buildCloseImage(context),
// ],
// ),
// );
// },
// ),
// );
);
Navigator.push(context, route);
}
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 4 / 3,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: GestureDetector(
onTap: () {
_navigateToImagePreview(context, imageUrls, index);
},
child: Image.network(
'${AppConfig.imageBaseUrl}${imageUrls[index]}',
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return _buildImageLoadingIndicator(loadingProgress);
},
errorBuilder: (context, error, stackTrace) => _buildErrorImage(),
),
),
),
);
}
}
Widget buildImageUploadButton({required VoidCallback onPickImage}) {
class ImagePreview extends StatelessWidget {
final List<String> imageUrls;
final int index;
final VoidCallback onRemoveImage;
const ImagePreview({
super.key,
required this.imageUrls,
required this.index,
required this.onRemoveImage,
});
Widget _buildDeleteImage() {
return Positioned(
top: 0,
right: 0,
child: GestureDetector(
onTap: onRemoveImage,
child: Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: const Icon(Icons.close, color: Colors.white, size: 16),
),
),
);
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Stack(
children: [
CommonImage(imageUrls: imageUrls, index: index),
_buildDeleteImage(),
],
),
),
);
}
}
Widget buildImageUploadButton({required VoidCallback onTap}) {
return InkWell(
onTap: onPickImage,
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
width: 96,
@@ -243,22 +226,3 @@ Widget buildImageUploadButton({required VoidCallback onPickImage}) {
),
);
}
Widget buildDeleteImage({required VoidCallback onRemoveImage}) {
return Positioned(
top: 0,
right: 0,
child: GestureDetector(
onTap: onRemoveImage,
child: Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: const Icon(Icons.close, color: Colors.white, size: 16),
),
),
);
}

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:toggle_switch/toggle_switch.dart';
Widget formLabelText({required String labelText, bool isRequired = false}) {
@@ -28,33 +27,6 @@ InputDecoration formInputDecoration({
);
}
class BuildCard extends StatelessWidget {
final Widget? child;
const BuildCard({super.key, this.child});
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Container(
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(12),
// border: Border.all(color: colors.outline.withAlpha(50), width: 1),
),
padding: const EdgeInsets.all(10),
child: child,
);
}
}
Widget buildEmptyData() {
return Center(
child: Text('暂无数据', style: TextStyle(fontSize: 16, color: Colors.grey)),
);
}
Widget buildToggleSwitch<T extends Enum>({
required BuildContext context,
required T currentTab,
@@ -153,63 +125,3 @@ Widget errorImageContainer(double height) {
child: const Icon(Icons.image, color: Colors.grey, size: 30),
);
}
/// 成功消息
void showSuccessToast(String message) {
Fluttertoast.showToast(
msg: message,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
timeInSecForIosWeb: 1,
backgroundColor: Colors.green,
textColor: Colors.white,
fontSize: 16.0,
);
}
/// 错误消息
void showErrorToast(String message) {
Fluttertoast.showToast(
msg: message,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
timeInSecForIosWeb: 1,
backgroundColor: Colors.red,
textColor: Colors.white,
fontSize: 16.0,
);
}
Widget buildLoadingIndicator(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Center(
child: Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 2)),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(colors.primary),
),
SizedBox(height: 12),
Text(
'加载中',
style: TextStyle(
fontSize: 14,
color: colors.onSurface,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}

View File

@@ -1,121 +0,0 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/widgets/common/index.dart';
class YearSelector extends StatefulWidget {
final int initialYear;
final int? minYear;
final int? maxYear;
final Function(int) onYearChanged;
const YearSelector({
super.key,
required this.initialYear,
required this.onYearChanged,
this.minYear,
this.maxYear,
});
@override
State<YearSelector> createState() => _YearSelectorState();
}
// SingleTickerProviderStateMixin 动画控制器
class _YearSelectorState extends State<YearSelector>
with SingleTickerProviderStateMixin {
late int _currentYear;
// 用于动画效果
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_currentYear = widget.initialYear;
// 初始化动画控制器
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
// 缩放动画
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.1).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
/// 切换到上一年
void _previousYear() {
if (widget.minYear == null || _currentYear > widget.minYear!) {
_animateYearChange(() {
setState(() {
_currentYear--;
});
widget.onYearChanged(_currentYear);
});
}
}
/// 切换到下一年
void _nextYear() {
if (widget.maxYear == null || _currentYear < widget.maxYear!) {
_animateYearChange(() {
setState(() {
_currentYear++;
});
widget.onYearChanged(_currentYear);
});
}
}
/// 年份变化时的动画效果
void _animateYearChange(VoidCallback onComplete) {
_animationController.forward().then((_) {
onComplete();
_animationController.reverse();
});
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
circleIconButton(
context: context,
icon: Icons.chevron_left,
isSmall: true,
onPressed: () => _previousYear(),
),
// 年份显示
AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(scale: _scaleAnimation.value, child: child);
},
child: Text(
'$_currentYear',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
),
circleIconButton(
context: context,
icon: Icons.chevron_right,
isSmall: true,
onPressed: () => _nextYear(),
)
],
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -170,7 +170,7 @@ packages:
source: hosted
version: "0.3.4+2"
crypto:
dependency: "direct main"
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
@@ -202,7 +202,7 @@ packages:
source: hosted
version: "0.7.11"
dio:
dependency: "direct main"
dependency: transitive
description:
name: dio
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
@@ -310,6 +310,13 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.0"
flutter_common:
dependency: "direct main"
description:
path: "D:\\Projects\\FlutterProjects\\flutter_common"
relative: false
source: path
version: "1.0.0+1"
flutter_form_builder:
dependency: "direct main"
description:
@@ -366,7 +373,7 @@ packages:
source: sdk
version: "0.0.0"
fluttertoast:
dependency: "direct main"
dependency: transitive
description:
name: fluttertoast
sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1"
@@ -614,7 +621,7 @@ packages:
source: hosted
version: "2.0.0"
minio:
dependency: "direct main"
dependency: transitive
description:
name: minio
sha256: ee2ce47766e46c7d164f960f2f5ed6a9a82844d877f6b82574f6876ec50c56d1
@@ -790,7 +797,7 @@ packages:
source: hosted
version: "6.1.0"
shared_preferences:
dependency: "direct main"
dependency: transitive
description:
name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
@@ -931,7 +938,7 @@ packages:
source: hosted
version: "1.4.1"
syncfusion_flutter_charts:
dependency: "direct main"
dependency: transitive
description:
name: syncfusion_flutter_charts
sha256: c58ca79e072680af6f0554f7c4b91886c1d8808f2522b49d557f16fb5cb3bb04

View File

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