Compare commits

...

38 Commits

Author SHA1 Message Date
a5b3053920 feat:更新用户表单 2026-07-05 20:41:06 +08:00
780526ddf4 feat:增加用户信息编辑功能 2026-07-05 14:14:43 +08:00
9d3a3f2e80 feat:更新编辑菜谱功能 2026-06-30 23:26:49 +08:00
613e50b0da feat:更新菜谱表单 2026-06-28 23:17:00 +08:00
c10e8e5ffb feat:更新好友菜谱功能 2026-06-28 19:54:32 +08:00
a340ba424e feat:重构模块 2026-06-27 17:12:38 +08:00
a5e788eee6 feat:更新菜谱表单 2026-06-23 20:02:17 +08:00
ed0ceb29d3 feat:增加查询朋友朋友圈功能 2025-11-25 19:16:53 +08:00
8cff5228ae feat:更新个人主页模块 2025-11-24 19:56:13 +08:00
97e7a6f44b feat:增加朋友圈表单功能 2025-11-23 23:19:17 +08:00
245edd9d27 feat:更新主题色适配 2025-11-23 15:27:50 +08:00
6308169013 feat:更新美食记录表单 2025-11-23 00:02:41 +08:00
1ac504f3d7 feat:增加主题色模块 2025-11-21 17:15:40 +08:00
41fe9d6a24 feat:更新朋友圈表单组件 2025-11-20 22:54:44 +08:00
515021781f feat:增加朋友圈评论显示模块 2025-11-20 19:55:44 +08:00
4f033a4d0a feat:增加分享图片功能 2025-11-20 18:43:09 +08:00
a9f2e102c5 feat:调整部分组件UI 2025-11-20 14:50:15 +08:00
eb4f7a3637 factor:布局颜色重构 2025-11-20 14:25:31 +08:00
e6ee934a3e feat:更新Card样式 2025-11-19 22:48:25 +08:00
5151430132 feat:增加朋友圈表单功能 2025-11-19 20:04:45 +08:00
07180965cc feat:更新组件UI 2025-11-19 19:53:05 +08:00
8acad9c63a feat:增加加载中组件 2025-11-19 16:35:20 +08:00
625a872d2f feat:更新图片url路径 2025-11-19 10:40:02 +08:00
7ede1c2d89 feat:更新上传记录表单 2025-11-18 23:18:24 +08:00
b6995d8dd1 feat:增加Minio工具类 2025-11-18 20:03:25 +08:00
9a8e1a7419 feat:更新菜谱显示模块 2025-11-18 15:57:00 +08:00
df26790389 feat:更新TabUI模块 2025-11-18 11:46:21 +08:00
21ff0ad94f feat:重构UI模块 2025-11-18 11:01:53 +08:00
6bba9a488c feat:更新api模块 2025-11-17 22:36:20 +08:00
504ee073ce feat:新增菜谱详细信息模块 2025-07-31 22:55:39 +08:00
b5dd0ab749 feat:增加食谱详细信息模块 2025-07-28 22:57:45 +08:00
8923d9e63a feat:更新统计模块 2025-07-28 16:22:52 +08:00
34780651a8 feat:更新图表模块 2025-07-27 22:28:04 +08:00
a2ed0f3d98 feat:增加轮播图组件 2025-07-24 23:00:37 +08:00
502bb9df52 feat:增加回到顶部功能 2025-07-24 16:50:50 +08:00
7da3ec9507 feat:更新朋友圈模块 2025-07-23 23:00:30 +08:00
d9f1781277 feat:增加朋友圈组件 2025-07-23 20:03:03 +08:00
5d156d3a02 feat:更新记录UI布局 2025-07-22 22:48:49 +08:00
82 changed files with 6363 additions and 5183 deletions

View File

@@ -15,21 +15,6 @@ migration:
- platform: root - platform: root
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: android
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: ios
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: linux
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: macos
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: web
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: windows - platform: windows
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1

View File

@@ -1,6 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:label="food_hub_app" android:label="食光集"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -27,7 +27,7 @@ pluginManagement {
plugins { plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.0" apply false id("com.android.application") version "8.7.0" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false id("org.jetbrains.kotlin.android") version "1.9.22" apply false
} }
include(":app") include(":app")

View File

@@ -1,108 +0,0 @@
import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/utils/http_util.dart';
import 'package:food_hub_app/utils/index.dart';
Future<Recipe> queryRecipeByIdApi(int id) {
return HttpUtil().get<Recipe>(
"/food-service/food/recipe/$id",
converter: (data) => Recipe.fromJson(data),
);
}
Future<bool> addRecipeApi(Recipe recipe) {
return HttpUtil().post<bool>("/food-service/food/food/recipe", data: recipe);
}
Future<bool> updateRecipeApi(int id, Recipe recipe) {
return HttpUtil().put<bool>(
"/food-service/food/food/recipe/$id",
data: recipe,
);
}
Future<bool> deleteRecipeApi(int id) {
return HttpUtil().delete<bool>("/food-service/food/food/recipe/$id");
}
Future<List<Recipe>> queryRecipeByUserApi(int id) {
return HttpUtil().get<List<Recipe>>(
"/food-service/food/recipe/user/$id",
converter: (data) => convertListResponse<Recipe>(data, Recipe.fromJson),
);
}
Future<List<Recipe>> queryRecipeUserFavouriteApi() {
return HttpUtil().get<List<Recipe>>(
"/food-service/food/recipe/user/favourite",
converter: (data) => convertListResponse<Recipe>(data, Recipe.fromJson),
);
}
Future<List<Recipe>> queryRecipeApi(RecipeQuery recipeQuery) {
return HttpUtil().get<List<Recipe>>(
"/food-service/food/recipe",
queryParameters: recipeQuery.toJson(),
converter: (data) => convertListResponse<Recipe>(data, Recipe.fromJson),
);
}
Future<List<String>> queryFoodNameListApi() {
return HttpUtil().get<List<String>>(
"/food-service/food/recipe/name",
converter:
(data) => convertListResponse<String>(data, (json) => json.toString()),
);
}
Future<bool> addRecordApi(Record record) {
return HttpUtil().post<bool>("/food-service/food/record", data: record);
}
Future<bool> updateRecordApi(int id, Record record) {
return HttpUtil().put<bool>("/food-service/food/record/$id", data: record);
}
Future<bool> deleteRecordApi(int id) {
return HttpUtil().delete<bool>("/food-service/food/record/$id");
}
Future<bool> addRecipeCommentApi(int id, String content) {
return HttpUtil().post<bool>(
"/food-service/food/recipe/$id/comment",
data: content,
);
}
Future<bool> addRecipeLikeApi(int id) {
return HttpUtil().post<bool>("/food-service/food/recipe/$id/like");
}
Future<bool> deleteRecipeLikeApi(int id) {
return HttpUtil().delete<bool>("/food-service/food/recipe/$id/like");
}
Future<bool> addRecipeFavouriteApi(int id) {
return HttpUtil().post<bool>("/food-service/food/recipe/$id/favourite");
}
Future<bool> deleteRecipeFavouriteApi(int id) {
return HttpUtil().delete<bool>("/food-service/food/recipe/$id/like");
}
Future<List<Record>> queryRecordApi(String startDate, String endDate) {
return HttpUtil().get<List<Record>>(
"/food-service/food/record",
queryParameters: {
"startDate": startDate,
"endDate": endDate
},
converter: (data) => convertListResponse<Record>(data, Record.fromJson),
);
}
Future<List<String>> queryCategoryApi() {
return HttpUtil().get<List<String>>(
"/food-service/food/category",
converter: (data) => convertListResponse<String>(data, (json) => json.toString()),
);
}

52
lib/apis/moment.dart Normal file
View File

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

103
lib/apis/recipe.dart Normal file
View File

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

View File

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

35
lib/apis/stats.dart Normal file
View File

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

16
lib/apis/user.dart Normal file
View File

@@ -0,0 +1,16 @@
import 'package:flutter_common/utils/http_utils.dart';
import 'package:food_hub_app/config/app_config.dart';
import 'package:food_hub_app/models/session.dart';
final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl);
Future<User> queryUserApi(int number) {
return httpUtil.get<User>(
"/user/$number",
converter: (data) => User.fromJson(data),
);
}
Future<bool> updateUserApi(int id, User user) {
return httpUtil.put<bool>("/user/$id", data: user);
}

View File

@@ -0,0 +1,14 @@
/// 应用信息
class AppConfig {
// 网络配置
// static const String baseApiUrl = "http://192.168.1.4:8083";
static const String baseApiUrl = "https://cxx0822.s.3q.hair/food-api";
static const String rustfsIp = '43.248.188.28';
static const int rustfsPort = 23125;
static const String rustfsAccessKey = 'sZuAg1WCo5i4kD936Tqh';
static const String rustfsSecretKey = 'g2fEyk6bslGrHepBvjhu0OLNF4qCRziPJIx1KAS8';
static const String rustfsFileUrl = 'http://$rustfsIp:$rustfsPort';
static const String bucketName = 'food';
static const String imageBaseUrl = '$rustfsFileUrl/$bucketName/';
// static const String imageBaseUrl = '$baseApiUrl/';
}

105
lib/layout/app_actions.dart Normal file
View File

@@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/provider/food_provider.dart';
import 'package:provider/provider.dart';
class AppActions extends StatefulWidget {
final int pageIndex;
const AppActions({super.key, required this.pageIndex});
@override
State<StatefulWidget> createState() => AppActionsState();
}
class AppActionsState extends State<AppActions> {
Widget _buildBottomSheetBody(FoodProvider provider) {
final colors = Theme.of(context).colorScheme;
final provider = context.watch<FoodProvider>();
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
const Text(
'请选择操作',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
if (widget.pageIndex == 0)
ListTile(
leading: Icon(Icons.book, color: colors.primary),
title: const Text('新增菜谱'),
onTap: () => _handleAddRecipe(provider),
),
if (widget.pageIndex == 0)
ListTile(
leading: Icon(Icons.note_add, color: colors.primary),
title: const Text('新增记录'),
onTap: () => _handleAddRecord(provider),
),
if (widget.pageIndex == 2)
ListTile(
leading: Icon(Icons.group, color: colors.primary),
title: const Text('发布朋友圈'),
onTap: () => _handleAddMoment(provider),
),
],
);
}
void _onPressAdd(FoodProvider provider) {
if (widget.pageIndex == 0) {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => _buildBottomSheetBody(provider),
);
}
if (widget.pageIndex == 2) {
_handleAddMoment(provider);
}
}
void _handleAddRecipe(FoodProvider provider) {
// provider.resetRecordForm();
Navigator.pop(context);
Navigator.pushNamed(context, "/recipeForm");
}
void _handleAddRecord(FoodProvider provider) {
provider.resetRecordForm();
provider.isEditing = false;
Navigator.pop(context);
Navigator.pushNamed(context, "/recordForm");
}
void _handleAddMoment(FoodProvider provider) {
provider.resetMomentForm();
provider.isEditing = false;
Navigator.pushNamed(context, "/momentForm");
}
@override
Widget build(BuildContext context) {
final provider = context.watch<FoodProvider>();
return Row(
children: [
IconButton(
icon: Icon(Icons.search, color: Colors.white),
onPressed: () {
// 搜索功能
},
),
if (widget.pageIndex == 0 || widget.pageIndex == 2)
IconButton(
icon: Icon(Icons.add, color: Colors.white),
onPressed: () => _onPressAdd(provider),
),
],
);
}
}

View File

@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/layout/theme_layout.dart';
class AppDrawer extends StatefulWidget {
const AppDrawer({super.key});
@override
State<StatefulWidget> createState() => AppDrawerState();
}
class AppDrawerState extends State<AppDrawer> {
@override
Widget build(BuildContext context) {
// final themeProvider = Provider.of<ThemeProvider>(context);
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 100),
child: DrawerHeader(
decoration: BoxDecoration(color: Theme.of(context).primaryColor),
child: const Text(
'设置',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
),
ThemeLayout(),
const Divider(),
ListTile(
leading: const Icon(Icons.info),
title: const Text('关于我们'),
onTap: () {
Navigator.pop(context); // 关闭抽屉
// 跳转到关于页面
},
),
],
),
);
}
}

View File

@@ -1,132 +0,0 @@
import 'package:flutter/material.dart';
class NavBar extends StatelessWidget {
final int currentIndex;
final Function(int) onTap;
static const List<BottomNavigationBarItem> navItems = [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: "记录",
),
BottomNavigationBarItem(
icon: Icon(Icons.pie_chart_outline),
activeIcon: Icon(Icons.pie_chart),
label: "统计",
),
BottomNavigationBarItem(
icon: Icon(Icons.group_outlined),
activeIcon: Icon(Icons.group),
label: "朋友圈",
),
BottomNavigationBarItem(
icon: Icon(Icons.account_circle_outlined),
activeIcon: Icon(Icons.account_circle),
label: "我的",
),
];
const NavBar({super.key, required this.currentIndex, required this.onTap});
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
currentIndex: currentIndex,
iconSize: 25,
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.white,
items: navItems,
onTap: onTap,
);
}
}
class SettingsDrawer extends StatelessWidget {
const SettingsDrawer({super.key});
@override
Widget build(BuildContext context) {
// final themeProvider = Provider.of<ThemeProvider>(context);
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 100),
child: DrawerHeader(
decoration: BoxDecoration(color: Theme.of(context).primaryColor),
child: const Text(
'设置',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
),
ListTile(
leading: const Icon(Icons.brightness_4),
title: const Text('夜间模式'),
// trailing: Switch(
// value: themeProvider.isDarkMode,
// onChanged: (bool value) {
// themeProvider.toggleTheme();
// },
// ),
// onTap: () => themeProvider.toggleTheme(),
),
// 其他设置选项
ListTile(
leading: const Icon(Icons.notifications),
title: const Text('通知设置'),
onTap: () {
Navigator.pop(context); // 关闭抽屉
// 跳转到通知设置页面
},
),
ListTile(
leading: const Icon(Icons.language),
title: const Text('语言'),
onTap: () {
Navigator.pop(context); // 关闭抽屉
// 跳转到语言设置页面
},
),
// 底部关于
const Divider(),
ListTile(
leading: const Icon(Icons.info),
title: const Text('关于我们'),
onTap: () {
Navigator.pop(context); // 关闭抽屉
// 跳转到关于页面
},
),
],
),
);
}
}
List<StatelessWidget> homeActions() {
return [
IconButton(
icon: Icon(Icons.search, color: Colors.white),
onPressed: () {
// 搜索功能
},
),
Builder(
builder: (BuildContext context) {
return IconButton(
icon: Icon(Icons.settings, color: Colors.white),
onPressed: () {
Scaffold.of(context).openEndDrawer();
},
);
},
),
];
}

View File

@@ -1,17 +1,48 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_common/provider/theme_provider.dart';
import 'package:flutter_common/utils/log_utils.dart';
import 'package:flutter_common/utils/sp_utils.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:food_hub_app/utils/sp_util.dart'; import 'package:food_hub_app/provider/app_provider.dart';
import 'package:food_hub_app/provider/food_provider.dart';
import 'package:food_hub_app/provider/user_provider.dart';
import 'package:food_hub_app/views/home.dart'; import 'package:food_hub_app/views/home.dart';
import 'package:food_hub_app/views/login.dart'; import 'package:food_hub_app/views/login.dart';
import 'package:food_hub_app/views/recordForm.dart'; import 'package:food_hub_app/views/moment.dart';
import 'package:food_hub_app/views/moment_form.dart';
import 'package:food_hub_app/views/moment_user.dart';
import 'package:food_hub_app/views/profile_form.dart';
import 'package:food_hub_app/views/profile_user.dart';
import 'package:food_hub_app/views/recipe_form.dart';
import 'package:food_hub_app/views/record_form.dart';
import 'package:food_hub_app/views/recipe_detail.dart';
import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:liquid_glass_widgets/liquid_glass_setup.dart';
import 'package:provider/provider.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
FormBuilderLocalizations.delegate.load(const Locale('zh', 'CN')); FormBuilderLocalizations.delegate.load(const Locale('zh', 'CN'));
await SPUtil.init(); await SPUtil.init();
runApp(const MyApp()); await initLogger();
// 预编译 shader防止首帧白闪
await LiquidGlassWidgets.initialize();
// wrap() 安装无障碍桥接、全局主题、自适应质量
runApp(
LiquidGlassWidgets.wrap(
child: MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => FoodProvider()),
ChangeNotifierProvider(create: (context) => ThemeProvider()),
ChangeNotifierProvider(create: (context) => UserProvider()),
ChangeNotifierProvider(create: (context) => AppProvider()),
],
child: const MyApp(),
),
),
);
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
@@ -20,9 +51,53 @@ class MyApp extends StatelessWidget {
// This widget is the root of your application. // This widget is the root of your application.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final themeProvider = context.watch<ThemeProvider>();
final brightness = MediaQuery.of(context).platformBrightness;
themeProvider.isDarkMode = brightness == Brightness.dark;
return MaterialApp( return MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData(fontFamily: 'CustomFont'), theme: ThemeData(
scaffoldBackgroundColor: const Color(0xFFFAF6F0),
colorScheme: ColorScheme(
brightness: Brightness.light,
primary: const Color(0xFFE89F71),
secondary: const Color(0xFFE89F71),
surface: const Color(0xFFFFFFFF),
error: const Color(0xFFE76F51),
onPrimary: Colors.white,
onSecondary: Colors.white,
onSurface: const Color(0xFF3A332C),
onError: Colors.white,
),
textTheme: TextTheme(
titleLarge: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: const Color(0xFF3A332C),
),
titleMedium: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: const Color(0xFF3A332C),
),
bodyLarge: TextStyle(
fontSize: 15,
color: const Color(0xFF3A332C),
),
bodyMedium: TextStyle(
fontSize: 14,
color: const Color(0xFF8C8379),
),
bodySmall: TextStyle(
fontSize: 12,
color: const Color(0xFF8C8379),
),
),
fontFamily: 'CustomFont',
useMaterial3: true,
),
supportedLocales: const [ supportedLocales: const [
Locale('en', 'US'), // 英语 Locale('en', 'US'), // 英语
Locale('zh', 'CN'), // 中文 Locale('zh', 'CN'), // 中文
@@ -45,7 +120,14 @@ class MyApp extends StatelessWidget {
home: LoginPage(), home: LoginPage(),
routes: { routes: {
'/home': (context) => HomePage(), '/home': (context) => HomePage(),
'/recordForm': (context) => RecordFormPage(), '/recordForm': (context) => RecordForm(),
'/recipeForm': (context) => RecipeForm(),
'/recipeDetail': (context) => RecipeDetailPage(),
'/momentForm': (context) => MomentForm(),
'/moment': (context) => MomentPage(),
'/momentUser': (context) => MomentUserPage(),
'/profileUser': (context) => ProfileUserPage(),
'/profileForm': (context) => ProfileForm(),
}, },
); );
} }

15
lib/models/layout.dart Normal file
View File

@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class PageInfo {
final String label;
final IconData icon;
final IconData activeIcon;
final Widget page;
const PageInfo({
required this.label,
required this.icon,
required this.activeIcon,
required this.page,
});
}

82
lib/models/moment.dart Normal file
View File

@@ -0,0 +1,82 @@
import 'package:json_annotation/json_annotation.dart';
part 'moment.g.dart';
@JsonSerializable()
class Comment {
int id;
String username;
String avatar;
String content;
String date;
Comment({
required this.id,
required this.username,
required this.avatar,
required this.content,
required this.date,
});
factory Comment.fromJson(Map<String, dynamic> json) => _$CommentFromJson(json);
Map<String, dynamic> toJson() => _$CommentToJson(this);
}
@JsonSerializable()
class Moment {
int? id;
int? userId;
String? username;
String? avatar;
String content;
List<String> imageList;
String? date;
List<int>? likeList;
List<Comment>? commentList;
Moment({
this.id,
this.userId,
this.username,
this.avatar,
required this.content,
required this.imageList,
this.date,
this.likeList,
this.commentList,
});
factory Moment.fromJson(Map<String, dynamic> json) => _$MomentFromJson(json);
Map<String, dynamic> toJson() => _$MomentToJson(this);
static Moment getEmpty() {
return Moment(
id: 0,
content: '',
imageList: []
);
}
}
@JsonSerializable()
class PageMoment {
final List<Moment> records;
final int total;
final int size;
final int current;
final int pages;
PageMoment({
required this.records,
required this.total,
required this.size,
required this.current,
required this.pages,
});
factory PageMoment.fromJson(Map<String, dynamic> json) => _$PageMomentFromJson(json);
Map<String, dynamic> toJson() => _$PageMomentToJson(this);
}

74
lib/models/moment.g.dart Normal file
View File

@@ -0,0 +1,74 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'moment.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Comment _$CommentFromJson(Map<String, dynamic> json) => Comment(
id: (json['id'] as num).toInt(),
username: json['username'] as String,
avatar: json['avatar'] as String,
content: json['content'] as String,
date: json['date'] as String,
);
Map<String, dynamic> _$CommentToJson(Comment instance) => <String, dynamic>{
'id': instance.id,
'username': instance.username,
'avatar': instance.avatar,
'content': instance.content,
'date': instance.date,
};
Moment _$MomentFromJson(Map<String, dynamic> json) => Moment(
id: (json['id'] as num?)?.toInt(),
userId: (json['userId'] as num?)?.toInt(),
username: json['username'] as String?,
avatar: json['avatar'] as String?,
content: json['content'] as String,
imageList:
(json['imageList'] as List<dynamic>).map((e) => e as String).toList(),
date: json['date'] as String?,
likeList:
(json['likeList'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList(),
commentList:
(json['commentList'] as List<dynamic>?)
?.map((e) => Comment.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$MomentToJson(Moment instance) => <String, dynamic>{
'id': instance.id,
'userId': instance.userId,
'username': instance.username,
'avatar': instance.avatar,
'content': instance.content,
'imageList': instance.imageList,
'date': instance.date,
'likeList': instance.likeList,
'commentList': instance.commentList,
};
PageMoment _$PageMomentFromJson(Map<String, dynamic> json) => PageMoment(
records:
(json['records'] as List<dynamic>)
.map((e) => Moment.fromJson(e as Map<String, dynamic>))
.toList(),
total: (json['total'] as num).toInt(),
size: (json['size'] as num).toInt(),
current: (json['current'] as num).toInt(),
pages: (json['pages'] as num).toInt(),
);
Map<String, dynamic> _$PageMomentToJson(PageMoment instance) =>
<String, dynamic>{
'records': instance.records,
'total': instance.total,
'size': instance.size,
'current': instance.current,
'pages': instance.pages,
};

View File

@@ -4,43 +4,56 @@ part 'recipe.g.dart';
/// 食材信息 /// 食材信息
@JsonSerializable() @JsonSerializable()
class Material { class RecipeMaterial {
int id;
String type; String type;
String name; String name;
String amount; String amount;
Material({required this.type, required this.name, required this.amount}); RecipeMaterial({
required this.id,
required this.type,
required this.name,
required this.amount,
});
factory Material.fromJson(Map<String, dynamic> json) => factory RecipeMaterial.fromJson(Map<String, dynamic> json) =>
_$MaterialFromJson(json); _$RecipeMaterialFromJson(json);
Map<String, dynamic> toJson() => _$MaterialToJson(this); Map<String, dynamic> toJson() => _$RecipeMaterialToJson(this);
} }
/// 步骤信息 /// 步骤信息
@JsonSerializable() @JsonSerializable()
class Step { class RecipeStep {
int id;
int sort; int sort;
String content; String content;
String imageUrl; String imageUrl;
Step({required this.sort, required this.content, required this.imageUrl}); RecipeStep({
required this.id,
required this.sort,
required this.content,
required this.imageUrl,
});
factory Step.fromJson(Map<String, dynamic> json) => _$StepFromJson(json); factory RecipeStep.fromJson(Map<String, dynamic> json) =>
_$RecipeStepFromJson(json);
Map<String, dynamic> toJson() => _$StepToJson(this); Map<String, dynamic> toJson() => _$RecipeStepToJson(this);
} }
/// 评论信息 /// 评论信息
@JsonSerializable() @JsonSerializable()
class Comment { class RecipeComment {
int id; int id;
String username; String username;
String avatar; String avatar;
String content; String content;
String date; String date;
Comment({ RecipeComment({
required this.id, required this.id,
required this.username, required this.username,
required this.avatar, required this.avatar,
@@ -48,15 +61,15 @@ class Comment {
required this.date, required this.date,
}); });
factory Comment.fromJson(Map<String, dynamic> json) => factory RecipeComment.fromJson(Map<String, dynamic> json) =>
_$CommentFromJson(json); _$RecipeCommentFromJson(json);
Map<String, dynamic> toJson() => _$CommentToJson(this); Map<String, dynamic> toJson() => _$RecipeCommentToJson(this);
} }
/// 成果信息 /// 成果信息
@JsonSerializable() @JsonSerializable()
class Record { class FoodRecord {
int? id; int? id;
String name; String name;
String category; String category;
@@ -64,7 +77,7 @@ class Record {
String date; String date;
String imageUrl; String imageUrl;
Record({ FoodRecord({
this.id, this.id,
required this.name, required this.name,
required this.category, required this.category,
@@ -73,20 +86,31 @@ class Record {
required this.imageUrl, required this.imageUrl,
}); });
factory Record.fromJson(Map<String, dynamic> json) => _$RecordFromJson(json); factory FoodRecord.fromJson(Map<String, dynamic> json) =>
_$FoodRecordFromJson(json);
Map<String, dynamic> toJson() => _$RecordToJson(this); Map<String, dynamic> toJson() => _$FoodRecordToJson(this);
static FoodRecord getEmpty() {
return FoodRecord(
id: 0,
name: '',
category: '',
person: 0,
date: '',
imageUrl: '',
);
}
} }
@JsonSerializable() @JsonSerializable()
class RecipeQuery { class RecipeQuery {
String category; String category;
RecipeQuery({ RecipeQuery({required this.category});
required this.category
});
factory RecipeQuery.fromJson(Map<String, dynamic> json) => _$RecipeQueryFromJson(json); factory RecipeQuery.fromJson(Map<String, dynamic> json) =>
_$RecipeQueryFromJson(json);
Map<String, dynamic> toJson() => _$RecipeQueryToJson(this); Map<String, dynamic> toJson() => _$RecipeQueryToJson(this);
} }
@@ -94,49 +118,156 @@ class RecipeQuery {
/// 菜谱信息 /// 菜谱信息
@JsonSerializable() @JsonSerializable()
class Recipe { class Recipe {
int? id; int id;
String name; String name;
String category; String category;
double recommendRate; double recommendRate;
String? remark; String remark;
bool isShare; bool isShare;
int? userId; int userId;
String? username; String username;
String? avatar; List<RecipeMaterial> materialList;
List<Material>? materialList; List<RecipeStep> stepList;
List<Step>? stepList; List<FoodRecord> recordList;
List<Record> recordList; List<int> likeList;
List<int>? likeList; List<int> favouriteList;
int? likeCount; List<RecipeComment> commentList;
List<int>? favouriteList;
int? favouriteCount;
List<Comment>? commentList;
int? commentCount;
Recipe({ Recipe({
this.id, required this.id,
required this.name, required this.name,
required this.category, required this.category,
required this.recommendRate, required this.recommendRate,
this.remark, required this.remark,
required this.isShare, required this.isShare,
this.userId, required this.userId,
this.username, required this.username,
this.avatar, required this.materialList,
this.materialList, required this.stepList,
this.stepList,
required this.recordList, required this.recordList,
this.likeList, required this.likeList,
this.likeCount, required this.favouriteList,
this.favouriteList, required this.commentList,
this.favouriteCount,
this.commentList,
this.commentCount,
}); });
factory Recipe.fromJson(Map<String, dynamic> json) => _$RecipeFromJson(json); factory Recipe.fromJson(Map<String, dynamic> json) => _$RecipeFromJson(json);
Map<String, dynamic> toJson() => _$RecipeToJson(this); Map<String, dynamic> toJson() => _$RecipeToJson(this);
static Recipe getEmpty() {
return Recipe(
id: 0,
name: '',
category: '家常菜',
recommendRate: 3,
remark: '',
isShare: true,
userId: 0,
username: '',
materialList: [],
stepList: [],
recordList: [],
likeList: [],
favouriteList: [],
commentList: []
);
}
}
@JsonSerializable()
class RecipeSummary {
int id;
String name;
String category;
double recommendRate;
bool isShare;
List<FoodRecord> recordList;
int likeCount;
int favouriteCount;
int commentCount;
int userId;
String username;
RecipeSummary({
required this.id,
required this.name,
required this.category,
required this.recommendRate,
required this.isShare,
required this.userId,
required this.username,
required this.recordList,
required this.likeCount,
required this.favouriteCount,
required this.commentCount,
});
factory RecipeSummary.fromJson(Map<String, dynamic> json) =>
_$RecipeSummaryFromJson(json);
Map<String, dynamic> toJson() => _$RecipeSummaryToJson(this);
}
@JsonSerializable()
class RecipeDetail {
int id;
String name;
String category;
double recommendRate;
String remark;
bool isShare;
int userId;
String username;
String avatar;
List<RecipeMaterial> materialList;
List<RecipeStep> stepList;
List<FoodRecord> recordList;
List<int> likeList;
List<int> favouriteList;
List<RecipeComment> commentList;
RecipeDetail({
required this.id,
required this.name,
required this.category,
required this.recommendRate,
required this.remark,
required this.isShare,
required this.userId,
required this.username,
required this.avatar,
required this.materialList,
required this.stepList,
required this.recordList,
required this.likeList,
required this.favouriteList,
required this.commentList,
});
factory RecipeDetail.fromJson(Map<String, dynamic> json) =>
_$RecipeDetailFromJson(json);
Map<String, dynamic> toJson() => _$RecipeDetailToJson(this);
static RecipeDetail getEmpty() {
return RecipeDetail(
id: 0,
name: '',
category: '',
recommendRate: 0,
remark: '',
isShare: false,
userId: 0,
username: '',
avatar: '',
materialList: [],
stepList: [],
recordList: [],
likeList: [],
favouriteList: [],
commentList: [],
);
}
} }
enum ViewType { recipe, calendar, timeline } enum ViewType { recipe, calendar, timeline }

View File

@@ -6,47 +6,56 @@ part of 'recipe.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
Material _$MaterialFromJson(Map<String, dynamic> json) => Material( RecipeMaterial _$RecipeMaterialFromJson(Map<String, dynamic> json) =>
type: json['type'] as String, RecipeMaterial(
name: json['name'] as String, id: (json['id'] as num).toInt(),
amount: json['amount'] as String, type: json['type'] as String,
); name: json['name'] as String,
amount: json['amount'] as String,
);
Map<String, dynamic> _$MaterialToJson(Material instance) => <String, dynamic>{ Map<String, dynamic> _$RecipeMaterialToJson(RecipeMaterial instance) =>
'type': instance.type, <String, dynamic>{
'name': instance.name, 'id': instance.id,
'amount': instance.amount, 'type': instance.type,
}; 'name': instance.name,
'amount': instance.amount,
};
Step _$StepFromJson(Map<String, dynamic> json) => Step( RecipeStep _$RecipeStepFromJson(Map<String, dynamic> json) => RecipeStep(
id: (json['id'] as num).toInt(),
sort: (json['sort'] as num).toInt(), sort: (json['sort'] as num).toInt(),
content: json['content'] as String, content: json['content'] as String,
imageUrl: json['imageUrl'] as String, imageUrl: json['imageUrl'] as String,
); );
Map<String, dynamic> _$StepToJson(Step instance) => <String, dynamic>{ Map<String, dynamic> _$RecipeStepToJson(RecipeStep instance) =>
'sort': instance.sort, <String, dynamic>{
'content': instance.content, 'id': instance.id,
'imageUrl': instance.imageUrl, 'sort': instance.sort,
}; 'content': instance.content,
'imageUrl': instance.imageUrl,
};
Comment _$CommentFromJson(Map<String, dynamic> json) => Comment( RecipeComment _$RecipeCommentFromJson(Map<String, dynamic> json) =>
id: (json['id'] as num).toInt(), RecipeComment(
username: json['username'] as String, id: (json['id'] as num).toInt(),
avatar: json['avatar'] as String, username: json['username'] as String,
content: json['content'] as String, avatar: json['avatar'] as String,
date: json['date'] as String, content: json['content'] as String,
); date: json['date'] as String,
);
Map<String, dynamic> _$CommentToJson(Comment instance) => <String, dynamic>{ Map<String, dynamic> _$RecipeCommentToJson(RecipeComment instance) =>
'id': instance.id, <String, dynamic>{
'username': instance.username, 'id': instance.id,
'avatar': instance.avatar, 'username': instance.username,
'content': instance.content, 'avatar': instance.avatar,
'date': instance.date, 'content': instance.content,
}; 'date': instance.date,
};
Record _$RecordFromJson(Map<String, dynamic> json) => Record( FoodRecord _$FoodRecordFromJson(Map<String, dynamic> json) => FoodRecord(
id: (json['id'] as num?)?.toInt(), id: (json['id'] as num?)?.toInt(),
name: json['name'] as String, name: json['name'] as String,
category: json['category'] as String, category: json['category'] as String,
@@ -55,14 +64,15 @@ Record _$RecordFromJson(Map<String, dynamic> json) => Record(
imageUrl: json['imageUrl'] as String, imageUrl: json['imageUrl'] as String,
); );
Map<String, dynamic> _$RecordToJson(Record instance) => <String, dynamic>{ Map<String, dynamic> _$FoodRecordToJson(FoodRecord instance) =>
'id': instance.id, <String, dynamic>{
'name': instance.name, 'id': instance.id,
'category': instance.category, 'name': instance.name,
'person': instance.person, 'category': instance.category,
'date': instance.date, 'person': instance.person,
'imageUrl': instance.imageUrl, 'date': instance.date,
}; 'imageUrl': instance.imageUrl,
};
RecipeQuery _$RecipeQueryFromJson(Map<String, dynamic> json) => RecipeQuery _$RecipeQueryFromJson(Map<String, dynamic> json) =>
RecipeQuery(category: json['category'] as String); RecipeQuery(category: json['category'] as String);
@@ -71,42 +81,38 @@ Map<String, dynamic> _$RecipeQueryToJson(RecipeQuery instance) =>
<String, dynamic>{'category': instance.category}; <String, dynamic>{'category': instance.category};
Recipe _$RecipeFromJson(Map<String, dynamic> json) => Recipe( Recipe _$RecipeFromJson(Map<String, dynamic> json) => Recipe(
id: (json['id'] as num?)?.toInt(), id: (json['id'] as num).toInt(),
name: json['name'] as String, name: json['name'] as String,
category: json['category'] as String, category: json['category'] as String,
recommendRate: (json['recommendRate'] as num).toDouble(), recommendRate: (json['recommendRate'] as num).toDouble(),
remark: json['remark'] as String?, remark: json['remark'] as String,
isShare: json['isShare'] as bool, isShare: json['isShare'] as bool,
userId: (json['userId'] as num?)?.toInt(), userId: (json['userId'] as num).toInt(),
username: json['username'] as String?, username: json['username'] as String,
avatar: json['avatar'] as String?,
materialList: materialList:
(json['materialList'] as List<dynamic>?) (json['materialList'] as List<dynamic>)
?.map((e) => Material.fromJson(e as Map<String, dynamic>)) .map((e) => RecipeMaterial.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
stepList: stepList:
(json['stepList'] as List<dynamic>?) (json['stepList'] as List<dynamic>)
?.map((e) => Step.fromJson(e as Map<String, dynamic>)) .map((e) => RecipeStep.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
recordList: recordList:
(json['recordList'] as List<dynamic>) (json['recordList'] as List<dynamic>)
.map((e) => Record.fromJson(e as Map<String, dynamic>)) .map((e) => FoodRecord.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
likeList: likeList:
(json['likeList'] as List<dynamic>?) (json['likeList'] as List<dynamic>)
?.map((e) => (e as num).toInt()) .map((e) => (e as num).toInt())
.toList(), .toList(),
likeCount: (json['likeCount'] as num?)?.toInt(),
favouriteList: favouriteList:
(json['favouriteList'] as List<dynamic>?) (json['favouriteList'] as List<dynamic>)
?.map((e) => (e as num).toInt()) .map((e) => (e as num).toInt())
.toList(), .toList(),
favouriteCount: (json['favouriteCount'] as num?)?.toInt(),
commentList: commentList:
(json['commentList'] as List<dynamic>?) (json['commentList'] as List<dynamic>)
?.map((e) => Comment.fromJson(e as Map<String, dynamic>)) .map((e) => RecipeComment.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
commentCount: (json['commentCount'] as num?)?.toInt(),
); );
Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{ Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{
@@ -118,14 +124,98 @@ Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{
'isShare': instance.isShare, 'isShare': instance.isShare,
'userId': instance.userId, 'userId': instance.userId,
'username': instance.username, 'username': instance.username,
'avatar': instance.avatar,
'materialList': instance.materialList, 'materialList': instance.materialList,
'stepList': instance.stepList, 'stepList': instance.stepList,
'recordList': instance.recordList, 'recordList': instance.recordList,
'likeList': instance.likeList, 'likeList': instance.likeList,
'likeCount': instance.likeCount,
'favouriteList': instance.favouriteList, 'favouriteList': instance.favouriteList,
'favouriteCount': instance.favouriteCount,
'commentList': instance.commentList, 'commentList': instance.commentList,
'commentCount': instance.commentCount,
}; };
RecipeSummary _$RecipeSummaryFromJson(Map<String, dynamic> json) =>
RecipeSummary(
id: (json['id'] as num).toInt(),
name: json['name'] as String,
category: json['category'] as String,
recommendRate: (json['recommendRate'] as num).toDouble(),
isShare: json['isShare'] as bool,
userId: (json['userId'] as num).toInt(),
username: json['username'] as String,
recordList:
(json['recordList'] as List<dynamic>)
.map((e) => FoodRecord.fromJson(e as Map<String, dynamic>))
.toList(),
likeCount: (json['likeCount'] as num).toInt(),
favouriteCount: (json['favouriteCount'] as num).toInt(),
commentCount: (json['commentCount'] as num).toInt(),
);
Map<String, dynamic> _$RecipeSummaryToJson(RecipeSummary instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'category': instance.category,
'recommendRate': instance.recommendRate,
'isShare': instance.isShare,
'recordList': instance.recordList,
'likeCount': instance.likeCount,
'favouriteCount': instance.favouriteCount,
'commentCount': instance.commentCount,
'userId': instance.userId,
'username': instance.username,
};
RecipeDetail _$RecipeDetailFromJson(Map<String, dynamic> json) => RecipeDetail(
id: (json['id'] as num).toInt(),
name: json['name'] as String,
category: json['category'] as String,
recommendRate: (json['recommendRate'] as num).toDouble(),
remark: json['remark'] as String,
isShare: json['isShare'] as bool,
userId: (json['userId'] as num).toInt(),
username: json['username'] as String,
avatar: json['avatar'] as String,
materialList:
(json['materialList'] as List<dynamic>)
.map((e) => RecipeMaterial.fromJson(e as Map<String, dynamic>))
.toList(),
stepList:
(json['stepList'] as List<dynamic>)
.map((e) => RecipeStep.fromJson(e as Map<String, dynamic>))
.toList(),
recordList:
(json['recordList'] as List<dynamic>)
.map((e) => FoodRecord.fromJson(e as Map<String, dynamic>))
.toList(),
likeList:
(json['likeList'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(),
favouriteList:
(json['favouriteList'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(),
commentList:
(json['commentList'] as List<dynamic>)
.map((e) => RecipeComment.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$RecipeDetailToJson(RecipeDetail instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'category': instance.category,
'recommendRate': instance.recommendRate,
'remark': instance.remark,
'isShare': instance.isShare,
'userId': instance.userId,
'username': instance.username,
'avatar': instance.avatar,
'materialList': instance.materialList,
'stepList': instance.stepList,
'recordList': instance.recordList,
'likeList': instance.likeList,
'favouriteList': instance.favouriteList,
'commentList': instance.commentList,
};

View File

@@ -30,14 +30,16 @@ class SaTokenInfo {
this.tag, this.tag,
}); });
factory SaTokenInfo.fromJson(Map<String, dynamic> json) => _$SaTokenInfoFromJson(json); factory SaTokenInfo.fromJson(Map<String, dynamic> json) =>
_$SaTokenInfoFromJson(json);
Map<String, dynamic> toJson() => _$SaTokenInfoToJson(this); Map<String, dynamic> toJson() => _$SaTokenInfoToJson(this);
} }
@JsonSerializable() @JsonSerializable()
class User { class User {
int? id; int? id;
String? username; String username;
int? gender; int? gender;
String? phoneNumber; String? phoneNumber;
String? email; String? email;
@@ -51,7 +53,7 @@ class User {
User({ User({
this.id, this.id,
this.username, required this.username,
this.gender, this.gender,
this.phoneNumber, this.phoneNumber,
this.email, this.email,
@@ -65,7 +67,24 @@ class User {
}); });
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json); factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this); Map<String, dynamic> toJson() => _$UserToJson(this);
static User getEmpty() {
return User(
username: '',
gender: 0,
phoneNumber: '',
email: '',
birthDate: null,
avatar: '',
area: [],
address: '',
job: '',
tags: [],
description: '',
);
}
} }
@JsonSerializable() @JsonSerializable()
@@ -75,6 +94,8 @@ class Session {
Session({required this.saToken, required this.userInfo}); Session({required this.saToken, required this.userInfo});
factory Session.fromJson(Map<String, dynamic> json) => _$SessionFromJson(json); factory Session.fromJson(Map<String, dynamic> json) =>
_$SessionFromJson(json);
Map<String, dynamic> toJson() => _$SessionToJson(this); Map<String, dynamic> toJson() => _$SessionToJson(this);
} }

View File

@@ -37,7 +37,7 @@ Map<String, dynamic> _$SaTokenInfoToJson(SaTokenInfo instance) =>
User _$UserFromJson(Map<String, dynamic> json) => User( User _$UserFromJson(Map<String, dynamic> json) => User(
id: (json['id'] as num?)?.toInt(), id: (json['id'] as num?)?.toInt(),
username: json['username'] as String?, username: json['username'] as String,
gender: (json['gender'] as num?)?.toInt(), gender: (json['gender'] as num?)?.toInt(),
phoneNumber: json['phoneNumber'] as String?, phoneNumber: json['phoneNumber'] as String?,
email: json['email'] as String?, email: json['email'] as String?,

21
lib/models/stats.dart Normal file
View File

@@ -0,0 +1,21 @@
import 'package:json_annotation/json_annotation.dart';
part 'stats.g.dart';
@JsonSerializable()
class SummaryStats {
final int recipeCount;
final int categoryCount;
final int workCount;
const SummaryStats({
required this.recipeCount,
required this.categoryCount,
required this.workCount,
});
factory SummaryStats.fromJson(Map<String, dynamic> json) =>
_$SummaryStatsFromJson(json);
Map<String, dynamic> toJson() => _$SummaryStatsToJson(this);
}

20
lib/models/stats.g.dart Normal file
View File

@@ -0,0 +1,20 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'stats.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SummaryStats _$SummaryStatsFromJson(Map<String, dynamic> json) => SummaryStats(
recipeCount: (json['recipeCount'] as num).toInt(),
categoryCount: (json['categoryCount'] as num).toInt(),
workCount: (json['workCount'] as num).toInt(),
);
Map<String, dynamic> _$SummaryStatsToJson(SummaryStats instance) =>
<String, dynamic>{
'recipeCount': instance.recipeCount,
'categoryCount': instance.categoryCount,
'workCount': instance.workCount,
};

View File

@@ -0,0 +1,8 @@
import 'package:flutter/material.dart';
class AppProvider with ChangeNotifier {
String appVersion = '1.0.0';
String buildNumber = '1';
String get fullVersion => '$appVersion+$buildNumber';
}

View File

@@ -0,0 +1,517 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/models/common_model.dart';
import 'package:food_hub_app/apis/moment.dart';
import 'package:food_hub_app/apis/recipe.dart';
import 'package:food_hub_app/apis/stats.dart';
import 'package:food_hub_app/models/moment.dart';
import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/models/stats.dart';
import 'package:food_hub_app/utils/date_util.dart';
import 'package:food_hub_app/utils/index.dart';
import 'package:food_hub_app/views/record.dart';
class FoodProvider with ChangeNotifier {
late RecordTab currentRecordTab = RecordTab.recipe;
late FoodRecord recordFormItem;
late Recipe recipeFormItem;
late Moment momentFormItem;
late bool isEditing;
bool isLoading = false;
String? error;
String queryCategory = '全部菜系';
late List<String> categoryList = [];
late List<RecipeSummary> recipeSummaryList = [];
int selectYear = DateTime.now().year;
DateTime selectedDay = DateTime.now();
DateTime focusedDay = DateTime.now();
late List<FoodRecord> recordList = [];
late List<FoodRecord> selectRecordList = [];
late SummaryStats summaryStats = SummaryStats(
recipeCount: 0,
categoryCount: 0,
workCount: 0,
);
late double averageRecordCount = 0;
late List<ChartData> recordStats = [];
late List<ChartData> categoryStats = [];
late List<ChartData> rankStats = [];
List<Moment> momentList = [];
int currentPage = 1;
final int pageSize = 5;
bool hasMore = true;
void resetRecordForm() {
recordFormItem = FoodRecord.getEmpty();
notifyListeners();
}
void initRecordForm(FoodRecord record) {
recordFormItem = record;
notifyListeners();
}
void resetRecipeForm() {
recipeFormItem = Recipe.getEmpty();
notifyListeners();
}
void initRecipeForm(Recipe recipe) {
recipeFormItem = recipe;
notifyListeners();
}
void updateRecipeFormItem({
String? name,
String? category,
double? recommendRate,
String? remark,
bool? isShare,
}) {
if (name != null) recipeFormItem.name = name;
if (category != null) recipeFormItem.category = category;
if (recommendRate != null) recipeFormItem.recommendRate = recommendRate;
if (remark != null) recipeFormItem.remark = remark;
if (isShare != null) recipeFormItem.isShare = isShare;
notifyListeners();
}
void addRecipeMaterial() {
recipeFormItem.materialList.add(
RecipeMaterial(
id: DateTime.now().microsecondsSinceEpoch,
type: '主料',
name: '',
amount: '',
),
);
notifyListeners();
}
void updateRecipeMaterial(
int index, {
String? type,
String? name,
String? amount,
}) {
final material = recipeFormItem.materialList[index];
recipeFormItem.materialList[index] = RecipeMaterial(
id: material.id,
type: type ?? material.type,
name: name ?? material.name,
amount: amount ?? material.amount,
);
notifyListeners();
}
void removeRecipeMaterial(int index) {
recipeFormItem.materialList.removeAt(index);
notifyListeners();
}
void addRecipeStep() {
recipeFormItem.stepList.add(
RecipeStep(
id: DateTime.now().microsecondsSinceEpoch,
sort: recipeFormItem.stepList.length,
content: '',
imageUrl: '',
),
);
notifyListeners();
}
void updateRecipeStep(int index, {String? content, String? imageUrl}) {
final step = recipeFormItem.stepList[index];
recipeFormItem.stepList[index] = RecipeStep(
id: step.id,
sort: step.sort,
content: content ?? step.content,
imageUrl: imageUrl ?? step.imageUrl,
);
}
void removeRecipeStep(int index) {
recipeFormItem.stepList.removeAt(index);
for (int i = 0; i < recipeFormItem.stepList.length; i++) {
recipeFormItem.stepList[i].sort = i;
}
notifyListeners();
}
void updateRecordFormItem({String? name, String? date, String? imageUrl}) {
if (name != null) recordFormItem.name = name;
if (date != null) recordFormItem.date = date;
if (imageUrl != null) recordFormItem.imageUrl = imageUrl;
notifyListeners();
}
void updateMomentFormItem({String? content}) {
if (content != null) momentFormItem.content = content;
notifyListeners();
}
void addMomentFormImage(List<String> imageUrls) {
momentFormItem.imageList.addAll(imageUrls);
notifyListeners();
}
void removeMomentFormImage(int index) {
momentFormItem.imageList.removeAt(index);
notifyListeners();
}
void resetMomentForm() {
momentFormItem = Moment.getEmpty();
notifyListeners();
}
void initMomentForm(Moment moment) {
momentFormItem = moment;
notifyListeners();
}
Future<void> onRecordTabChange() async {
switch (currentRecordTab) {
case RecordTab.recipe:
await refreshRecipeList();
break;
case RecordTab.calendar:
case RecordTab.timeline:
await refreshRecordList();
break;
}
}
Future<void> queryCategoryList() async {
try {
error = null;
notifyListeners();
final result = await queryCategoryApi();
categoryList.clear();
categoryList.add('全部菜系');
categoryList.addAll(result);
} catch (e) {
error = '加载数据失败: $e';
debugPrint('加载数据失败: $e');
} finally {
notifyListeners();
}
}
Future<void> queryRecipeByUserId(int userId) async {
if (isLoading) return;
try {
isLoading = true;
error = null;
notifyListeners();
final result = await queryRecipeByUserApi(userId);
recipeSummaryList = result;
notifyListeners();
} catch (e) {
error = '加载数据失败: $e';
debugPrint('加载数据失败: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
Future<void> refreshRecipeList() async {
if (isLoading) return;
try {
isLoading = true;
error = null;
notifyListeners();
final category = queryCategory == '全部菜系' ? '' : queryCategory;
final result = await queryRecipeApi(RecipeQuery(category: category));
recipeSummaryList = result;
} catch (e) {
error = '加载数据失败: $e';
debugPrint('加载数据失败: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
Future<bool> handleRecipe() async {
if (isLoading) return true;
try {
isLoading = true;
error = null;
notifyListeners();
if (isEditing) {
await updateRecipeApi(recipeFormItem.id!, recipeFormItem);
} else {
await addRecipeApi(recipeFormItem);
}
return true;
} catch (e) {
error = '处理数据失败: $e';
debugPrint('处理数据失败: $e');
return false;
} finally {
isLoading = false;
notifyListeners();
}
}
Future<void> refreshRecordList() async {
if (isLoading) return;
if (currentRecordTab == RecordTab.recipe) return;
try {
isLoading = true;
error = null;
notifyListeners();
late String startDate;
late String endDate;
switch (currentRecordTab) {
case RecordTab.calendar:
startDate = getFirstDayOfMonth(focusedDay);
endDate = getLastDayOfMonth(focusedDay);
break;
case RecordTab.timeline:
startDate = '$selectYear-01-01';
endDate = '$selectYear-12-31';
break;
case RecordTab.recipe:
break;
}
final result = await queryRecordApi(startDate, endDate);
recordList = result;
if (recordList.isNotEmpty) {
selectedDay = DateTime.parse(recordList.last.date);
} else {
selectedDay = focusedDay;
}
refreshSelectRecord();
} catch (e) {
error = '加载数据失败: $e';
debugPrint('加载数据失败: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
Future<bool> handleRecord() async {
if (isLoading) return true;
try {
isLoading = true;
error = null;
notifyListeners();
if (isEditing) {
await updateRecordApi(recordFormItem.id!, recordFormItem);
} else {
await addRecordApi(recordFormItem);
}
return true;
} catch (e) {
error = '处理数据失败: $e';
debugPrint('处理数据失败: $e');
return false;
} finally {
isLoading = false;
notifyListeners();
}
}
Future<bool> deleteRecord() async {
if (isLoading) return true;
try {
isLoading = true;
error = null;
notifyListeners();
await deleteRecordApi(recordFormItem.id!);
return true;
} catch (e) {
error = '处理数据失败: $e';
debugPrint('处理数据失败: $e');
return false;
} finally {
isLoading = false;
notifyListeners();
}
}
void refreshSelectRecord() {
selectRecordList =
recordList
.where((record) => record.date == formatDateTime(selectedDay))
.toList();
}
Future<void> refreshFoodStats() async {
if (isLoading) return;
try {
isLoading = true;
error = null;
notifyListeners();
final statsResult = await queryStatsApi();
final recordStatsResult = await queryRecordStatsApi();
final categoryStatsResult = await queryCategoryStatsApi();
final rankStatsResult = await queryRankStatsApi();
summaryStats = statsResult;
recordStats = recordStatsResult;
categoryStats = categoryStatsResult;
rankStats = rankStatsResult.take(15).toList();
if (recordStats.isNotEmpty) {
double sumValue = recordStats.fold(
0.0,
(sum, item) => sum + item.value,
);
averageRecordCount = sumValue / recordStats.length;
}
} catch (e) {
error = '加载数据失败: $e';
debugPrint('加载数据失败: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
Future<void> queryMomentByPage({bool isRefresh = true}) async {
if (isLoading) return;
try {
if (currentPage == 1) {
isLoading = true;
}
error = null;
notifyListeners();
// 如果是刷新,重置页码
if (isRefresh) {
currentPage = 1;
}
final result = await queryMomentByPageApi(currentPage, pageSize);
// 更新数据
if (isRefresh) {
momentList = result.records;
} else {
momentList.addAll(result.records);
}
// 判断是否还有更多数据
hasMore = result.current < result.pages;
if (hasMore) {
currentPage++;
}
} catch (e) {
error = '加载数据失败: $e';
debugPrint('加载数据失败: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
Future<void> queryMomentByUserId(int userId) async {
if (isLoading) return;
try {
isLoading = true;
error = null;
notifyListeners();
final result = await queryMomentListByUserIdApi(userId);
momentList = result;
notifyListeners();
} catch (e) {
error = '加载数据失败: $e';
debugPrint('加载数据失败: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
Future<void> loadMoreMomentList() async {
if (hasMore && !isLoading) {
await queryMomentByPage(isRefresh: false);
}
}
Future<void> refreshMomentList() async {
await queryMomentByPage(isRefresh: true);
}
Future<bool> handleMoment() async {
if (isLoading) return true;
try {
isLoading = true;
error = null;
notifyListeners();
if (isEditing) {
await updateMomentApi(momentFormItem.id!, momentFormItem);
} else {
await addMomentApi(momentFormItem);
}
return true;
} catch (e) {
error = '处理数据失败: $e';
debugPrint('处理数据失败: $e');
return false;
} finally {
isLoading = false;
notifyListeners();
}
}
Future<bool> deleteMoment() async {
if (isLoading) return true;
try {
isLoading = true;
error = null;
notifyListeners();
await deleteMomentApi(momentFormItem.id!);
return true;
} catch (e) {
error = '处理数据失败: $e';
debugPrint('处理数据失败: $e');
return false;
} finally {
isLoading = false;
notifyListeners();
}
}
}

View File

@@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/apis/moment.dart';
import 'package:food_hub_app/apis/user.dart';
import 'package:food_hub_app/models/moment.dart';
import 'package:food_hub_app/models/session.dart';
class UserProvider with ChangeNotifier {
late User currentUser = User.getEmpty();
bool isLoading = false;
String? error = '';
List<Moment> momentList = [];
late User userFormItem;
Future<void> refreshUser(int id) async {
if (isLoading) return;
try {
isLoading = true;
error = '';
notifyListeners();
final result = await queryUserApi(id);
currentUser = result;
} catch (e) {
error = '加载数据失败: $e';
debugPrint('加载数据失败: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
Future<void> queryMomentByUserId(int userId) async {
if (isLoading) return;
try {
isLoading = true;
error = '';
notifyListeners();
final result = await queryMomentListByUserIdApi(userId);
momentList = result;
notifyListeners();
} catch (e) {
error = '加载数据失败: $e';
debugPrint('加载数据失败: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
void initUserForm(User user) {
userFormItem = user;
notifyListeners();
}
void updateUserFormItem({
String? username,
int? gender,
String? phoneNumber,
String? email,
DateTime? birthDate,
String? avatar,
List<String>? area,
String? address,
String? job,
List<String>? tags,
String? description
}) {
if (username != null) userFormItem.username = username;
if (gender != null) userFormItem.gender = gender;
if (phoneNumber != null) userFormItem.phoneNumber = phoneNumber;
if (email != null) userFormItem.email = email;
if (birthDate != null) userFormItem.birthDate = birthDate;
if (avatar != null) userFormItem.avatar = avatar;
if (area != null) userFormItem.area = area;
if (address != null) userFormItem.address = address;
if (job != null) userFormItem.job = job;
if (tags != null) userFormItem.tags = tags;
if (description != null) userFormItem.description = description;
notifyListeners();
}
Future<bool> handleUser() async {
if (isLoading) return true;
try {
isLoading = true;
error = null;
notifyListeners();
await updateUserApi(userFormItem.id!, userFormItem);
return true;
} catch (e) {
error = '处理数据失败: $e';
debugPrint('处理数据失败: $e');
return false;
} finally {
isLoading = false;
notifyListeners();
}
}
}

View File

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

View File

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

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

@@ -0,0 +1,43 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter_common/utils/file_utils.dart';
import 'package:food_hub_app/config/app_config.dart';
import 'package:minio/io.dart';
import 'package:minio/minio.dart';
class MinIOHelper {
static final MinIOHelper _instance = MinIOHelper._internal();
factory MinIOHelper() => _instance;
final String ip = AppConfig.rustfsIp;
MinIOHelper._internal() {
_minio = Minio(
endPoint: AppConfig.rustfsIp,
port: AppConfig.rustfsPort,
accessKey: AppConfig.rustfsAccessKey,
secretKey: AppConfig.rustfsSecretKey,
useSSL: false,
);
}
late Minio _minio;
Future<String> uploadFile({
required PlatformFile file,
required String bucketName,
Function(double)? onProgress,
}) async {
try {
String hashName = await generateMD5HashName(file.path!);
String fileName = '$hashName${getFileExtension(file.name)}';
await _minio.fPutObject(bucketName, fileName, file.path!);
return fileName;
} catch (e) {
throw Exception('文件上传失败: $e');
}
}
}

View File

@@ -0,0 +1,124 @@
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_common/utils/toast_util.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
/// 截图工具类
class ScreenshotUtil {
static double pixelRatio = 3.0;
static Duration delay = const Duration(milliseconds: 50);
static String watermarkText = '来自 食光集';
// 截图功能
static Future<Uint8List?> captureImage({required GlobalKey globalKey}) async {
// 等待一帧确保UI已渲染
await Future.delayed(delay);
final RenderObject? render = globalKey.currentContext!.findRenderObject();
final RenderRepaintBoundary boundary = render as RenderRepaintBoundary;
final ui.Image image = await boundary.toImage(pixelRatio: pixelRatio);
// 添加水印
return await _addWatermarkToImage(image);
}
// 添加水印到图片
static Future<Uint8List> _addWatermarkToImage(ui.Image image) async {
// 创建画布
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
// 绘制原始图片
canvas.drawImage(image, Offset.zero, Paint());
// 添加文字水印
_drawTextWatermark(canvas, image);
// 生成最终图片
final picture = recorder.endRecording();
final watermarkedImage = await picture.toImage(image.width, image.height);
final ByteData? byteData = await watermarkedImage.toByteData(
format: ui.ImageByteFormat.png,
);
return byteData!.buffer.asUint8List();
}
// 绘制文字水印
static void _drawTextWatermark(Canvas canvas, ui.Image image) {
final textPainter = TextPainter(
text: TextSpan(
text: watermarkText,
style: TextStyle(
color: Color(0x99FFFFFF),
fontSize: 28,
fontFamily: 'CustomFont',
fontWeight: FontWeight.w600,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout(maxWidth: image.width.toDouble());
// 计算水印位置(右下角,带边距)
final double x = image.width - textPainter.width - 40;
final double y = image.height - textPainter.height - 40;
// 绘制文字背景
final backgroundPaint =
Paint()
..color = const ui.Color(0x4D000000)
..style = PaintingStyle.fill;
// 绘制圆角矩形背景
final backgroundRect = RRect.fromRectAndRadius(
Rect.fromLTWH(
x - 12,
y - 6,
textPainter.width + 24,
textPainter.height + 12,
),
Radius.circular(6),
);
canvas.drawRRect(backgroundRect, backgroundPaint);
// 绘制文字
textPainter.paint(canvas, Offset(x, y));
}
// 保存图片到临时文件
static Future<File> saveImageToFile(Uint8List bytes, String fileName) async {
final directory = await getTemporaryDirectory();
final File imageFile = File('${directory.path}/$fileName');
await imageFile.writeAsBytes(bytes);
return imageFile;
}
static Future<void> captureAndShare({
required GlobalKey globalKey,
required String fileName,
required String shareText,
}) async {
try {
final imageBytes = await captureImage(globalKey: globalKey);
if (imageBytes != null) {
final imageFile = await saveImageToFile(imageBytes, fileName);
await SharePlus.instance.share(
ShareParams(files: [XFile(imageFile.path)], text: shareText),
);
// 分享后删除临时文件
await imageFile.delete();
} else {
ToastUtil.error('生成图片失败,请重试');
}
} catch (e) {
ToastUtil.error('分享失败: $e');
}
}
}

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();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:food_hub_app/layout/index.dart';
import 'package:food_hub_app/views/moment.dart';
import 'package:food_hub_app/views/profile.dart'; import 'package:food_hub_app/views/profile.dart';
import 'package:food_hub_app/views/record.dart'; import 'package:food_hub_app/views/record.dart';
import 'package:food_hub_app/views/stats.dart'; import 'package:food_hub_app/views/stats.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
import 'package:liquid_glass_widgets/widgets/shared/glass_page.dart';
import 'package:liquid_glass_widgets/widgets/surfaces/glass_app_bar.dart';
import 'package:liquid_glass_widgets/widgets/surfaces/glass_bottom_bar.dart';
import 'package:liquid_glass_widgets/widgets/surfaces/glass_scaffold.dart';
import 'moment.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
const HomePage({super.key}); const HomePage({super.key});
@@ -15,59 +20,52 @@ class HomePage extends StatefulWidget {
class _HomePage extends State<HomePage> { class _HomePage extends State<HomePage> {
int _currentIndex = 0; int _currentIndex = 0;
final List<Widget> _tabPages = const [ final _pages = [
RecordPage(), const RecordPage(),
StatsPage(), const StatsPage(),
MomentPage(), const MomentPage(),
ProfilePage(), const ProfilePage(),
]; ];
bool isShow(int index) {
return index == 0 || index == 2;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( final colors = Theme.of(context).colorScheme;
appBar: AppBar(
centerTitle: true, return GlassScaffold(
title: Text('Food Hub', style: TextStyle(color: Colors.white)), statusBarStyle: GlassStatusBarStyle.auto,
backgroundColor: Theme.of(context).primaryColor, bottomBar: GlassBottomBar(
leading: IconButton( settings: LiquidGlassSettings(
icon: Icon(Icons.menu, color: Colors.white), glassColor: colors.surface
onPressed: () {
// 打开侧边栏或菜单
},
), ),
actions: homeActions(), selectedIndex: _currentIndex,
), onTabSelected: (i) => setState(() => _currentIndex = i),
backgroundColor: Color(0xFFF5F5F5), indicatorColor: colors.primary,
endDrawer: const SettingsDrawer(), selectedIconColor: colors.surface,
body: _tabPages[_currentIndex], unselectedIconColor: Color(0xFF000000),
floatingActionButton: tabs: const [
isShow(_currentIndex) ? addItemButton(context) : null, GlassBottomBarTab(
bottomNavigationBar: NavBar( icon: Icon(Icons.home_outlined),
currentIndex: _currentIndex, activeIcon: Icon(Icons.home),
onTap: (index) { label: '记录',
setState(() { ),
_currentIndex = index; GlassBottomBarTab(
}); icon: Icon(Icons.pie_chart_outline),
}, activeIcon: Icon(Icons.pie_chart),
label: '统计',
),
GlassBottomBarTab(
icon: Icon(Icons.group_outlined),
activeIcon: Icon(Icons.group),
label: '朋友圈',
),
GlassBottomBarTab(
icon: Icon(Icons.account_circle_outlined),
activeIcon: Icon(Icons.account_circle),
label: '我的',
),
],
), ),
body: _pages[_currentIndex],
); );
} }
} }
Widget addItemButton(BuildContext context) {
void onAddItemClick(BuildContext context) {
Navigator.pushNamed(context, '/recordForm');
}
return FloatingActionButton(
mini: true,
onPressed: () => onAddItemClick(context),
backgroundColor: Theme.of(context).primaryColor,
shape: const CircleBorder(),
child: Icon(Icons.add, color: Colors.white),
);
}

View File

@@ -1,11 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_common/utils/sp_utils.dart';
import 'package:flutter_common/utils/toast_util.dart';
import 'package:flutter_common/widget/loading_widget.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:food_hub_app/api/session.dart'; import 'package:food_hub_app/apis/session.dart';
import 'package:food_hub_app/models/session.dart'; import 'package:food_hub_app/models/session.dart';
import 'package:food_hub_app/utils/sp_util.dart'; import 'package:food_hub_app/widgets/common/form.dart';
import 'package:food_hub_app/widgets/common/index.dart'; import 'package:food_hub_app/widgets/common/index.dart';
import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
class LoginPage extends StatefulWidget { class LoginPage extends StatefulWidget {
const LoginPage({super.key}); const LoginPage({super.key});
@@ -20,6 +22,7 @@ class _LoginPage extends State<LoginPage> {
String _username = "", _password = ""; String _username = "", _password = "";
bool _isRemember = false; bool _isRemember = false;
bool _isObscure = true; bool _isObscure = true;
bool _isLoading = false;
Color _eyeColor = Colors.grey; Color _eyeColor = Colors.grey;
final List _loginMethod = [ final List _loginMethod = [
{"title": "phone", "icon": Icons.phone_android}, {"title": "phone", "icon": Icons.phone_android},
@@ -34,13 +37,22 @@ class _LoginPage extends State<LoginPage> {
} }
Future<void> _loadRememberState() async { Future<void> _loadRememberState() async {
bool? isRemember = await SPUtil.getBool('isRemember'); bool? isRemember = SPUtil.getBool('isRemember');
if (isRemember) { if (isRemember == true) {
setState(() { final username = SPUtil.getString('username');
_isRemember = true; final password = SPUtil.getString('password');
_username = SPUtil.getString('username');
_password = SPUtil.getString('password'); if (username.isNotEmpty && password.isNotEmpty) {
}); setState(() {
_isRemember = true;
_username = username;
_password = password;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
loginClick();
});
}
} }
} }
@@ -60,39 +72,53 @@ class _LoginPage extends State<LoginPage> {
SPUtil.set('token', token); SPUtil.set('token', token);
} }
void loginClick(BuildContext context) async { void handleUserInfo(User user) {
SPUtil.set('userId', user.id);
}
void loginClick() async {
setState(() {
_isLoading = true;
});
// 表单校验通过才会继续执行 // 表单校验通过才会继续执行
if ((_formKey.currentState as FormState).validate()) { if ((_formKey.currentState as FormState).validate()) {
(_formKey.currentState as FormState).save(); (_formKey.currentState as FormState).save();
Session session = await loginApi(_username, _password);
showSuccessToast('登录成功');
handleRememberState(); try {
handleTokenState(session.saToken.tokenValue); LoadingDialog.show(context, message: '登录中');
Navigator.pushNamed(context, '/home'); Session session = await loginApi(_username, _password);
} else { ToastUtil.success('登录成功');
showErrorToast('请先输入信息'); handleRememberState();
handleTokenState(session.saToken.tokenValue);
handleUserInfo(session.userInfo);
LoadingDialog.hide(context);
Navigator.pushNamed(context, '/home');
} catch (e) {
LoadingDialog.hide(context);
ToastUtil.error(e.toString());
}
} }
setState(() {
_isLoading = false;
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Colors.grey[100],
body: Form( body: Form(
key: _formKey, key: _formKey,
// autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column( child: Column(
children: [ children: [
// 可滚动的主要内容区域 Expanded(child: buildLoginForm(context)),
Expanded(child: buildLoginForm()),
// 固定在底部的其他登录选项区域
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 50), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 50),
child: Column( child: Column(
children: [ children: [
buildOtherLoginText(), buildOtherLoginText(context),
const SizedBox(height: 15), const SizedBox(height: 15),
buildOtherMethod(context), buildOtherMethod(context),
const SizedBox(height: 20), const SizedBox(height: 20),
@@ -114,42 +140,36 @@ class _LoginPage extends State<LoginPage> {
); );
} }
Widget buildLoginForm() { Widget buildLoginForm(BuildContext context) {
return ListView( return ListView(
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
children: [ children: [
const SizedBox(height: kToolbarHeight), const SizedBox(height: kToolbarHeight),
buildTitle(), buildTitle(),
const SizedBox(height: 60), const SizedBox(height: 60),
buildUsernameTextField(), buildUsernameTextField(context),
const SizedBox(height: 20), const SizedBox(height: 20),
buildPasswordTextField(context), buildPasswordTextField(context),
Row( const SizedBox(height: 10),
mainAxisAlignment: MainAxisAlignment.spaceBetween, buildRememberPasswordCheckbox(context),
crossAxisAlignment: CrossAxisAlignment.center, const SizedBox(height: 20),
children: [
buildRememberPasswordCheckbox(context), // 记住密码(左对齐)
buildForgetPasswordText(context),
],
),
const SizedBox(height: 20), // 调整与登录按钮的间距
buildLoginButton(context), buildLoginButton(context),
], ],
); );
} }
Widget buildUsernameTextField() { Widget buildUsernameTextField(BuildContext context) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
formLabelText(labelText: "账号:"), Text("账号:", style: Theme.of(context).textTheme.bodyLarge),
const SizedBox(height: 10), const SizedBox(height: 10),
FormBuilderTextField( FormBuilderTextField(
name: 'username', name: 'username',
initialValue: _username, initialValue: _username,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
decoration: formInputDecoration( decoration: buildInputDecoration(
context: context,
hintText: "请输入账号", hintText: "请输入账号",
prefixIcon: Icons.person, prefixIcon: Icons.person,
), ),
@@ -164,7 +184,7 @@ class _LoginPage extends State<LoginPage> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
formLabelText(labelText: "密码:"), Text("密码:", style: Theme.of(context).textTheme.bodyLarge),
const SizedBox(height: 10), const SizedBox(height: 10),
FormBuilderTextField( FormBuilderTextField(
name: 'password', name: 'password',
@@ -173,22 +193,18 @@ class _LoginPage extends State<LoginPage> {
obscureText: _isObscure, obscureText: _isObscure,
onSaved: (v) => _password = v!, onSaved: (v) => _password = v!,
validator: FormBuilderValidators.required(), validator: FormBuilderValidators.required(),
decoration: InputDecoration( decoration: buildInputDecoration(
prefixIcon: Icon(Icons.lock), context: context,
hintText: "请输入密码", hintText: "请输入密码",
hintStyle: TextStyle(color: Colors.grey), prefixIcon: Icons.lock,
border: OutlineInputBorder(), ).copyWith(
filled: true,
fillColor: Colors.white,
suffixIcon: IconButton( suffixIcon: IconButton(
icon: Icon(Icons.remove_red_eye, color: _eyeColor), icon: Icon(Icons.remove_red_eye, color: _eyeColor),
onPressed: () { onPressed: () {
final colors = Theme.of(context).colorScheme;
setState(() { setState(() {
_isObscure = !_isObscure; _isObscure = !_isObscure;
_eyeColor = _eyeColor = (_isObscure ? Colors.grey : colors.surface);
(_isObscure
? Colors.grey
: Theme.of(context).iconTheme.color)!;
}); });
}, },
), ),
@@ -202,6 +218,8 @@ class _LoginPage extends State<LoginPage> {
return Row( return Row(
children: [ children: [
Checkbox( Checkbox(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
value: _isRemember, value: _isRemember,
onChanged: (bool? value) { onChanged: (bool? value) {
setState(() { setState(() {
@@ -209,48 +227,45 @@ class _LoginPage extends State<LoginPage> {
}); });
}, },
), ),
const Text('记住密码', style: TextStyle(color: Colors.grey, fontSize: 14)), Text('记住密码', style: Theme.of(context).textTheme.bodyMedium),
], ],
); );
} }
Widget buildForgetPasswordText(BuildContext context) {
return Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
print("忘记密码");
},
child: const Text(
"忘记密码?",
style: TextStyle(fontSize: 14, color: Colors.grey),
),
),
);
}
Widget buildLoginButton(BuildContext context) { Widget buildLoginButton(BuildContext context) {
return Align( return SizedBox(
child: SizedBox( height: 45,
height: 45, width: double.infinity,
width: double.infinity, child: ElevatedButton(
child: TDButton( onPressed: _isLoading ? null : loginClick,
text: '登录', style: ElevatedButton.styleFrom(
size: TDButtonSize.large, backgroundColor: Theme.of(context).colorScheme.primary,
type: TDButtonType.fill, foregroundColor: Colors.white,
shape: TDButtonShape.rectangle, elevation: 0,
theme: TDButtonTheme.primary, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
onTap: () => loginClick(context), ),
child: const Text(
'登录',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
), ),
), ),
); );
} }
Widget buildOtherLoginText() { Widget buildOtherLoginText(BuildContext context) {
return TDDivider(text: '其他方式登录', alignment: TextAlignment.center); return Row(
children: [
Expanded(child: Divider(color: Colors.grey[300], thickness: 1)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text('其他方式登录', style: Theme.of(context).textTheme.bodyMedium),
),
Expanded(child: Divider(color: Colors.grey[300], thickness: 1)),
],
);
} }
Widget buildOtherMethod(context) { Widget buildOtherMethod(BuildContext context) {
return OverflowBar( return OverflowBar(
alignment: MainAxisAlignment.center, alignment: MainAxisAlignment.center,
children: children:
@@ -283,16 +298,19 @@ class _LoginPage extends State<LoginPage> {
); );
} }
Widget buildRegisterText(context) { Widget buildRegisterText(BuildContext context) {
return Center( return Center(
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Text('没有账号?', style: TextStyle(fontSize: 18)), Text('没有账号?', style: Theme.of(context).textTheme.bodyMedium),
GestureDetector( GestureDetector(
child: const Text( child: Text(
'点击注册', '点击注册',
style: TextStyle(fontSize: 18, color: Colors.green), style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.primary,
),
), ),
onTap: () { onTap: () {
print("点击注册"); print("点击注册");

View File

@@ -1,15 +1,137 @@
import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:food_hub_app/provider/food_provider.dart';
import 'package:food_hub_app/widgets/common/easy_refresh.dart';
import 'package:food_hub_app/widgets/moment/card.dart';
import 'package:provider/provider.dart';
class MomentPage extends StatefulWidget { class MomentPage extends StatefulWidget {
const MomentPage({super.key}); const MomentPage({super.key});
@override @override
State<MomentPage> createState() => _MomentPage(); State<MomentPage> createState() => _MomentPageState();
} }
class _MomentPage extends State<MomentPage>{ class _MomentPageState extends State<MomentPage> {
// 初始化EasyRefresh控制器
final EasyRefreshController _freshController = EasyRefreshController(
controlFinishRefresh: true,
controlFinishLoad: true,
);
final ScrollController _scrollController = ScrollController();
bool _showScrollToTop = false;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
// 初始化加载数据
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<FoodProvider>().refreshMomentList();
});
}
@override
void dispose() {
_scrollController.removeListener(_onScroll);
_freshController.dispose();
_scrollController.dispose();
super.dispose();
}
// 下拉刷新
Future<void> _onRefresh() async {
await context.read<FoodProvider>().refreshMomentList();
_freshController.finishRefresh();
}
// 上拉加载
Future<void> _onLoad() async {
final provider = context.read<FoodProvider>();
if (provider.hasMore) {
await provider.loadMoreMomentList();
_freshController.finishLoad(IndicatorResult.success);
} else {
_freshController.finishLoad(IndicatorResult.noMore);
}
}
void _onScroll() {
// 当滚动距离超过时显示返回顶部按钮
if (_scrollController.offset > 100) {
if (!_showScrollToTop) {
setState(() {
_showScrollToTop = true;
});
}
} else {
if (_showScrollToTop) {
setState(() {
_showScrollToTop = false;
});
}
}
}
// 滚动到顶部
void _scrollToTop() => scrollToTopAnimateTo(_scrollController);
Widget _buildMomentList(FoodProvider provider) {
return ListView.separated(
padding: const EdgeInsets.only(bottom: 80),
controller: _scrollController,
itemCount: provider.momentList.length,
separatorBuilder: (context, index) => SizedBox(height: 8),
itemBuilder: (context, index) {
return MomentCard(moment: provider.momentList[index], isUser: false);
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Center(child: Text("朋友圈")); final provider = context.watch<FoodProvider>();
// 空状态显示
if (provider.momentList.isEmpty) {
return buildEmptyData();
}
// 有数据时显示列表
return Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
title: Text('朋友圈', style: Theme.of(context).textTheme.titleLarge),
centerTitle: true,
automaticallyImplyLeading: false,
),
body: Padding(
padding: EdgeInsetsGeometry.all(10),
child: Stack(
children: [
buildEasyRefresh(
freshController: _freshController,
onRefresh: _onRefresh,
onLoad: _onLoad,
body: Stack(
children: [
if (provider.isLoading)
buildLoadingIndicator()
else
_buildMomentList(provider),
],
),
),
// 返回顶部按钮
if (_showScrollToTop)
buildScrollToTop(context: context, scrollToTop: _scrollToTop),
],
),
),
);
} }
} }

227
lib/views/moment_form.dart Normal file
View File

@@ -0,0 +1,227 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/utils/log_utils.dart';
import 'package:flutter_common/utils/toast_util.dart';
import 'package:flutter_common/widget/dialog_widget.dart';
import 'package:flutter_common/widget/loading_widget.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:food_hub_app/config/app_config.dart';
import 'package:food_hub_app/provider/food_provider.dart';
import 'package:food_hub_app/utils/minio_utils.dart';
import 'package:food_hub_app/widgets/common/form.dart';
import 'package:food_hub_app/widgets/common/image.dart';
import 'package:provider/provider.dart';
class MomentForm extends StatefulWidget {
const MomentForm({super.key});
@override
State<MomentForm> createState() => _MomentFormState();
}
class _MomentFormState extends State<MomentForm> {
final _formKey = GlobalKey<FormBuilderState>();
final _focusNode = FocusNode();
static const String _contentField = 'content';
static const int maxContentLength = 200;
static const int maxImageCount = 9; // 最大图片数量
Widget _buildContentField(FoodProvider provider) {
final colors = Theme.of(context).colorScheme;
return FormBuilderTextField(
name: _contentField,
initialValue: provider.momentFormItem.content,
focusNode: _focusNode,
maxLines: 8,
minLines: 5,
maxLength: maxContentLength,
buildCounter: (
context, {
required currentLength,
required isFocused,
maxLength,
}) {
return Text(
'$currentLength/$maxContentLength',
style: TextStyle(
color: currentLength > maxLength! ? Colors.red : colors.primary,
),
);
},
onChanged: (value) {
provider.updateMomentFormItem(content: value);
},
decoration: buildInputDecoration(context: context, hintText: '请输入朋友圈内容'),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入朋友圈内容';
}
if (value.length > maxContentLength) {
return '内容不能超过$maxContentLength字';
}
return null;
},
);
}
Widget _buildImageField(FoodProvider provider) {
final imageUrls = provider.momentFormItem.imageList;
final totalItems =
imageUrls.length + (imageUrls.length < maxImageCount ? 1 : 0);
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 4 / 3,
),
itemCount: totalItems,
itemBuilder: (context, index) {
if (index < imageUrls.length) {
return ImagePreview(
imageUrls: imageUrls,
index: index,
onRemoveImage: () => _removeImage(provider, index),
);
}
// 上传按钮(最后一个)
else {
return buildImageUploadButton(
context: context,
onTap: () => _pickImages(provider),
);
}
},
);
}
Widget _buildFormBuilder(FoodProvider provider) {
return FormBuilder(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildFormLabel(context: context, text: '朋友圈内容', isRequired: true),
const SizedBox(height: 8),
_buildContentField(provider),
const SizedBox(height: 8),
buildFormLabel(
context: context,
text: '上传图片最多9张',
isRequired: false,
),
const SizedBox(height: 8),
_buildImageField(provider),
const SizedBox(height: 8),
buildFormButtonGroup(
context: context,
isShowDelete: provider.isEditing,
onConfirm: () => _submitForm(provider),
onDelete: () {},
),
],
),
);
}
/// 选择多张图片
Future<void> _pickImages(FoodProvider provider) async {
final currentCount = provider.momentFormItem.imageList.length;
final remainingCount = maxImageCount - currentCount;
if (remainingCount <= 0) {
ToastUtil.error('最多只能上传$maxImageCount张图片');
return;
}
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['jpg', 'jpeg', 'png'],
allowMultiple: true,
);
if (result != null) {
// 限制选择数量
final filesToUpload = result.files.take(remainingCount).toList();
LoadingDialog.show(context, message: '上传中');
try {
final newImageUrls = <String>[];
// 逐个上传图片
for (final file in filesToUpload) {
final fileName = await MinIOHelper().uploadFile(
bucketName: AppConfig.bucketName,
file: file,
);
logger.i('图片名称:$fileName');
newImageUrls.add(fileName);
}
// 更新图片列表
provider.addMomentFormImage(newImageUrls);
} catch (e) {
ToastUtil.error(e.toString());
} finally {
LoadingDialog.hide(context);
}
}
}
/// 移除单张图片
void _removeImage(FoodProvider provider, int index) {
provider.removeMomentFormImage(index);
}
/// 提交表单
void _submitForm(FoodProvider provider) async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
LoadingDialog.show(context, message: '提交中');
final result = await provider.handleMoment();
if (result == true) {
await provider.refreshMomentList();
LoadingDialog.hide(context);
if (provider.isEditing) {
showSuccessTip(context, '更新朋友圈成功');
} else {
showSuccessTip(context, '新增朋友圈成功');
}
Future.delayed(Duration(milliseconds: 1500), () {
if (mounted) {
Navigator.pop(context);
}
});
} else {
LoadingDialog.hide(context);
if (provider.isEditing) {
showErrorTip(context, '更新朋友圈失败');
} else {
showErrorTip(context, '新增朋友圈失败');
}
}
}
}
@override
Widget build(BuildContext context) {
final provider = context.watch<FoodProvider>();
return AnimatedPadding(
duration: const Duration(milliseconds: 200),
padding: MediaQuery.of(context).viewInsets,
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(16),
child: _buildFormBuilder(provider),
),
),
);
}
}

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/utils/sp_utils.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:food_hub_app/provider/food_provider.dart';
import 'package:food_hub_app/widgets/moment/list.dart';
import 'package:provider/provider.dart';
class MomentUserPage extends StatefulWidget {
const MomentUserPage({super.key});
@override
State<MomentUserPage> createState() => _MomentUserPageState();
}
class _MomentUserPageState extends State<MomentUserPage> {
@override
void initState() {
super.initState();
// 初始化加载数据
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<FoodProvider>().queryMomentByUserId(SPUtil.getInt('userId'));
});
}
@override
Widget build(BuildContext context) {
final provider = context.watch<FoodProvider>();
// 空状态显示
if (provider.momentList.isEmpty) {
return buildEmptyData();
}
return Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
title: Text('我的朋友圈', style: Theme.of(context).textTheme.titleLarge),
centerTitle: true,
automaticallyImplyLeading: false,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
),
body:
provider.isLoading
? buildLoadingIndicator()
: Padding(
padding: const EdgeInsets.all(10),
child: MomentList(
momentList: provider.momentList,
isUser: true,
),
),
);
}
}

View File

@@ -1,15 +1,182 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_common/utils/sp_utils.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:flutter_common/widget/dialog_widget.dart';
import 'package:food_hub_app/provider/app_provider.dart';
import 'package:food_hub_app/provider/user_provider.dart';
import 'package:food_hub_app/views/profile_form.dart';
import 'package:food_hub_app/widgets/profile/basic_info.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
import 'package:liquid_glass_widgets/widgets/containers/glass_card.dart';
import 'package:provider/provider.dart';
class ProfilePage extends StatefulWidget { class ProfilePage extends StatefulWidget {
const ProfilePage({super.key}); const ProfilePage({super.key});
@override @override
State<ProfilePage> createState() => _ProfilePage(); State<ProfilePage> createState() => ProfilePageState();
} }
class _ProfilePage extends State<ProfilePage>{ class ProfilePageState extends State<ProfilePage> {
@override
void initState() {
super.initState();
// 初始化加载数据
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<UserProvider>().refreshUser(SPUtil.getInt('userId'));
});
}
Widget _buildFunctionButton({
required IconData icon,
required String text,
required VoidCallback onTap,
}) {
final colors = Theme.of(context).colorScheme;
return ListTile(
contentPadding: EdgeInsets.zero,
leading: Icon(icon, color: colors.primary),
title: Text(text),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: colors.primary),
onTap: onTap,
);
}
void _onTapInfo(UserProvider provider) {
provider.initUserForm(provider.currentUser);
Navigator.pushNamed(context, '/profileForm');
// showModalBottomSheet(
// context: context,
// backgroundColor: Theme.of(context).scaffoldBackgroundColor,
// isScrollControlled: true,
// shape: const RoundedRectangleBorder(
// borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
// ),
// builder: (context) => ProfileForm(),
// );
}
void _onTapMoment() {
Navigator.pushNamed(context, '/momentUser');
}
Widget _buildFunctionButtons(BuildContext context) {
final provider = context.watch<UserProvider>();
final colors = Theme.of(context).colorScheme;
return GlassCard(
padding: EdgeInsetsGeometry.symmetric(vertical: 0, horizontal: 10),
useOwnLayer: true,
settings: LiquidGlassSettings(glassColor: colors.surface),
child: Column(
children: [
_buildFunctionButton(
icon: Icons.account_circle,
text: '基本信息',
onTap: () => _onTapInfo(provider),
),
const Divider(height: 1, thickness: 0.2),
_buildFunctionButton(
icon: Icons.group_outlined,
text: '朋友圈',
onTap: _onTapMoment,
),
const Divider(height: 1, thickness: 0.2),
_buildFunctionButton(
icon: Icons.favorite,
text: '我的收藏',
onTap: () {
// 跳转到浏览历史页面
// LogUtils.i('点击浏览历史');
},
),
const Divider(height: 1, thickness: 0.2),
_buildFunctionButton(
icon: Icons.lock,
text: '更改密码',
onTap: () {
// 跳转到帮助与反馈页面
// LogUtils.i('点击帮助与反馈');
},
),
const Divider(height: 1, thickness: 0.2),
_buildFunctionButton(icon: Icons.message, text: '反馈意见', onTap: () {}),
const Divider(height: 1, thickness: 0.2),
_buildFunctionButton(icon: Icons.refresh, text: '检查更新', onTap: () {}),
const Divider(height: 1, thickness: 0.2),
_buildFunctionButton(
icon: Icons.exit_to_app,
text: '退出登录',
onTap: _onTapLogout,
),
],
),
);
}
void _onTapLogout() async {
final result = await showConfirmDialog(context, '确认要退出吗?');
if (result == true) {
SPUtil.remove('token');
Navigator.pop(context);
Navigator.pushNamed(context, '/login');
}
}
Widget _buildVersionInfo() {
final provider = context.read<AppProvider>();
return Text(
'版本 v${provider.fullVersion}',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.primary,
),
);
}
Widget _buildContent(UserProvider provider, BuildContext context) {
final user = provider.currentUser;
return Column(
children: [
SizedBox(width: double.infinity, child: ProfileInfo(user: user)),
const SizedBox(height: 16),
_buildFunctionButtons(context),
const SizedBox(height: 8),
_buildVersionInfo(),
],
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Center(child: Text("个人主页")); final provider = context.watch<UserProvider>();
return Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
title: Text('我的', style: Theme.of(context).textTheme.titleLarge),
centerTitle: true,
automaticallyImplyLeading: false,
),
body: Padding(
padding: EdgeInsetsGeometry.all(10),
child: Stack(
children: [
if (provider.isLoading)
buildLoadingIndicator()
else
SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 90),
child: _buildContent(provider, context),
),
],
),
),
);
} }
} }

254
lib/views/profile_form.dart Normal file
View File

@@ -0,0 +1,254 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/dialog_widget.dart';
import 'package:flutter_common/widget/loading_widget.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_input_chips/flutter_input_chips.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:food_hub_app/provider/user_provider.dart';
import 'package:food_hub_app/widgets/common/form.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class ProfileForm extends StatefulWidget {
const ProfileForm({super.key});
@override
State<ProfileForm> createState() => _ProfileFormState();
}
class _ProfileFormState extends State<ProfileForm> {
final _formKey = GlobalKey<FormBuilderState>();
@override
void initState() {
super.initState();
}
Widget _buildFormBuilder(UserProvider provider) {
final double sizeBoxHeight = 8;
return FormBuilder(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildFormLabel(context: context, text: '用户名', isRequired: true),
SizedBox(height: sizeBoxHeight),
FormBuilderTextField(
name: 'username',
initialValue: provider.userFormItem.username,
onChanged: (value) {
provider.updateUserFormItem(username: value);
},
decoration: buildInputDecoration(
context: context,
hintText: '请输入用户名',
),
validator: FormBuilderValidators.required(errorText: '请输入用户名'),
),
SizedBox(height: sizeBoxHeight),
buildFormLabel(context: context, text: '性别', isRequired: true),
SizedBox(height: sizeBoxHeight),
FormBuilderDropdown<int>(
name: 'gender',
initialValue: provider.userFormItem.gender,
decoration: buildInputDecoration(
context: context,
hintText: '请选择性别',
),
items: const [
DropdownMenuItem(value: 1, child: Text('')),
DropdownMenuItem(value: 2, child: Text('')),
],
onChanged: (value) {
provider.updateUserFormItem(gender: value);
},
),
SizedBox(height: sizeBoxHeight),
buildFormLabel(context: context, text: '手机号', isRequired: true),
SizedBox(height: sizeBoxHeight),
FormBuilderTextField(
name: 'phoneNumber',
initialValue: provider.userFormItem.phoneNumber,
keyboardType: TextInputType.phone,
decoration: buildInputDecoration(
context: context,
hintText: '请输入手机号',
),
onChanged: (value) {
provider.updateUserFormItem(phoneNumber: value);
},
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(errorText: '请输入手机号'),
FormBuilderValidators.match(
RegExp(r'^1[3-9]\d{9}$'),
errorText: '请输入正确的11位手机号',
),
]),
),
SizedBox(height: sizeBoxHeight),
buildFormLabel(context: context, text: '邮箱', isRequired: true),
SizedBox(height: sizeBoxHeight),
FormBuilderTextField(
name: 'email',
initialValue: provider.userFormItem.email,
keyboardType: TextInputType.emailAddress,
decoration: buildInputDecoration(
context: context,
hintText: '请输入邮箱',
),
onChanged: (value) {
provider.updateUserFormItem(email: value);
},
validator: FormBuilderValidators.compose([
FormBuilderValidators.email(errorText: '邮箱格式不正确'),
]),
),
SizedBox(height: sizeBoxHeight),
buildFormLabel(context: context, text: '出生日期', isRequired: true),
SizedBox(height: sizeBoxHeight),
FormBuilderDateTimePicker(
name: 'birthDate',
initialValue: provider.userFormItem.birthDate,
inputType: InputType.date,
format: DateFormat('yyyy-MM-dd'),
firstDate: DateTime(1900),
lastDate: DateTime.now(),
decoration: buildInputDecoration(
context: context,
hintText: '请选择出生日期',
),
onChanged: (value) {
provider.updateUserFormItem(birthDate: value);
},
),
SizedBox(height: sizeBoxHeight),
buildFormLabel(context: context, text: '详细地址', isRequired: false),
SizedBox(height: sizeBoxHeight),
FormBuilderTextField(
name: 'address',
initialValue: provider.userFormItem.address,
decoration: buildInputDecoration(
context: context,
hintText: '请输入详细地址',
),
onChanged: (value) {
provider.updateUserFormItem(address: value);
},
),
SizedBox(height: sizeBoxHeight),
buildFormLabel(context: context, text: '职业', isRequired: false),
SizedBox(height: sizeBoxHeight),
FormBuilderTextField(
name: 'job',
initialValue: provider.userFormItem.job,
decoration: buildInputDecoration(
context: context,
hintText: '请输入职业',
),
onChanged: (value) {
provider.updateUserFormItem(job: value);
},
),
SizedBox(height: sizeBoxHeight),
buildFormLabel(context: context, text: '标签', isRequired: false),
SizedBox(height: sizeBoxHeight),
FormBuilderField<List<String>>(
name: 'tags',
initialValue: provider.userFormItem.tags,
builder: (FormFieldState<List<String>> field) {
return FlutterInputChips(
padding: EdgeInsets.all(0),
initialValue: field.value ?? [],
onChanged: (value) {
provider.updateUserFormItem(tags: value);
},
inputDecoration: buildInputDecoration(
context: context,
hintText: '请输入标签,多个标签用回车键隔开',
),
);
},
),
SizedBox(height: sizeBoxHeight),
buildFormLabel(context: context, text: '简介', isRequired: false),
SizedBox(height: sizeBoxHeight),
FormBuilderTextField(
name: 'description',
initialValue: provider.userFormItem.description,
decoration: buildInputDecoration(
context: context,
hintText: '请输入简介',
),
onChanged: (value) {
provider.updateUserFormItem(description: value);
},
),
SizedBox(height: sizeBoxHeight),
buildFormButtonGroup(
context: context,
isShowDelete: false,
onConfirm: () => _submitForm(provider),
onDelete: () => {},
),
],
),
);
}
/// 提交表单
void _submitForm(UserProvider provider) async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
LoadingDialog.show(context, message: '提交中');
final result = await provider.handleUser();
if (result == true) {
LoadingDialog.hide(context);
showSuccessTip(context, '更新信息成功');
Future.delayed(Duration(milliseconds: 1500), () {
if (mounted) {
Navigator.pop(context);
}
});
} else {
LoadingDialog.hide(context);
showErrorTip(context, '更新信息失败');
}
}
}
@override
Widget build(BuildContext context) {
final provider = context.watch<UserProvider>();
final colors = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: Text('基本信息', style: Theme.of(context).textTheme.titleLarge),
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: colors.primary),
onPressed: () => Navigator.pop(context),
),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: _buildFormBuilder(provider),
),
),
);
}
}

109
lib/views/profile_user.dart Normal file
View File

@@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:food_hub_app/provider/user_provider.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:food_hub_app/widgets/moment/list.dart';
import 'package:food_hub_app/widgets/profile/basic_info.dart';
import 'package:food_hub_app/widgets/recipe/list.dart';
import 'package:provider/provider.dart';
enum ProfileTab {
profile('基础信息'),
moment('朋友圈'),
recipe('菜谱');
final String label;
const ProfileTab(this.label);
}
class ProfileUserPage extends StatefulWidget {
const ProfileUserPage({super.key});
@override
State<ProfileUserPage> createState() => ProfileUserPageState();
}
class ProfileUserPageState extends State<ProfileUserPage> {
late ProfileTab currentTab = ProfileTab.profile;
void _onTabChange(ProfileTab tab) {
setState(() {
currentTab = tab;
});
// provider.onRecordTabChange();
}
Widget _buildTabs({
required BuildContext context,
required ProfileTab currentTab,
required ValueChanged<ProfileTab> onTabChanged,
}) {
return Container(
padding: EdgeInsets.all(8),
child: buildToggleSwitch(
context: context,
currentTab: currentTab,
tabValues: ProfileTab.values,
labels: ProfileTab.values.map((e) => e.label).toList(),
onTabChanged: onTabChanged,
),
);
}
Widget _buildContent(UserProvider provider) {
return Column(
children: [
_buildTabs(
context: context,
currentTab: currentTab,
onTabChanged: (tab) => _onTabChange(tab),
),
Expanded(
child: IndexedStack(
index: currentTab.index,
children: [
SizedBox(
width: double.infinity, child: ProfileInfo(user: provider.currentUser)
),
MomentList(momentList: provider.momentList, isUser: false),
RecipeList(userId: provider.currentUser.id!),
],
),
),
],
);
}
@override
Widget build(BuildContext context) {
final provider = context.watch<UserProvider>();
final colors = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: Text(
'${provider.currentUser.username}的个人信息',
style: Theme.of(context).textTheme.titleLarge,
),
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: colors.primary),
onPressed: () => Navigator.pop(context),
),
),
body: Padding(
padding: const EdgeInsets.all(10),
child: Stack(
children: [
if (provider.isLoading)
buildLoadingIndicator()
else
_buildContent(provider),
],
),
),
);
}
}

View File

@@ -0,0 +1,291 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:food_hub_app/apis/recipe.dart';
import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/provider/food_provider.dart';
import 'package:food_hub_app/utils/screenshot_util.dart';
import 'package:food_hub_app/views/recipe_form.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:provider/provider.dart';
class RecipeDetailPage extends StatefulWidget {
const RecipeDetailPage({super.key});
@override
State<RecipeDetailPage> createState() => _RecipeDetailState();
}
class _RecipeDetailState extends State<RecipeDetailPage> {
Recipe recipe = Recipe.getEmpty();
final GlobalKey _recipeDetailKey = GlobalKey();
bool _isSharing = false;
List<RecipeMaterial> materialList = [];
@override
void didChangeDependencies() {
super.didChangeDependencies();
refreshRecipeDetail();
}
Future<void> refreshRecipeDetail() async {
// 获取参数
final args = ModalRoute.of(context)?.settings.arguments as Map;
final result = await queryRecipeByIdApi(args['id']);
setState(() {
recipe = result;
getRecipeMaterial();
});
}
void getRecipeMaterial() {
materialList.clear();
for (var material in recipe.materialList) {
materialList.add(material);
}
}
void _handleEditRecipe(FoodProvider provider) {
provider.initRecipeForm(recipe);
provider.isEditing = true;
showModalBottomSheet(
context: context,
constraints: BoxConstraints(minHeight: 600),
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => RecipeForm(),
);
}
// 分享图片
Future<void> _shareRecipeAsImage() async {
if (_isSharing) return;
setState(() {
_isSharing = true;
});
final timestamp = DateTime.now().millisecondsSinceEpoch;
final imageName = '${recipe.name}_$timestamp.png';
await ScreenshotUtil.captureAndShare(
globalKey: _recipeDetailKey,
fileName: imageName,
shareText: '分享菜谱: ${recipe.name}',
);
setState(() {
_isSharing = false;
});
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final provider = context.watch<FoodProvider>();
return Scaffold(
appBar: AppBar(
title: Text('菜谱信息', style: Theme.of(context).textTheme.titleLarge),
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: colors.primary),
onPressed: () => Navigator.pop(context),
),
),
body: SingleChildScrollView(
child: Column(
children: [
// 用RepaintBoundary包裹要截图的部分
RepaintBoundary(
key: _recipeDetailKey,
child: Padding(
padding: EdgeInsets.all(10),
child: _buildRecipeDetail(),
),
),
_buildButtons(provider),
],
),
),
);
}
Widget _buildRecipeDetail() {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildTitleSection(),
SizedBox(height: 8),
_buildInfoCard(
context,
icon: Icons.info,
title: "基础信息",
content: Text(recipe.category),
),
SizedBox(height: 8),
_buildInfoCard(
context,
icon: Icons.star,
title: "推荐指数",
content: Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(5, (index) {
return Icon(
index < recipe.recommendRate ? Icons.star : Icons.star_border,
color: Theme.of(context).primaryColor,
size: 20,
);
}),
),
),
SizedBox(height: 8),
_buildInfoCard(
context,
icon: Icons.shopping_cart,
title: "食材信息",
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [_buildMaterialSection(materialList)],
),
),
SizedBox(height: 8),
_buildInfoCard(
context,
icon: Icons.list,
title: "步骤信息",
content: Column(children: [_buildStepSection(recipe.stepList)]),
),
SizedBox(height: 8),
_buildInfoCard(
context,
icon: Icons.note,
title: "其他信息",
content: Text(
'备注:${recipe.remark.isEmpty ? '' : recipe.remark}',
style: const TextStyle(height: 1.5),
),
),
],
);
}
Widget _buildTitleSection() {
return Text(
recipe.name,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
);
}
Widget _buildMaterialSection(List<RecipeMaterial> materials) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (materials.isEmpty)
buildTag(context, '')
else
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8.0,
children: materials.map((m) => _buildMaterialItem(m)).toList(),
),
],
);
}
Widget _buildMaterialItem(RecipeMaterial material) {
return Text('${material.name} ${material.amount}');
}
Widget _buildStepSection(List<RecipeStep> steps) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (steps.isEmpty)
buildTag(context, '')
else
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8.0,
children: steps.map((m) => _buildStepItem(m)).toList(),
),
],
);
}
Widget _buildStepItem(RecipeStep step) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("${step.sort + 1}"),
SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
step.content,
style: const TextStyle(
height: 1.4,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
],
),
),
],
);
}
Widget _buildInfoCard(
BuildContext context, {
required IconData icon,
required String title,
required Widget content,
}) {
return CommonCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 5),
Text(
title,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
],
),
SizedBox(height: 5),
content,
],
),
);
}
Widget _buildButtons(FoodProvider provider) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
circleIconButton(
context: context,
icon: Icons.edit,
onPressed: () => _handleEditRecipe(provider),
),
SizedBox(width: 8),
circleIconButton(
context: context,
icon: _isSharing ? Icons.hourglass_top : Icons.share,
onPressed: _shareRecipeAsImage,
),
],
);
}
}

569
lib/views/recipe_form.dart Normal file
View File

@@ -0,0 +1,569 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/dialog_widget.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/provider/food_provider.dart';
import 'package:food_hub_app/widgets/common/form.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:flutter_common/utils/toast_util.dart';
import 'package:flutter_common/widget/loading_widget.dart';
import 'package:provider/provider.dart';
class RecipeForm extends StatefulWidget {
const RecipeForm({super.key});
@override
State<RecipeForm> createState() => _RecipeFormState();
}
class _RecipeFormState extends State<RecipeForm> {
final _formKey = GlobalKey<FormBuilderState>();
int _currentStep = 0;
// 表单字段名称常量
static const String _nameField = 'name';
static const String _categoryField = 'category';
static const String _recommendRateField = 'recommendRate';
static const String _remarkField = 'remark';
static const String _isShareField = 'isShare';
// 分类选项
final List<String> _categoryOptions = [
'家常菜',
'川菜',
'粤菜',
'湘菜',
'西餐',
'甜品',
'汤羹',
'其他',
];
@override
void initState() {
super.initState();
}
// 构建步骤内容
Widget _buildStepContent(FoodProvider provider) {
switch (_currentStep) {
case 0:
return _buildInfo(provider);
case 1:
return _buildMaterials(provider);
case 2:
return _buildSteps(provider);
default:
return _buildInfo(provider);
}
}
// 第一步:基本信息
Widget _buildInfo(FoodProvider provider) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildFormLabel(context: context, text: '菜谱名称', isRequired: true),
const SizedBox(height: 8),
FormBuilderTextField(
name: _nameField,
initialValue: provider.recipeFormItem.name,
decoration: buildInputDecoration(
context: context,
hintText: '请输入菜谱名称',
prefixIcon: Icons.restaurant_menu,
),
onChanged: (value) {
provider.updateRecipeFormItem(name: value ?? '');
},
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(errorText: '菜谱名称不能为空'),
FormBuilderValidators.maxLength(50, errorText: '名称不能超过50个字符'),
]),
),
const SizedBox(height: 16),
buildFormLabel(context: context, text: '分类', isRequired: true),
const SizedBox(height: 8),
FormBuilderDropdown(
name: _categoryField,
initialValue: provider.recipeFormItem.category,
decoration: buildInputDecoration(
context: context,
hintText: '请选择分类',
prefixIcon: Icons.widgets,
),
items:
_categoryOptions
.map(
(category) => DropdownMenuItem(
value: category,
child: Text(category),
),
)
.toList(),
onChanged: (value) {
provider.updateRecipeFormItem(category: value ?? '');
},
validator: FormBuilderValidators.required(errorText: '请选择分类'),
),
const SizedBox(height: 16),
buildFormLabel(context: context, text: '推荐评分', isRequired: false),
const SizedBox(height: 8),
FormBuilderSlider(
name: _recommendRateField,
initialValue: provider.recipeFormItem.recommendRate,
min: 1,
max: 5,
divisions: 4,
activeColor: Theme.of(context).colorScheme.primary,
decoration: const InputDecoration(border: InputBorder.none),
onChanged: (value) {
provider.updateRecipeFormItem(recommendRate: value ?? 0);
},
),
const SizedBox(height: 16),
buildFormLabel(context: context, text: '备注', isRequired: false),
const SizedBox(height: 8),
FormBuilderTextField(
name: _remarkField,
initialValue: provider.recipeFormItem.remark,
maxLines: 3,
decoration: buildInputDecoration(
context: context,
hintText: '请输入菜谱的特别说明或小贴士',
),
onChanged: (value) {
provider.updateRecipeFormItem(remark: value ?? '');
},
),
const SizedBox(height: 16),
FormBuilderCheckbox(
name: _isShareField,
initialValue: provider.recipeFormItem.isShare,
title: const Text('公开分享此菜谱'),
onChanged: (value) {
provider.updateRecipeFormItem(isShare: value ?? false);
},
),
],
),
);
}
// 第二步:食材清单
Widget _buildMaterials(FoodProvider provider) {
final materials = provider.recipeFormItem.materialList;
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text('食材清单', style: Theme.of(context).textTheme.titleMedium),
const Spacer(),
IconButton(
icon: Icon(
Icons.add,
color: Theme.of(context).colorScheme.primary,
),
onPressed: () => provider.addRecipeMaterial(),
tooltip: '添加食材',
),
],
),
if (materials.isEmpty)
Container(
color: Theme.of(context).colorScheme.surface,
margin: EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(10),
child: Column(
children: [
Icon(Icons.inventory_2, size: 48, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'暂无食材,请点击添加按钮添加食材',
style: TextStyle(color: Colors.grey[600]),
),
],
),
)
else
Column(
children:
materials.asMap().entries.map((entry) {
final index = entry.key;
final material = entry.value;
return Column(
children: [
_buildMaterialItem(index, material, provider),
const SizedBox(height: 8),
],
);
}).toList(),
),
],
),
);
}
Widget _buildMaterialItem(
int index,
RecipeMaterial material,
FoodProvider provider,
) {
return Column(
key: ValueKey(material.id),
children: [
Row(
children: [
Expanded(
child: TextFormField(
initialValue: material.name,
decoration: buildInputDecoration(
context: context,
hintText: '请输入名称',
prefixIcon: Icons.content_paste,
),
onChanged: (value) {
provider.updateRecipeMaterial(index, name: value);
},
),
),
const SizedBox(width: 12),
Expanded(
child: TextFormField(
initialValue: material.amount,
decoration: buildInputDecoration(
context: context,
hintText: '请输入用量',
prefixIcon: Icons.scale,
),
onChanged: (value) {
provider.updateRecipeMaterial(index, amount: value);
},
),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red, size: 20),
onPressed: () => provider.removeRecipeMaterial(index),
),
],
),
],
);
}
// 第三步:制作步骤
Widget _buildSteps(FoodProvider provider) {
final steps = provider.recipeFormItem.stepList;
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text('制作步骤', style: Theme.of(context).textTheme.titleMedium),
const Spacer(),
IconButton(
icon: Icon(
Icons.add,
color: Theme.of(context).colorScheme.primary,
),
onPressed: () => provider.addRecipeStep(),
tooltip: '添加步骤',
),
],
),
if (steps.isEmpty)
Container(
color: Theme.of(context).colorScheme.surface,
margin: EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(10),
child: Column(
children: [
Icon(Icons.list_alt, size: 48, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'暂无步骤,请点击添加按钮添加制作步骤',
style: TextStyle(color: Colors.grey[600]),
),
],
),
)
else
Column(
children:
steps.asMap().entries.map((entry) {
final index = entry.key;
final step = entry.value;
return Column(
children: [
_buildStepItem(index, step, provider),
const SizedBox(height: 8),
],
);
}).toList(),
),
],
),
);
}
Widget _buildStepItem(int index, RecipeStep step, FoodProvider provider) {
return Row(
key: ValueKey(step.id),
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 步骤编号
Container(
width: 28,
height: 28,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
),
child: Text(
'${index + 1}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
const SizedBox(width: 12),
// 步骤内容
Expanded(
child: TextFormField(
initialValue: step.content,
decoration: buildInputDecoration(
context: context,
hintText: '请输入内容',
prefixIcon: Icons.content_paste,
),
onChanged: (value) {
provider.updateRecipeStep(index, content: value);
},
),
),
// 删除按钮
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => provider.removeRecipeStep(index),
),
],
);
}
// 步骤指示器
Widget _buildStepIndicator() {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildStepCircle(0),
SizedBox(width: 10),
_buildStepConnector(0),
SizedBox(width: 10),
_buildStepCircle(1),
SizedBox(width: 10),
_buildStepConnector(1),
SizedBox(width: 10),
_buildStepCircle(2),
],
),
Container(
margin: const EdgeInsets.only(top: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildStepTitle(0, '基本信息'),
SizedBox(width: 40),
_buildStepTitle(1, '食材清单'),
SizedBox(width: 40),
_buildStepTitle(2, '制作步骤'),
],
),
),
],
),
);
}
// 只构建圆圈,不包含标题文字
Widget _buildStepCircle(int stepIndex) {
final isActive = _currentStep == stepIndex;
final isCompleted = _currentStep > stepIndex;
final boxColor =
isActive || isCompleted
? Theme.of(context).colorScheme.primary
: Colors.grey;
return Container(
width: 32,
height: 32,
decoration: BoxDecoration(color: boxColor, shape: BoxShape.circle),
child: Center(
child:
isCompleted
? const Icon(Icons.check, color: Colors.white, size: 16)
: Text(
'${stepIndex + 1}',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
);
}
// 箭头连接器
Widget _buildStepConnector(int stepIndex) {
final isActive = _currentStep > stepIndex;
return SizedBox(
width: 40,
child: Icon(
Icons.arrow_forward,
size: 20,
color: isActive ? Theme.of(context).colorScheme.primary : Colors.grey,
),
);
}
Widget _buildStepTitle(int stepIndex, String title) {
final isActive = _currentStep == stepIndex;
final isCompleted = _currentStep > stepIndex;
final textColor =
isActive || isCompleted
? Theme.of(context).colorScheme.primary
: Colors.grey;
return Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
color: textColor,
fontSize: 12,
),
);
}
// 导航按钮
Widget _buildNavigationButtons(FoodProvider provider) {
final isLastStep = _currentStep == 2;
final isFirstStep = _currentStep == 0;
return Row(
children: [
if (!isFirstStep)
Expanded(
child: buildInfoButton(
context: context,
text: '上一步',
onPressed: _previousStep,
),
),
if (!isFirstStep) const SizedBox(width: 12),
Expanded(
child: buildPrimaryButton(
context: context,
text: isLastStep ? '提交' : '下一步',
onPressed: () => _nextStep(provider),
),
),
],
);
}
void _nextStep(FoodProvider provider) {
if (_currentStep < 2) {
if (_formKey.currentState?.saveAndValidate() ?? false) {
setState(() {
_currentStep++;
});
}
} else {
_submitForm(provider);
}
}
void _previousStep() {
if (_currentStep > 0) {
setState(() {
_currentStep--;
});
}
}
void _submitForm(FoodProvider provider) async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
LoadingDialog.show(context, message: '提交中');
final result = await provider.handleRecipe();
if (result == true) {
LoadingDialog.hide(context);
if (provider.isEditing) {
showSuccessTip(context, '更新菜谱成功');
} else {
showSuccessTip(context, '新增菜谱成功');
}
Future.delayed(Duration(milliseconds: 1500), () {
if (mounted) {
Navigator.pop(context);
}
});
} else {
LoadingDialog.hide(context);
if (provider.isEditing) {
showErrorTip(context, '更新菜谱失败');
} else {
showErrorTip(context, '新增菜谱失败');
}
}
} else {
ToastUtil.error('请检查表单填写是否正确');
}
}
@override
Widget build(BuildContext context) {
final provider = context.watch<FoodProvider>();
return AnimatedPadding(
duration: const Duration(milliseconds: 200),
padding: MediaQuery.of(context).viewInsets,
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
_buildStepIndicator(),
FormBuilder(key: _formKey, child: _buildStepContent(provider)),
_buildNavigationButtons(provider),
],
),
),
),
);
}
}

View File

@@ -1,9 +1,23 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:food_hub_app/widgets/recipe/recipe_calendar.dart'; import 'package:food_hub_app/provider/food_provider.dart';
import 'package:food_hub_app/widgets/recipe/recipe_list.dart'; import 'package:food_hub_app/views/moment_form.dart';
import 'package:food_hub_app/widgets/recipe/recipe_timeline.dart'; import 'package:food_hub_app/views/recipe_form.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart'; import 'package:food_hub_app/views/record_form.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:food_hub_app/widgets/recipe/calendar.dart';
import 'package:food_hub_app/widgets/recipe/list.dart';
import 'package:food_hub_app/widgets/recipe/timeline.dart';
import 'package:provider/provider.dart';
enum RecordTab {
recipe('菜谱'),
calendar('日历'),
timeline('时间轴');
final String label;
const RecordTab(this.label);
}
class RecordPage extends StatefulWidget { class RecordPage extends StatefulWidget {
const RecordPage({super.key}); const RecordPage({super.key});
@@ -12,18 +26,11 @@ class RecordPage extends StatefulWidget {
State<StatefulWidget> createState() => _RecordPageState(); State<StatefulWidget> createState() => _RecordPageState();
} }
class _RecordPageState extends State<RecordPage> class _RecordPageState extends State<RecordPage> {
with SingleTickerProviderStateMixin { final List<Widget> _tabPages = [
RecipeList(userId: 0),
late final TabController _tabController = TabController( RecipeCalendar(),
length: 3, RecipeTimeline(),
vsync: this,
);
final List<TDTab> tabs = [
const TDTab(text: '菜谱', icon: Icon(Icons.book)),
const TDTab(text: '日历', icon: Icon(Icons.calendar_month)),
const TDTab(text: '时间轴', icon: Icon(Icons.timeline)),
]; ];
@override @override
@@ -33,37 +40,176 @@ class _RecordPageState extends State<RecordPage>
@override @override
void dispose() { void dispose() {
_tabController.dispose();
super.dispose(); super.dispose();
} }
Widget _buildTabs({
required BuildContext context,
required RecordTab currentTab,
required ValueChanged<RecordTab> onTabChanged,
}) {
return Container(
padding: EdgeInsets.all(8),
child: buildToggleSwitch(
context: context,
currentTab: currentTab,
tabValues: RecordTab.values,
labels: RecordTab.values.map((e) => e.label).toList(),
onTabChanged: onTabChanged,
),
);
}
void _onTabChange(RecordTab tab, FoodProvider provider) {
provider.currentRecordTab = tab;
provider.onRecordTabChange();
}
void _handleAddRecord(FoodProvider provider) {
provider.resetRecordForm();
provider.isEditing = false;
Navigator.pop(context);
showModalBottomSheet(
context: context,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => RecordForm(),
);
}
void _handleAddRecipe(FoodProvider provider) {
provider.resetRecipeForm();
provider.isEditing = false;
Navigator.pop(context);
showModalBottomSheet(
context: context,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => RecipeForm(),
);
}
void _handleAddMoment(FoodProvider provider) {
provider.resetMomentForm();
provider.isEditing = false;
Navigator.pop(context);
showModalBottomSheet(
context: context,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => MomentForm(),
);
}
void _onPressAdd(FoodProvider provider) {
showModalBottomSheet(
context: context,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder:
(context) => Padding(
padding: EdgeInsetsGeometry.symmetric(horizontal: 0, vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
Text('请选择操作', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 10),
ListTile(
leading: Icon(
Icons.book,
color: Theme.of(context).colorScheme.primary,
),
title: Text(
'新增菜谱',
style: Theme.of(context).textTheme.bodyLarge,
),
onTap: () => _handleAddRecipe(provider),
),
ListTile(
leading: Icon(
Icons.note_add,
color: Theme.of(context).colorScheme.primary,
),
title: Text(
'新增记录',
style: Theme.of(context).textTheme.bodyLarge,
),
onTap: () => _handleAddRecord(provider),
),
ListTile(
leading: Icon(
Icons.group,
color: Theme.of(context).colorScheme.primary,
),
title: Text(
'发布朋友圈',
style: Theme.of(context).textTheme.bodyLarge,
),
onTap: () => _handleAddMoment(provider),
),
],
),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( final provider = context.watch<FoodProvider>();
padding: EdgeInsets.all(0),
child: Column( return Scaffold(
children: [ backgroundColor: Colors.transparent,
TDTabBar( appBar: AppBar(
tabs: tabs, elevation: 0,
controller: _tabController, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
backgroundColor: Colors.white, title: Text('记录', style: Theme.of(context).textTheme.titleLarge),
showIndicator: true, centerTitle: true,
), leading: Builder(
Expanded( builder:
child: Padding( (context) => IconButton(
padding: EdgeInsets.all(5), icon: Icon(Icons.menu, color: Theme.of(context).primaryColor),
child: TabBarView( onPressed: () => {},
controller: _tabController,
children: [
RecipeList(),
RecipeCalendar(),
RecipeTimeline(),
],
), ),
), ),
actions: [
IconButton(
icon: Icon(Icons.search, color: Theme.of(context).primaryColor),
onPressed: () {},
),
IconButton(
icon: Icon(Icons.add, color: Theme.of(context).primaryColor),
onPressed: () => _onPressAdd(provider),
), ),
], ],
), ),
body: Padding(
padding: const EdgeInsets.all(10),
child: Column(
children: [
_buildTabs(
context: context,
currentTab: provider.currentRecordTab,
onTabChanged: (tab) => _onTabChange(tab, provider),
),
Expanded(child: _tabPages[provider.currentRecordTab.index]),
],
),
),
); );
} }
} }

View File

@@ -1,243 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:food_hub_app/utils/index.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:image_picker/image_picker.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
class RecordFormPage extends StatefulWidget {
const RecordFormPage({super.key});
@override
State<RecordFormPage> createState() => _RecordFormPage();
}
class _RecordFormPage extends State<RecordFormPage> {
final _formKey = GlobalKey<FormBuilderState>();
List<TextEditingController> _controller = [];
FormController _formController = FormController();
String _selected_1 = '';
String _selected_2 = '';
String? _initLocalData;
final ImagePicker _picker = ImagePicker();
XFile? _selectedImage;
/// 整个表单存放的数据
Map<String, dynamic> _formData = {"name": '', "date": '', "photo": ''};
/// 定义整个校验规则
final Map<String, TDFormValidation> _validationRules = {
'name': TDFormValidation(
validate: (value) => value == null || value.isEmpty ? 'empty' : null,
errorMessage: '输入不能为空',
type: TDFormItemType.input,
),
"birth": TDFormValidation(
validate: (value) => value == null || value.isEmpty ? 'empty' : null,
errorMessage: '不能为空',
type: TDFormItemType.dateTimePicker,
),
"photo": TDFormValidation(
validate: (value) => value == null || value.isEmpty ? 'empty' : null,
errorMessage: '不能为空',
type: TDFormItemType.upLoadImg,
),
};
List<TDUploadFile> files = [];
List<TDUploadFile> _onValueChanged(
List<TDUploadFile> fileList,
List<TDUploadFile> value,
TDUploadType event,
) {
switch (event) {
case TDUploadType.add:
fileList.addAll(value);
break;
case TDUploadType.remove:
fileList.removeWhere((element) => element.key == value[0].key);
break;
case TDUploadType.replace:
final firstReplaceFile = value.first;
final index = fileList.indexWhere(
(file) => file.key == firstReplaceFile.key,
);
if (index != -1) {
fileList[index] = firstReplaceFile;
}
break;
}
return fileList;
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
void _confirmClick(BuildContext context) {
if (_formKey.currentState?.saveAndValidate() ?? false) {
// showSuccessToast("新增记录成功");
print(_formData);
} else {
TDMessage.showMessage(
context: context,
visible: true,
icon: true,
content: "请先输入信息",
theme: MessageTheme.error,
duration: 3000,
);
}
}
TDFormItem buildNameItem() {
return TDFormItem(
label: '菜谱名称',
name: 'name',
type: TDFormItemType.input,
labelWidth: 82.0,
showErrorMessage: true,
requiredMark: true,
child: TDInput(
leftContentSpace: 0,
inputDecoration: InputDecoration(
hintText: "请输入菜谱名称",
border: InputBorder.none,
hintStyle: TextStyle(color: TDTheme.of(context).grayColor6),
),
backgroundColor: Colors.white,
additionInfoColor: TDTheme.of(context).errorColor6,
showBottomDivider: false,
onChanged: (value) {
_formData['name'] = value;
},
),
);
}
TDFormItem buildDateItem() {
return TDFormItem(
label: '完成时间',
name: 'date',
labelWidth: 82.0,
type: TDFormItemType.dateTimePicker,
contentAlign: TextAlign.left,
tipAlign: TextAlign.left,
hintText: '请选择完成时间',
select: _formData['date'],
selectFn: (BuildContext context) {
DateTime now = DateTime.now();
TDPicker.showDatePicker(
context,
title: '选择时间',
onConfirm: (selected) {
setState(() {
_formData['date'] = parseDatePickerSelected(selected);
});
Navigator.of(context).pop();
},
dateStart: [2000, 01, 01],
dateEnd: [2100, 12, 31],
initialDate: [now.year, now.month, now.day],
);
},
);
}
TDFormItem buildImageItem() {
return TDFormItem(
label: '上传图片',
name: 'photo',
labelWidth: 82.0,
type: TDFormItemType.upLoadImg,
child: TDUpload(
files: files,
onError: print,
onValidate: print,
onChange: ((imgList, type) {
files = _onValueChanged(files ?? [], imgList, type);
List imgs = files.map((e) => e.remotePath ?? e.assetPath).toList();
setState(() {});
}),
),
);
}
Widget buildFormBtnGroup() {
return Container(
decoration: BoxDecoration(color: TDTheme.of(context).whiteColor1),
child: Padding(
padding: const EdgeInsets.all(10),
child: Row(
children: [
Expanded(
child: TDButton(
text: '取消',
type: TDButtonType.fill,
theme: TDButtonTheme.light,
shape: TDButtonShape.rectangle,
onTap: () => Navigator.pop(context),
),
),
SizedBox(width: 20),
Expanded(
child: TDButton(
text: '提交',
type: TDButtonType.fill,
theme: TDButtonTheme.primary,
shape: TDButtonShape.rectangle,
onTap: () => _confirmClick(context),
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('新增记录', style: TextStyle(color: Colors.white)),
backgroundColor: Theme.of(context).primaryColor,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context)
),
),
backgroundColor: Color(0xFFF5F5F5),
body: Padding(
padding: const EdgeInsets.all(10),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: SingleChildScrollView(
child: TDForm(
formController: _formController,
data: _formData,
rules: _validationRules,
formContentAlign: TextAlign.left,
formShowErrorMessage: true,
onSubmit: () => {},
items: [buildNameItem(), buildDateItem(), buildImageItem()],
btnGroup: [buildFormBtnGroup()],
),
),
),
),
);
}
}

346
lib/views/record_form.dart Normal file
View File

@@ -0,0 +1,346 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/utils/toast_util.dart';
import 'package:flutter_common/widget/dialog_widget.dart';
import 'package:flutter_common/widget/loading_widget.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:food_hub_app/apis/recipe.dart';
import 'package:food_hub_app/config/app_config.dart';
import 'package:food_hub_app/provider/food_provider.dart';
import 'package:food_hub_app/utils/minio_utils.dart';
import 'package:food_hub_app/widgets/common/form.dart';
import 'package:food_hub_app/widgets/common/image.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class RecordForm extends StatefulWidget {
const RecordForm({super.key});
@override
State<RecordForm> createState() => _RecordFormState();
}
class _RecordFormState extends State<RecordForm> {
final _formKey = GlobalKey<FormBuilderState>();
static const String _nameField = 'name';
static const String _dateField = 'date';
List<String> _foodNameList = [];
@override
void initState() {
super.initState();
refreshCategoryList();
}
Future<void> refreshCategoryList() async {
final result = await queryFoodNameListApi();
setState(() {
_foodNameList = result;
});
}
Widget _buildFormBuilder(FoodProvider provider) {
return FormBuilder(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildFormLabel(context: context, text: '菜谱名称', isRequired: true),
const SizedBox(height: 12),
_buildRecipeField(provider),
const SizedBox(height: 12),
buildFormLabel(context: context, text: '完成时间', isRequired: true),
const SizedBox(height: 12),
_buildDateField(provider),
const SizedBox(height: 12),
buildFormLabel(context: context, text: '上传图片', isRequired: true),
const SizedBox(height: 12),
_buildImageField(provider),
const SizedBox(height: 12),
buildFormButtonGroup(
context: context,
isShowDelete: provider.isEditing,
onConfirm: () => _submitForm(provider),
onDelete: () => _deleteForm(provider),
),
],
),
);
}
Widget _buildRecipeField(FoodProvider provider) {
return Autocomplete<String>(
initialValue: TextEditingValue(text: provider.recordFormItem.name),
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty || _foodNameList.isEmpty) {
return const Iterable<String>.empty();
}
return _foodNameList.where(
(option) => option.toLowerCase().contains(
textEditingValue.text.toLowerCase(),
),
);
},
onSelected: (String value) {
provider.updateRecordFormItem(name: value);
},
optionsViewBuilder: (
BuildContext context,
AutocompleteOnSelected<String> onSelected,
Iterable<String> options,
) {
Widget buildOptionItem(String option) {
return InkWell(
onTap: () => onSelected(option),
child: Container(
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: Text(
option,
style: TextStyle(fontSize: 14, color: Colors.grey.shade800),
),
),
);
}
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 2,
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context).colorScheme.surface,
constraints: BoxConstraints(
maxHeight: 200,
maxWidth: MediaQuery.of(context).size.width - 58,
),
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
return buildOptionItem(options.elementAt(index));
},
),
),
),
);
},
fieldViewBuilder: (
BuildContext context,
TextEditingController controller,
FocusNode focusNode,
VoidCallback onFieldSubmitted,
) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (provider.recordFormItem.name.isNotEmpty &&
controller.text.isEmpty) {
controller.text = provider.recordFormItem.name;
}
});
void onClearRecipeName() {
controller.clear();
provider.updateRecordFormItem(name: '');
}
Widget? buildSuffixIcon() {
final isShow = controller.text.isNotEmpty && !provider.isEditing;
return isShow
? IconButton(
icon: Icon(Icons.clear, size: 18),
onPressed: onClearRecipeName,
)
: null;
}
return FormBuilderTextField(
name: _nameField,
controller: controller,
focusNode: focusNode,
enabled: !provider.isEditing,
onChanged: (value) {
provider.updateRecordFormItem(name: value ?? '');
},
decoration: buildInputDecoration(
context: context,
hintText: '请输入菜谱名称',
prefixIcon: Icons.restaurant_menu,
).copyWith(suffixIcon: buildSuffixIcon()),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入菜谱名称';
}
return null;
},
);
},
);
}
Widget _buildDateField(FoodProvider provider) {
return FormBuilderTextField(
name: _dateField,
readOnly: true,
initialValue: provider.recordFormItem.date,
decoration: buildInputDecoration(
context: context,
hintText: '请选择完成时间',
prefixIcon: Icons.calendar_month,
),
onTap: () => _onSelectDate(provider),
validator: (value) {
if (value == null || value.isEmpty) {
return '请选择完成时间';
}
return null;
},
);
}
/// 选择日期
Future<void> _onSelectDate(FoodProvider provider) async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2100),
);
if (picked != null) {
final String date = DateFormat('yyyy-MM-dd').format(picked);
_formKey.currentState?.fields[_dateField]?.didChange(date);
provider.updateRecordFormItem(date: date);
}
}
Widget _buildImageField(FoodProvider provider) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (provider.recordFormItem.imageUrl.isEmpty)
buildImageUploadButton(
context: context,
onTap: () => _pickImage(provider),
)
else
ImagePreview(
imageUrls: [provider.recordFormItem.imageUrl],
index: 0,
onRemoveImage: () => _removeImage(provider),
),
],
);
}
/// 选择图片(限制单张)
Future<void> _pickImage(FoodProvider provider) async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['jpg', 'jpeg', 'png'],
allowMultiple: false,
);
if (result != null) {
PlatformFile file = result.files.first;
LoadingDialog.show(context, message: '上传中');
try {
final fileName = await MinIOHelper().uploadFile(
bucketName: AppConfig.bucketName,
file: file,
);
ToastUtil.success('上传成功');
provider.updateRecordFormItem(imageUrl: fileName);
} catch (e) {
ToastUtil.error(e.toString());
} finally {
LoadingDialog.hide(context);
}
}
}
/// 移除图片
void _removeImage(FoodProvider provider) {
provider.updateRecordFormItem(imageUrl: '');
}
/// 提交表单
void _submitForm(FoodProvider provider) async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
if (provider.recordFormItem.imageUrl.isEmpty) {
ToastUtil.error('请上传图片');
return;
}
LoadingDialog.show(context, message: '提交中');
final result = await provider.handleRecord();
if (result == true) {
await provider.onRecordTabChange();
LoadingDialog.hide(context);
if (provider.isEditing) {
showSuccessTip(context, '更新记录成功');
} else {
showSuccessTip(context, '新增记录成功');
}
Future.delayed(Duration(milliseconds: 1500), () {
if (mounted) {
Navigator.pop(context);
}
});
} else {
LoadingDialog.hide(context);
if (provider.isEditing) {
showErrorTip(context, '更新记录失败');
} else {
showErrorTip(context, '新增记录失败');
}
}
}
}
void _deleteForm(FoodProvider provider) async {
final result = await showConfirmDialog(context, '确认删除该记录?');
if (result == true) {
LoadingDialog.show(context, message: '删除中');
final isDelete = await provider.deleteRecord();
if (isDelete) {
await provider.onRecordTabChange();
LoadingDialog.hide(context);
showSuccessTip(context, '删除记录成功');
Future.delayed(Duration(milliseconds: 1500), () {
if (mounted) {
Navigator.pop(context);
}
});
}
}
}
@override
Widget build(BuildContext context) {
final provider = context.watch<FoodProvider>();
return AnimatedPadding(
duration: const Duration(milliseconds: 200),
padding: MediaQuery.of(context).viewInsets,
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(16),
child: _buildFormBuilder(provider),
),
),
);
}
}

View File

@@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:graphic/graphic.dart'; import 'package:flutter_common/widget/chart.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'data.dart'; import 'package:food_hub_app/provider/food_provider.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
import 'package:provider/provider.dart';
class StatsPage extends StatefulWidget { class StatsPage extends StatefulWidget {
const StatsPage({super.key}); const StatsPage({super.key});
@@ -11,88 +13,145 @@ class StatsPage extends StatefulWidget {
} }
class _StatsPage extends State<StatsPage> { class _StatsPage extends State<StatsPage> {
var _smooth = false; final double chartHeight = 400;
var _stepped = false;
@override
void initState() {
super.initState();
// 初始化加载数据
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<FoodProvider>().refreshFoodStats();
});
}
Widget _buildSummaryStats(BuildContext context, FoodProvider provider) {
final colors = Theme.of(context).colorScheme;
return GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
// 禁用网格自身滚动
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 2,
children: [
GlassCard(
useOwnLayer: true,
settings: LiquidGlassSettings(glassColor: colors.surface),
padding: EdgeInsetsGeometry.all(10),
child: StatsCard(
icon: Icons.restaurant_menu,
title: '菜谱总数',
value: provider.summaryStats.recipeCount.toString(),
unit: '',
),
),
GlassCard(
useOwnLayer: true,
settings: LiquidGlassSettings(glassColor: colors.surface),
padding: EdgeInsetsGeometry.all(10),
child: StatsCard(
icon: Icons.grid_view_rounded,
title: '菜谱类别',
value: provider.summaryStats.categoryCount.toString(),
unit: '',
),
),
GlassCard(
useOwnLayer: true,
settings: LiquidGlassSettings(glassColor: colors.surface),
padding: EdgeInsetsGeometry.all(10),
child: StatsCard(
icon: Icons.flag,
title: '做菜次数',
value: provider.summaryStats.workCount.toString(),
unit: '',
),
),
GlassCard(
useOwnLayer: true,
settings: LiquidGlassSettings(glassColor: colors.surface),
padding: EdgeInsetsGeometry.all(10),
child: StatsCard(
icon: Icons.show_chart,
title: '平均次数',
value: provider.averageRecordCount.toStringAsFixed(2),
unit: '次/月',
),
),
],
);
}
Widget _buildContent(BuildContext context, FoodProvider provider) {
final colors = Theme.of(context).colorScheme;
return Column(
children: [
_buildSummaryStats(context, provider),
SizedBox(height: 8),
SizedBox(
height: chartHeight,
child: GlassCard(
useOwnLayer: true,
settings: LiquidGlassSettings(glassColor: colors.surface),
child: LineChart(
title: '记录统计',
xAxisName: '日期',
yAxisName: '次数',
unit: '',
data: provider.recordStats,
),
),
),
SizedBox(height: 8),
SizedBox(
height: chartHeight,
child: GlassCard(
useOwnLayer: true,
settings: LiquidGlassSettings(glassColor: colors.surface),
child: PieChart(
title: '菜谱统计',
unit: '',
data: provider.categoryStats,
),
),
),
SizedBox(height: 8),
GlassCard(
useOwnLayer: true,
settings: LiquidGlassSettings(glassColor: colors.surface),
child: RankChart(title: '排行榜', data: provider.rankStats, unit: ''),
),
],
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SingleChildScrollView( final provider = context.watch<FoodProvider>();
child: Center(
child: Column( return Scaffold(
children: <Widget>[ backgroundColor: Colors.transparent,
Container( appBar: AppBar(
margin: const EdgeInsets.only(top: 10), backgroundColor: Theme.of(context).scaffoldBackgroundColor,
width: 350, title: Text('统计', style: Theme.of(context).textTheme.titleLarge),
height: 300, centerTitle: true,
child: Chart( automaticallyImplyLeading: false,
data: basicData, ),
variables: { body: Padding(
'genre': Variable( padding: EdgeInsetsGeometry.all(10),
accessor: (Map map) => map['genre'] as String, child: Stack(
), children: [
'sold': Variable(accessor: (Map map) => map['sold'] as num), if (provider.isLoading)
}, buildLoadingIndicator()
marks: [ else
IntervalMark( SingleChildScrollView(
label: LabelEncode( padding: const EdgeInsets.only(bottom: 90),
encoder: (tuple) => Label(tuple['sold'].toString()), child: _buildContent(context, provider),
),
elevation: ElevationEncode(
value: 0,
updaters: {
'tap': {true: (_) => 5},
},
),
color: ColorEncode(
value: Defaults.primaryColor,
updaters: {
'tap': {false: (color) => color.withAlpha(100)},
},
),
),
],
axes: [Defaults.horizontalAxis, Defaults.verticalAxis],
selections: {'tap': PointSelection(dim: Dim.x)},
tooltip: TooltipGuide(),
crosshair: CrosshairGuide(),
), ),
),
Container(
padding: const EdgeInsets.fromLTRB(20, 40, 20, 5),
child: const Text(
'Transposed Bar Chart',
style: TextStyle(fontSize: 20),
),
),
Container(
margin: const EdgeInsets.only(top: 10),
width: 350,
height: 300,
child: Chart(
data: basicData,
variables: {
'genre': Variable(
accessor: (Map map) => map['genre'] as String,
),
'sold': Variable(accessor: (Map map) => map['sold'] as num),
},
transforms: [Proportion(variable: 'sold', as: 'percent')],
marks: [
IntervalMark(
position: Varset('percent') / Varset('genre'),
label: LabelEncode(
encoder: (tuple) => Label(tuple['sold'].toString()),
),
color: ColorEncode(
variable: 'genre',
values: Defaults.colors10,
),
modifiers: [StackModifier()],
),
],
coord: PolarCoord(transposed: true, dimCount: 1, dimFill: 1.05),
),
),
], ],
), ),
), ),

View File

@@ -0,0 +1,67 @@
import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.dart';
Widget buildEasyRefresh({
required EasyRefreshController freshController,
required Future<void> Function()? onRefresh,
required Future<void> Function()? onLoad,
required Widget body,
}) {
return EasyRefresh(
controller: freshController,
header: ClassicHeader(
dragText: '下拉刷新',
armedText: '释放刷新',
readyText: '准备刷新',
processingText: '刷新中...',
processedText: '刷新完成',
failedText: '刷新失败',
noMoreText: '没有更多数据',
showText: true,
messageText: '更新于 %T',
showMessage: true,
),
footer: ClassicFooter(
dragText: '上拉加载',
armedText: '释放加载',
readyText: '准备加载',
processingText: '加载中...',
processedText: '加载完成',
failedText: '加载失败',
noMoreText: '没有更多数据',
showText: true,
messageText: '更新于 %T',
showMessage: true,
),
onRefresh: onRefresh,
onLoad: onLoad,
child: body,
);
}
Widget buildScrollToTop({
required BuildContext context,
required VoidCallback scrollToTop,
}) {
final colors = Theme.of(context).colorScheme;
return Positioned(
right: 0,
bottom: 80,
child: FloatingActionButton(
onPressed: scrollToTop,
backgroundColor: colors.primary,
elevation: 2,
mini: true,
child: Icon(Icons.arrow_upward, color: colors.surface),
),
);
}
void scrollToTopAnimateTo(ScrollController controller) {
controller.animateTo(
0,
duration: const Duration(milliseconds: 500), // 滚动动画时长
curve: Curves.easeInOut, // 滚动动画曲线
);
}

View File

@@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'index.dart';
/// 通用输入框样式
InputDecoration buildInputDecoration({
required BuildContext context,
required String hintText,
IconData? prefixIcon,
}) {
final colors = Theme.of(context).colorScheme;
return InputDecoration(
hintText: hintText,
hintStyle: Theme.of(context).textTheme.bodyMedium,
filled: true,
fillColor: colors.surface,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(10),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: colors.primary, width: 2),
borderRadius: BorderRadius.circular(10),
),
errorBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.red, width: 2),
borderRadius: BorderRadius.circular(10),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 14),
errorStyle: const TextStyle(fontSize: 12, height: 1, color: Colors.red),
prefixIcon:
prefixIcon != null
? Icon(prefixIcon, size: 20, color: colors.primary)
: null,
prefixIconConstraints: const BoxConstraints(minWidth: 40),
isDense: true,
);
}
Widget buildFormLabel({
required BuildContext context,
required String text,
required bool isRequired,
}) {
final colors = Theme.of(context).colorScheme;
return RichText(
text: TextSpan(
text: text,
style: TextStyle(
color: colors.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
fontFamily: 'CustomFont',
height: 1.2,
),
children: [
if (isRequired)
const TextSpan(
text: ' *',
style: TextStyle(color: Colors.red, fontSize: 16),
),
],
),
);
}
/// 表单底部按钮组组件
Widget buildFormButtonGroup({
required BuildContext context,
required bool isShowDelete,
required VoidCallback onConfirm,
required VoidCallback onDelete,
}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: _buildCancelButton(context)),
if (isShowDelete) ...[
SizedBox(width: 16),
Expanded(child: _buildDeleteButton(context, onDelete)),
],
SizedBox(width: 16),
Expanded(child: _buildSubmitButton(context, onConfirm)),
],
);
}
/// 取消按钮
Widget _buildCancelButton(BuildContext context) {
return buildInfoButton(
context: context,
text: '取消',
onPressed: () => Navigator.pop(context),
);
}
/// 提交按钮
Widget _buildSubmitButton(BuildContext context, VoidCallback onConfirm) {
return buildPrimaryButton(context: context, text: '提交', onPressed: onConfirm);
}
/// 删除按钮
Widget _buildDeleteButton(BuildContext context, VoidCallback onDelete) {
return buildErrorButton(text: '删除', onPressed: onDelete);
}

View File

@@ -0,0 +1,220 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:food_hub_app/config/app_config.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
class ImagePreviewPage extends StatefulWidget {
final List<String> images;
final int initialIndex;
const ImagePreviewPage({
super.key,
required this.images,
required this.initialIndex,
}) : assert(initialIndex >= 0 && initialIndex < images.length);
@override
State<ImagePreviewPage> createState() => _ImagePreviewPageState();
}
class _ImagePreviewPageState extends State<ImagePreviewPage> {
// 声明 PageController 并初始化初始索引
late PageController _pageController;
// 记录当前显示的图片索引(用于更新页码)
int _currentIndex = 0;
@override
void initState() {
super.initState();
// 初始化控制器,设置初始页面
_pageController = PageController(initialPage: widget.initialIndex);
// 初始化当前索引为初始索引
_currentIndex = widget.initialIndex;
// 监听页面切换事件
_pageController.addListener(() {
// 取当前页面的整数索引(避免滑动过程中的小数)
final currentPage = _pageController.page?.round() ?? 0;
// 只有当索引变化时才更新状态
if (currentPage != _currentIndex) {
setState(() {
_currentIndex = currentPage;
});
}
});
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final index = '${_currentIndex + 1}/${widget.images.length}';
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black54,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
// 显示实时更新的页码(当前索引+1 / 总数量)
title: Text(index, style: const TextStyle(color: Colors.white)),
centerTitle: true,
),
body: PhotoViewGallery(
pageOptions:
widget.images.map((url) {
return PhotoViewGalleryPageOptions(
imageProvider: NetworkImage('${AppConfig.imageBaseUrl}$url'),
minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 2,
// 点击空白处关闭预览
onTapDown: (context, details, controllerValue) {
Navigator.pop(context);
},
);
}).toList(),
pageController: _pageController,
scrollDirection: Axis.horizontal,
),
);
}
}
class CommonImage extends StatelessWidget {
final List<String> imageUrls;
final int index;
const CommonImage({super.key, required this.imageUrls, required this.index});
Widget _buildErrorImage() {
return Container(
color: Colors.grey[200],
child: const Icon(Icons.image, color: Colors.grey, size: 30),
);
}
void _navigateToImagePreview(
BuildContext context,
List<String> imageUrls,
int index,
) {
final route = MaterialPageRoute(
builder:
(context) => ImagePreviewPage(images: imageUrls, initialIndex: index),
);
Navigator.push(context, route);
}
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 4 / 3,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: GestureDetector(
onTap: () {
_navigateToImagePreview(context, imageUrls, index);
},
child: CachedNetworkImage(
imageUrl: '${AppConfig.imageBaseUrl}${imageUrls[index]}',
fit: BoxFit.cover,
progressIndicatorBuilder:
(context, url, downloadProgress) => CircularProgressIndicator(
strokeWidth: 1.5,
padding: EdgeInsetsGeometry.all(20),
value: downloadProgress.progress,
),
errorWidget: (context, error, stackTrace) => _buildErrorImage(),
),
),
),
);
}
}
class ImagePreview extends StatelessWidget {
final List<String> imageUrls;
final int index;
final VoidCallback onRemoveImage;
const ImagePreview({
super.key,
required this.imageUrls,
required this.index,
required this.onRemoveImage,
});
Widget _buildDeleteImage() {
return Positioned(
top: 0,
right: 0,
child: GestureDetector(
onTap: onRemoveImage,
child: Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: const Icon(Icons.close, color: Colors.white, size: 16),
),
),
);
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Stack(
children: [
CommonImage(imageUrls: imageUrls, index: index),
_buildDeleteImage(),
],
),
),
);
}
}
Widget buildImageUploadButton({
required BuildContext context,
required VoidCallback onTap,
}) {
final colors = Theme.of(context).colorScheme;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
width: double.infinity,
height: 180,
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: colors.surface),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.camera_alt, color: colors.onSurface.withAlpha(100)),
SizedBox(height: 6),
Text('点击上传美食图片', style: Theme.of(context).textTheme.bodyMedium),
],
),
),
);
}

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:toggle_switch/toggle_switch.dart';
Widget formLabelText({required String labelText, bool isRequired = false}) { Widget formLabelText({required String labelText, bool isRequired = false}) {
return Text.rich( return Text.rich(
@@ -14,7 +14,7 @@ Widget formLabelText({required String labelText, bool isRequired = false}) {
} }
InputDecoration formInputDecoration({ InputDecoration formInputDecoration({
required String hintText, // 使用命名参数并标记为必填 required String hintText,
IconData? prefixIcon, IconData? prefixIcon,
}) { }) {
return InputDecoration( return InputDecoration(
@@ -27,67 +27,124 @@ InputDecoration formInputDecoration({
); );
} }
/// 确认按钮样式 Widget buildToggleSwitch<T extends Enum>({
ButtonStyle primaryButtonStyle() { required BuildContext context,
return ButtonStyle( required T currentTab,
backgroundColor: WidgetStateProperty.all(Colors.green), required List<T> tabValues,
shape: WidgetStateProperty.all( required List<String> labels,
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), required ValueChanged<T> onTabChanged,
), }) {
final colors = Theme.of(context).colorScheme;
return ToggleSwitch(
minWidth: 90.0,
minHeight: 40.0,
initialLabelIndex: tabValues.indexOf(currentTab),
totalSwitches: tabValues.length,
labels: labels,
activeBgColor: [colors.primary],
activeFgColor: colors.surface,
inactiveBgColor: colors.surface,
inactiveFgColor: colors.onSurface,
cornerRadius: 12.0,
customTextStyles: [TextStyle(fontSize: 12, fontWeight: FontWeight.w500)],
onToggle: (index) {
if (index != null && index < tabValues.length) {
onTabChanged(tabValues[index]);
}
},
); );
} }
/// 取消按钮样式 /// 圆形图标按钮
ButtonStyle cancelButtonStyle() { Widget circleIconButton({
return ButtonStyle( required IconData icon,
backgroundColor: WidgetStateProperty.all(Colors.grey), required VoidCallback onPressed,
shape: WidgetStateProperty.all( required BuildContext context,
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), bool? isSmall,
}) {
final double size = isSmall == true ? 24 : 36;
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
fixedSize: Size(size, size),
shape: CircleBorder(),
elevation: 0,
backgroundColor: Theme.of(context).colorScheme.primary,
padding: EdgeInsets.zero,
minimumSize: const Size(0, 0),
), ),
child: Icon(icon, color: Colors.white),
); );
} }
Text buttonText({required String text}) { Widget buildTag(BuildContext context, String title) {
return Text(text, style: TextStyle(color: Colors.white)); final colors = Theme.of(context).colorScheme;
return Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: colors.primary,
borderRadius: BorderRadius.circular(10),
),
child: Text(title, style: TextStyle(color: colors.surface)),
);
} }
Widget buildPrimaryButton({
required BuildContext context,
required String text,
required VoidCallback onPressed,
}) {
return ElevatedButton(
onPressed: () => onPressed(),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
elevation: 0,
),
child: Text(text, style: TextStyle(color: Colors.white)),
);
}
Widget buildErrorButton({
required String text,
required VoidCallback onPressed,
}) {
return ElevatedButton(
onPressed: () => onPressed(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
elevation: 0,
),
child: Text(text, style: TextStyle(color: Colors.white)),
);
}
Widget buildInfoButton({
required BuildContext context,
required String text,
required VoidCallback onPressed,
}) {
return ElevatedButton(
onPressed: () => onPressed(),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
elevation: 0,
),
child: Text(text),
);
}
/// 图片错误显示
Widget errorImageContainer(double height) { Widget errorImageContainer(double height) {
return Container( return Container(
height: height, height: 200,
width: double.infinity,
color: Colors.grey[200], color: Colors.grey[200],
child: Row( child: const Icon(Icons.image, color: Colors.grey, size: 30),
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, color: Colors.red),
const SizedBox(width: 5),
const Text("加载失败", style: TextStyle(color: Colors.red)),
],
),
);
}
/// 成功消息
void showSuccessToast(String message) {
Fluttertoast.showToast(
msg: message,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
timeInSecForIosWeb: 1,
backgroundColor: Colors.green,
textColor: Colors.white,
fontSize: 16.0,
);
}
/// 错误消息
void showErrorToast(String message) {
Fluttertoast.showToast(
msg: message,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
timeInSecForIosWeb: 1,
backgroundColor: Colors.red,
textColor: Colors.white,
fontSize: 16.0,
); );
} }

View File

@@ -0,0 +1,263 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/flutter_common.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:food_hub_app/config/app_config.dart';
import 'package:food_hub_app/models/moment.dart';
import 'package:food_hub_app/provider/food_provider.dart';
import 'package:food_hub_app/provider/user_provider.dart';
import 'package:food_hub_app/widgets/common/image.dart';
import 'package:provider/provider.dart';
class MomentCard extends StatefulWidget {
final Moment moment;
final bool isUser;
const MomentCard({super.key, required this.moment, required this.isUser});
@override
State<StatefulWidget> createState() => MomentCardState();
}
class MomentCardState extends State<MomentCard> {
@override
Widget build(BuildContext context) {
return CommonCard(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
onTap: () => _onTapUser(context),
child: _buildAvatar(context),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildNickname(context),
const SizedBox(height: 5),
_buildContent(context),
if (widget.moment.imageList.isNotEmpty) ...[
const SizedBox(height: 8),
_buildPostImages(),
],
const SizedBox(height: 10),
_buildTime(context),
const SizedBox(height: 5),
_buildActionButtons(),
if (widget.moment.commentList!.isNotEmpty) ...[
const SizedBox(height: 8),
_buildCommentList(),
],
],
),
),
],
),
);
}
void _onTapUser(BuildContext context) async {
final provider = context.read<UserProvider>();
if (widget.moment.userId != SPUtil.getInt('userId')) {
await provider.refreshUser(widget.moment.userId!);
await provider.queryMomentByUserId(widget.moment.userId!);
if (mounted) {
Navigator.pushNamed(context, '/profileUser');
}
}
}
// 构建头像组件
Widget _buildAvatar(BuildContext context) {
if (widget.moment.avatar!.isEmpty) {
return CircleAvatar(
radius: 18,
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.surface,
child: Text(widget.moment.username![0], style: TextStyle(fontSize: 14)),
);
} else {
return CircleAvatar(
radius: 18,
backgroundImage: NetworkImage('${AppConfig.imageBaseUrl}/${widget.moment.avatar}')
);
}
}
// 构建昵称组件
Widget _buildNickname(BuildContext context) {
return Text(
widget.moment.username ?? "",
style: Theme.of(context).textTheme.titleMedium,
);
}
// 构建内容组件
Widget _buildContent(BuildContext context) {
return Text(
widget.moment.content,
style: Theme.of(context).textTheme.bodyMedium,
);
}
// 构建朋友圈图片列表(网格布局)
Widget _buildPostImages() {
final imageCount = widget.moment.imageList.length;
// 计算网格列数
int crossAxisCount;
if (imageCount == 1) {
crossAxisCount = 1;
} else if (imageCount == 2 || imageCount == 4) {
crossAxisCount = 2;
} else {
crossAxisCount = 3;
}
// 计算宽高比
double itemAspectRatio = 1.0;
if (imageCount == 1) {
itemAspectRatio = 1.5;
}
// 生成图片URL列表用于预览时切换
final List<String> imageUrls =
widget.moment.imageList.map((path) => path).toList();
return GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: crossAxisCount,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
childAspectRatio: itemAspectRatio,
children: List.generate(imageCount, (index) {
return CommonImage(imageUrls: imageUrls, index: index);
}),
);
}
// 构建时间组件
Widget _buildTime(BuildContext context) {
return Text(
widget.moment.date ?? "",
style: Theme.of(context).textTheme.bodySmall,
);
}
void _onTapEdit() {
final provider = Provider.of<FoodProvider>(context, listen: false);
provider.isEditing = true;
provider.momentFormItem = widget.moment;
Navigator.pushNamed(context, '/momentForm');
}
// 构建点赞和评论按钮
Widget _buildActionButtons() {
return Row(
children: [
if (widget.isUser)
SizedBox(
width: 42,
height: 36,
child: Stack(
alignment: Alignment.bottomLeft,
children: [
InkWell(onTap: () => _onTapEdit(), child: Icon(Icons.edit)),
],
),
),
SizedBox(
width: 42,
height: 36,
child: Stack(
alignment: Alignment.bottomLeft,
children: [
Icon(Icons.thumb_up_alt_outlined),
// Positioned(
// left: 20,
// bottom: 15,
// child: TDBadge(
// TDBadgeType.message,
// count: (widget.moment.likeList?.length ?? 0).toString(),
// ),
// ),
],
),
),
SizedBox(
width: 42,
height: 36,
child: Stack(
alignment: Alignment.bottomLeft,
children: [
Icon(Icons.comment_outlined),
// Positioned(
// left: 20,
// bottom: 15,
// child: TDBadge(
// TDBadgeType.message,
// count: (widget.moment.commentList?.length ?? 0).toString(),
// ),
// ),
],
),
),
],
);
}
Widget _buildCommentItem(Comment comment) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: TextSpan(
children: [
TextSpan(
text: comment.username,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
fontFamily: 'CustomFont',
color: Colors.black,
),
),
const TextSpan(text: ': ', style: TextStyle(color: Colors.black)),
TextSpan(
text: comment.content,
style: const TextStyle(
fontSize: 14,
fontFamily: 'CustomFont',
color: Colors.black87,
),
),
],
),
),
],
);
}
Widget _buildCommentList() {
return Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: widget.moment.commentList!.length,
separatorBuilder: (context, index) => SizedBox(height: 8),
itemBuilder: (context, index) {
return _buildCommentItem(widget.moment.commentList![index]);
},
),
);
}
}

View File

@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/models/moment.dart';
import 'package:food_hub_app/widgets/moment/card.dart';
class MomentList extends StatelessWidget {
final List<Moment> momentList;
final bool isUser;
const MomentList({super.key, required this.momentList, required this.isUser});
@override
Widget build(BuildContext context) {
return ListView.separated(
itemCount: momentList.length,
separatorBuilder: (context, index) => SizedBox(height: 8),
itemBuilder: (context, index) {
return MomentCard(moment: momentList[index], isUser: isUser);
},
);
}
}

View File

@@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/utils/date_utils.dart';
import 'package:food_hub_app/config/app_config.dart';
import 'package:food_hub_app/models/session.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
import 'package:liquid_glass_widgets/widgets/containers/glass_card.dart';
class ProfileInfo extends StatelessWidget {
final User user;
const ProfileInfo({super.key, required this.user});
Widget _buildAvatar(BuildContext context, User user) {
if (user.avatar == null) {
return CircleAvatar(
radius: 24,
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.surface,
child: Text(
user.username.isNotEmpty ? user.username[0] : '?',
style: TextStyle(fontSize: 14),
),
);
} else {
return CircleAvatar(
radius: 24,
backgroundImage: NetworkImage(
'${AppConfig.imageBaseUrl}/${user.avatar}',
),
);
}
}
Widget _buildInfoItem({
required BuildContext context,
required IconData icon,
required String text,
}) {
final colors = Theme.of(context).colorScheme;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 16, color: colors.primary),
const SizedBox(width: 6),
Text(text, style: Theme.of(context).textTheme.bodySmall),
],
);
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return GlassCard(
padding: EdgeInsetsGeometry.all(10),
useOwnLayer: true,
settings: LiquidGlassSettings(glassColor: colors.surface),
child: Column(
children: [
_buildAvatar(context, user),
const SizedBox(height: 8),
Text(user.username, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
if (user.tags != null && user.tags!.isNotEmpty)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children:
user.tags!.map((tag) => buildTag(context, tag)).toList(),
),
if (user.tags != null && user.tags!.isNotEmpty)
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
if (user.area != null && user.area!.length >= 2)
_buildInfoItem(
context: context,
icon: Icons.location_on,
text: '${user.area![0]}-${user.area![1]}',
),
if (user.birthDate != null)
_buildInfoItem(
context: context,
icon: Icons.cake,
text: formatDate(user.birthDate!),
),
if (user.job != null && user.job!.isNotEmpty)
_buildInfoItem(
context: context,
icon: Icons.work,
text: user.job!,
),
if (user.phoneNumber != null && user.phoneNumber!.isNotEmpty)
_buildInfoItem(
context: context,
icon: Icons.phone_android,
text: user.phoneNumber!,
),
if (user.email != null && user.email!.isNotEmpty)
_buildInfoItem(
context: context,
icon: Icons.email,
text: user.email!,
),
],
),
if (user.description != null && user.description!.isNotEmpty)
const SizedBox(height: 8),
if (user.description != null && user.description!.isNotEmpty)
Text(user.description!),
],
),
);
}
}

View File

@@ -0,0 +1,226 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/provider/food_provider.dart';
import 'package:food_hub_app/utils/index.dart';
import 'package:food_hub_app/views/record_form.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:provider/provider.dart';
class RecipeCalendar extends StatefulWidget {
const RecipeCalendar({super.key});
@override
State<RecipeCalendar> createState() => _RecipeCalendarState();
}
class _RecipeCalendarState extends State<RecipeCalendar> {
TableCalendar _recipeCalendar(FoodProvider provider) {
return TableCalendar(
locale: 'zh_CN',
headerStyle: const HeaderStyle(
headerPadding: EdgeInsets.symmetric(vertical: 0),
formatButtonVisible: false,
titleCentered: true,
),
firstDay: DateTime.utc(2010, 1, 1),
lastDay: DateTime.utc(2100, 12, 31),
focusedDay: provider.focusedDay,
selectedDayPredicate: (day) => isSameDay(provider.selectedDay, day),
onDaySelected: (selectedDay, focusedDay) {
if (!isSameDay(provider.selectedDay, selectedDay)) {
setState(() {
provider.selectedDay = selectedDay;
provider.focusedDay = focusedDay;
provider.refreshSelectRecord();
});
}
},
onPageChanged: (focusedDay) {
provider.focusedDay = focusedDay;
provider.refreshRecordList();
},
// 自定义日期单元格构建器
calendarBuilders: CalendarBuilders(
defaultBuilder:
(context, day, focusedDay) =>
_buildDateItem(day, false, false, provider),
selectedBuilder:
(context, day, focusedDay) =>
_buildDateItem(day, true, false, provider),
todayBuilder:
(context, day, focusedDay) =>
_buildDateItem(day, false, true, provider),
),
);
}
Widget _dailyItem(BuildContext context, FoodProvider provider) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(formatDateTime(provider.selectedDay, 'MM月dd日 EEEE')),
SizedBox(height: 4),
if (provider.selectRecordList.isEmpty)
SizedBox(height: 100, child: buildEmptyData())
else
Column(
children:
provider.selectRecordList.map((record) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _buildRecordItem(context, record),
);
}).toList(),
),
],
);
}
Widget _buildRecordItemAvatar(String name, Color color) {
return CircleAvatar(
radius: 20,
backgroundColor: color,
child: Text(
name,
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
);
}
Widget _buildRecordItemBody(String name, String category) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(name, style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 5),
Text(category, style: TextStyle(color: Colors.grey)),
],
);
}
void _handleEditRecord(BuildContext context, FoodRecord record) {
final provider = context.read<FoodProvider>();
provider.initRecordForm(record);
provider.isEditing = true;
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => RecordForm(),
);
}
Widget _buildRecordItem(BuildContext context, FoodRecord record) {
final colors = Theme.of(context).colorScheme;
return Card(
elevation: 0,
color: colors.primary.withAlpha(50),
margin: EdgeInsets.zero,
child: Padding(
padding: EdgeInsets.all(10),
child: Row(
children: [
_buildRecordItemAvatar(record.category[0], colors.primary),
SizedBox(width: 10),
Expanded(child: _buildRecordItemBody(record.name, record.category)),
circleIconButton(
context: context,
icon: Icons.chevron_right,
onPressed: () => _handleEditRecord(context, record),
),
],
),
),
);
}
Widget _buildDateItem(
DateTime day,
bool isSelected,
bool isToday,
FoodProvider provider,
) {
final colors = Theme.of(context).colorScheme;
// 检查当前日期是否在需要显示红点的列表中
bool shouldShowRedDot = provider.recordList.any(
(item) => item.date == formatDateTime(day),
);
late Color boxColor;
if (isSelected) {
boxColor = colors.secondary;
} else {
boxColor = isToday ? colors.primary : Colors.transparent;
}
late Color textColor;
if (isSelected) {
textColor = colors.surface;
} else {
textColor = isToday ? colors.surface : colors.onSurface;
}
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(color: boxColor, shape: BoxShape.circle),
child: Stack(
alignment: Alignment.center,
children: [
// 日期数字始终居中
Text(day.day.toString(), style: TextStyle(color: textColor)),
// 红点
if (shouldShowRedDot)
Align(
alignment: Alignment(0, 0.8),
child: Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
),
],
),
);
}
Widget _buildContent(FoodProvider provider) {
return SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 80),
child: Column(
children: [
CommonCard(child: _recipeCalendar(provider)),
const SizedBox(height: 8),
CommonCard(child: _dailyItem(context, provider)),
],
),
);
}
@override
Widget build(BuildContext context) {
final provider = context.watch<FoodProvider>();
if (provider.isLoading) {
return buildLoadingIndicator();
}
return _buildContent(provider);
}
}

View File

@@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/widgets/common/image.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
class RecipeCard extends StatelessWidget {
final RecipeSummary recipe;
const RecipeCard({super.key, required this.recipe});
void navigatorToRecipeDetail(BuildContext context) {
Navigator.pushNamed(context, '/recipeDetail', arguments: {'id': recipe.id});
}
@override
Widget build(BuildContext context) {
final List<String> imageUrls =
recipe.recordList.reversed.map((record) => record.imageUrl).toList();
final colors = Theme.of(context).colorScheme;
return GlassCard(
padding: EdgeInsetsGeometry.all(10),
useOwnLayer: true,
settings: LiquidGlassSettings(glassColor: colors.surface),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CommonImage(imageUrls: imageUrls, index: 0),
InkWell(
onTap: () => navigatorToRecipeDetail(context),
child: _buildRecipeContent(context),
),
],
),
);
}
Widget _buildRecipeContent(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
recipe.name,
style: textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
_buildIconText(
icon: Icons.star,
text: recipe.recommendRate.toString(),
color: colors.primary,
),
],
),
SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildIconText(
icon: Icons.food_bank,
text: recipe.category,
color: colors.primary,
),
const SizedBox(width: 12),
_buildIconText(
icon: Icons.note_add,
text: '${recipe.recordList.length.toString()}',
color: colors.primary,
),
],
),
],
);
}
// 通用图标文本组件
Widget _buildIconText({
required IconData icon,
required String text,
Color color = Colors.grey,
double iconSize = 16,
double textSize = 16,
double spacing = 2,
}) {
return Row(
children: [
Icon(icon, size: iconSize, color: color),
SizedBox(width: spacing),
Text(text, style: TextStyle(fontSize: textSize, color: color)),
],
);
}
}

View File

@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:food_hub_app/provider/food_provider.dart';
import 'package:food_hub_app/widgets/recipe/card.dart';
import 'package:provider/provider.dart';
class RecipeList extends StatefulWidget {
final int userId;
const RecipeList({super.key, required this.userId});
@override
State<RecipeList> createState() => _RecipeListState();
}
class _RecipeListState extends State<RecipeList> {
@override
void initState() {
super.initState();
// 初始化加载数据
WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.userId == 0) {
context.read<FoodProvider>().refreshRecipeList();
} else {
context.read<FoodProvider>().queryRecipeByUserId(widget.userId);
}
});
}
Widget _buildRecipeList(FoodProvider provider) {
return GridView.builder(
padding: const EdgeInsets.only(bottom: 80),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 0.85,
),
itemCount: provider.recipeSummaryList.length,
itemBuilder: (context, index) {
return RecipeCard(recipe: provider.recipeSummaryList[index]);
},
);
}
Widget _buildContent(FoodProvider provider) {
if (provider.recipeSummaryList.isEmpty) {
return buildEmptyData();
} else {
return Column(
children: [
Expanded(child: _buildRecipeList(provider)),
],
);
}
}
@override
Widget build(BuildContext context) {
final provider = context.watch<FoodProvider>();
return Stack(
children: [
if (provider.isLoading)
buildLoadingIndicator()
else
_buildContent(provider),
],
);
}
}

View File

@@ -1,215 +0,0 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/api/recipe.dart';
import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/utils/date_util.dart';
import 'package:food_hub_app/utils/index.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
class RecipeCalendar extends StatefulWidget {
const RecipeCalendar({super.key});
@override
State<RecipeCalendar> createState() => _RecipeCalendarState();
}
class _RecipeCalendarState extends State<RecipeCalendar> {
DateTime _selectedDay = DateTime.now();
DateTime _focusedDay = DateTime.now();
List<Record> recordList = [];
List<Record> selectRecordList = [];
@override
void initState() {
super.initState();
refreshRecord();
}
Future<void> refreshRecord() async {
final result = await queryRecordApi(
getFirstDayOfMonth(_focusedDay),
getLastDayOfMonth(_focusedDay),
);
setState(() {
recordList = result;
if (recordList.isNotEmpty) {
_selectedDay = DateTime.parse(recordList.last.date);
} else {
_selectedDay = _focusedDay;
}
refreshSelectRecord();
});
}
void refreshSelectRecord() {
setState(() {
selectRecordList =
recordList
.where((record) => record.date == formatDateTime(_selectedDay))
.toList();
});
}
TableCalendar recipeCalendar() {
return TableCalendar(
locale: 'zh_CN',
headerStyle: const HeaderStyle(
formatButtonVisible: false,
titleCentered: true,
),
firstDay: DateTime.utc(2010, 1, 1),
lastDay: DateTime.utc(2100, 12, 31),
focusedDay: _focusedDay,
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
onDaySelected: (selectedDay, focusedDay) {
if (!isSameDay(_selectedDay, selectedDay)) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
refreshSelectRecord();
});
}
},
onPageChanged: (focusedDay) {
_focusedDay = focusedDay;
refreshRecord();
},
// 自定义日期单元格构建器
calendarBuilders: CalendarBuilders(
defaultBuilder:
(context, day, focusedDay) => _buildDateWidget(day, false, false),
selectedBuilder:
(context, day, focusedDay) => _buildDateWidget(day, true, false),
todayBuilder:
(context, day, focusedDay) => _buildDateWidget(day, false, true),
),
);
}
Widget dailyItem() {
return Card(
elevation: 0,
color: Colors.white,
child: Padding(
padding: EdgeInsets.all(5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(formatDateTime(_selectedDay, 'MM月dd日 EEEE')),
if (selectRecordList.isEmpty)
TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据')
else
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: selectRecordList.length,
itemBuilder: (context, index) {
return recipeRecordItem(selectRecordList[index]);
},
),
],
),
),
);
}
Widget recipeRecordItem(Record record) {
return Card(
elevation: 0,
color: Color(0xFFF5F5DC),
child: Padding(
padding: EdgeInsets.all(10),
child: Row(
children: [
TDAvatar(
size: TDAvatarSize.medium,
type: TDAvatarType.customText,
text: record.category[0],
),
SizedBox(width: 10),
Expanded(
// 使用Expanded让文本区域占据剩余空间
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
record.name,
style: TextStyle(fontWeight: FontWeight.bold),
),
SizedBox(height: 5),
Text(record.category, style: TextStyle(color: Colors.grey)),
],
),
),
SizedBox(width: 10),
TDButton(
icon: TDIcons.arrow_right,
type: TDButtonType.fill,
shape: TDButtonShape.circle,
theme: TDButtonTheme.primary,
),
],
),
),
);
}
Widget _buildDateWidget(DateTime day, bool isSelected, bool isToday) {
// 检查当前日期是否在需要显示红点的列表中
bool shouldShowRedDot = recordList.any(
(item) => item.date == formatDateTime(day),
);
return Container(
width: 40,
height: 40,
margin: EdgeInsets.all(2),
decoration: BoxDecoration(
color:
isSelected
? Colors.blue
: isToday
? Colors.grey[200]
: Colors.transparent,
shape: BoxShape.circle,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
// 日期数字
Text(
day.day.toString(),
style: TextStyle(color: isSelected ? Colors.white : Colors.black87),
),
// 底部红点 - 只在指定日期显示
if (shouldShowRedDot)
Container(
margin: EdgeInsets.only(top: 2),
width: 6,
height: 6,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Card(elevation: 0, color: Colors.white, child: recipeCalendar()),
const SizedBox(height: 10),
Expanded(child: dailyItem()),
],
);
}
}

View File

@@ -1,146 +0,0 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
class RecipeCard extends StatelessWidget {
final Recipe recipe;
const RecipeCard({super.key, required this.recipe});
@override
Widget build(BuildContext context) {
return Card(
elevation: 0,
color: Colors.white,
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
'http://172.29.101.108:8100/${recipe.recordList[0].imageUrl}',
height: 200,
width: double.infinity,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) => errorImageContainer(200),
),
Divider(
height: 1,
thickness: 1,
color: Theme.of(context).primaryColor,
indent: 0,
endIndent: 0,
),
Padding(
padding: const EdgeInsets.all(10),
child: _buildRecipeContent(context),
),
],
),
);
}
Widget _buildRecipeContent(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
recipe.name,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
maxLines: 1, // 限制单行,避免挤压按钮
overflow: TextOverflow.ellipsis,
),
),
Row(
children: [
TDButton(
icon: Icons.edit,
size: TDButtonSize.small,
type: TDButtonType.fill,
shape: TDButtonShape.circle,
theme: TDButtonTheme.primary,
onTap: () => {},
),
SizedBox(width: 10),
TDButton(
icon: Icons.book,
size: TDButtonSize.small,
type: TDButtonType.fill,
shape: TDButtonShape.circle,
theme: TDButtonTheme.primary,
onTap: () => {},
),
],
),
],
),
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
children: [
_buildIconText(
icon: Icons.food_bank,
text: recipe.category,
color: Theme.of(context).primaryColor,
),
const SizedBox(width: 12),
_buildIconText(
icon: Icons.thumb_up_alt_outlined,
text: recipe.likeCount.toString(),
color: Colors.red,
),
const SizedBox(width: 12),
_buildIconText(
icon: Icons.star_outline,
text: recipe.favouriteCount.toString(),
color: Colors.blue,
),
const SizedBox(width: 12),
_buildIconText(
icon: Icons.comment_outlined,
text: recipe.commentCount.toString(),
color: Colors.deepOrange,
),
],
),
Row(
children: [
_buildIconText(
icon: Icons.date_range,
text: recipe.recordList[0].date,
color: Colors.red,
),
],
),
],
),
],
);
}
// 通用图标文本组件
Widget _buildIconText({
required IconData icon,
required String text,
Color color = Colors.grey, // 默认灰色
double iconSize = 16,
double textSize = 16,
double spacing = 2, // 图标与文本间距
}) {
return Row(
children: [
Icon(icon, size: iconSize, color: color),
SizedBox(width: spacing),
Text(text, style: TextStyle(fontSize: textSize, color: color)),
],
);
}
}

View File

@@ -1,48 +0,0 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/api/recipe.dart';
import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/widgets/recipe/recipe_card.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
class RecipeList extends StatefulWidget {
const RecipeList({super.key});
@override
State<RecipeList> createState() => _RecipeListState();
}
class _RecipeListState extends State<RecipeList> {
List<Recipe> recipeList = [];
@override
void initState() {
super.initState();
refreshRecipeList();
}
Future<void> refreshRecipeList() async {
final result = await queryRecipeApi(RecipeQuery(category: ""));
setState(() {
recipeList = result;
});
}
@override
Widget build(BuildContext context) {
if (recipeList.isEmpty) {
return const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据');
} else {
return ListView.separated(
itemCount: recipeList.length,
itemBuilder: (context, index) {
return RecipeCard(recipe: recipeList[index]);
},
separatorBuilder: (context, index) {
return const SizedBox(height: 10);
},
);
}
}
}

View File

@@ -1,248 +0,0 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/api/recipe.dart';
import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
import 'package:timelines_plus/timelines_plus.dart';
class RecipeTimeline extends StatefulWidget {
const RecipeTimeline({super.key});
@override
State<StatefulWidget> createState() => _RecipeTimeline();
}
class _RecipeTimeline extends State<RecipeTimeline> {
List<Record> recordList = [];
@override
void initState() {
super.initState();
refreshRecord();
}
Future<void> refreshRecord() async {
final result = await queryRecordApi("2025-01-01", "2025-07-08");
setState(() {
recordList = result;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
YearSelector(
initialYear: DateTime.now().year,
minYear: 2000,
maxYear: 2100,
onYearChanged: (year) {
print('选中的年份: $year');
},
accentColor: Colors.blue,
),
Expanded(child: TimelineContainer(recordList))
],
);
}
}
Widget TimelineContainer(List<Record> recordList) {
if (recordList.isEmpty) {
return const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据');
} else {
return Padding(
padding: EdgeInsets.all(10),
child: Timeline.tileBuilder(
theme: TimelineThemeData(nodePosition: 0, indicatorPosition: 0),
builder: TimelineTileBuilder.connected(
itemCount: recordList.length,
connectorBuilder:
(context, index, type) => Connector.solidLine(thickness: 2),
indicatorBuilder: (context, index) {
return Indicator.dot(size: 12.0);
},
contentsBuilder: (context, index) {
return TimelineCard(record: recordList[index]);
},
),
),
);
}
}
class TimelineCard extends StatelessWidget {
final Record record;
const TimelineCard({super.key, required this.record});
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
child: Padding(
padding: const EdgeInsets.only(left: 10, bottom: 5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
record.date,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
Card(
elevation: 0,
color: Colors.white,
child: Padding(
padding: EdgeInsets.all(10),
child: Column(
children: [
Text(record.name, style: TextStyle(fontSize: 16)),
const SizedBox(height: 5),
Image.network(
'http://172.29.101.108:8100/${record.imageUrl}',
width: double.infinity,
height: 250,
fit: BoxFit.contain,
errorBuilder:
(context, error, stackTrace) =>
errorImageContainer(250),
),
],
),
),
),
],
),
),
);
}
}
class YearSelector extends StatefulWidget {
final int initialYear;
final int? minYear;
final int? maxYear;
final Function(int) onYearChanged;
final Color? accentColor;
const YearSelector({
super.key,
required this.initialYear,
required this.onYearChanged,
this.minYear,
this.maxYear,
this.accentColor,
});
@override
State<YearSelector> createState() => _YearSelectorState();
}
class _YearSelectorState extends State<YearSelector>
with SingleTickerProviderStateMixin {
late int _currentYear;
late Color _accentColor;
// 用于动画效果
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_currentYear = widget.initialYear;
_accentColor = widget.accentColor ?? Theme
.of(context)
.primaryColor;
// 初始化动画控制器
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
// 缩放动画
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.1).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
/// 切换到上一年
void _previousYear() {
if (widget.minYear == null || _currentYear > widget.minYear!) {
_animateYearChange(() {
setState(() {
_currentYear--;
});
widget.onYearChanged(_currentYear);
});
}
}
/// 切换到下一年
void _nextYear() {
if (widget.maxYear == null || _currentYear < widget.maxYear!) {
_animateYearChange(() {
setState(() {
_currentYear++;
});
widget.onYearChanged(_currentYear);
});
}
}
/// 年份变化时的动画效果
void _animateYearChange(VoidCallback onComplete) {
_animationController.forward().then((_) {
onComplete();
_animationController.reverse();
});
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TDButton(
icon: Icons.chevron_left,
size: TDButtonSize.small,
type: TDButtonType.fill,
shape: TDButtonShape.circle,
theme: TDButtonTheme.primary,
onTap: () => _previousYear(),
),
SizedBox(width: 10),
// 年份显示
AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(scale: _scaleAnimation.value, child: child);
},
child: Text(
'$_currentYear',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: _accentColor,
),
),
),
TDButton(
icon: Icons.chevron_right,
size: TDButtonSize.small,
type: TDButtonType.fill,
shape: TDButtonShape.circle,
theme: TDButtonTheme.primary,
onTap: () => _nextYear(),
),
],
);
}
}

View File

@@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:flutter_common/widget/year_selector.dart';
import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/provider/food_provider.dart';
import 'package:food_hub_app/widgets/common/image.dart';
import 'package:timelines_plus/timelines_plus.dart';
import 'package:provider/provider.dart';
class RecipeTimeline extends StatefulWidget {
const RecipeTimeline({super.key});
@override
State<StatefulWidget> createState() => _RecipeTimeline();
}
class _RecipeTimeline extends State<RecipeTimeline> {
Widget buildTimelineCard(BuildContext context, FoodRecord record) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.date_range,
color: Theme.of(context).colorScheme.primary,
),
SizedBox(width: 5),
Text(
record.date,
style: Theme.of(context).textTheme.titleMedium,
),
],
),
SizedBox(height: 8),
CommonCard(
child: Column(
children: [
Text(
record.name,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 5),
CommonImage(imageUrls: [record.imageUrl], index: 0),
],
),
),
SizedBox(height: 20)
],
);
}
Widget buildTimeline(BuildContext context, List<FoodRecord> recordList) {
if (recordList.isEmpty) {
return buildEmptyData();
} else {
final colors = Theme.of(context).colorScheme;
return Timeline.tileBuilder(
padding: const EdgeInsets.only(bottom: 80),
theme: TimelineThemeData(nodePosition: 0, indicatorPosition: 0),
builder: TimelineTileBuilder.connected(
itemCount: recordList.length,
connectorBuilder:
(context, index, type) =>
Connector.solidLine(thickness: 2, color: colors.primary),
indicatorBuilder: (context, index) {
return Indicator.dot(size: 12.0, color: colors.primary);
},
contentsBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 8),
child: buildTimelineCard(context, recordList[index]),
);
},
),
);
}
}
Widget _buildContent(FoodProvider provider) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
YearSelector(
currentYear: provider.selectYear,
onYearChanged: (year) {
provider.selectYear = year;
provider.refreshRecordList();
},
),
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 0, horizontal: 10),
child: buildTimeline(context, provider.recordList),
),
),
],
);
}
@override
Widget build(BuildContext context) {
final provider = context.watch<FoodProvider>();
return Stack(
children: [
if (provider.isLoading)
buildLoadingIndicator()
else
_buildContent(provider),
],
);
}
}

View File

@@ -6,10 +6,14 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h> #include <rive_native/rive_native_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = g_autoptr(FlPluginRegistrar) rive_native_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "RiveNativePlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar); rive_native_plugin_register_with_registrar(rive_native_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
} }

View File

@@ -3,7 +3,8 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux rive_native
url_launcher_linux
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@@ -5,10 +5,20 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import file_selector_macos import file_picker
import flutter_image_compress_macos
import path_provider_foundation
import rive_native
import share_plus
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
RiveNativePlugin.register(with: registry.registrar(forPlugin: "RiveNativePlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
} }

View File

@@ -33,6 +33,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.12.0" version: "2.12.0"
awesome_dialog:
dependency: transitive
description:
name: awesome_dialog
sha256: "4c5821a0a637ceee022084e78c1b8237dd4b8bfca4dd24ac2484662a56707338"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.3.0"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -41,6 +49,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
buffer:
dependency: transitive
description:
name: buffer
sha256: "389da2ec2c16283c8787e0adaede82b1842102f8c8aae2f49003a766c5c6b3d1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.3"
build: build:
dependency: transitive dependency: transitive
description: description:
@@ -105,6 +121,30 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "8.10.1" version: "8.10.1"
cached_network_image:
dependency: "direct main"
description:
name: cached_network_image
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.4.1"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.1.1"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.1"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@@ -165,10 +205,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: crypto name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.6" version: "3.0.7"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -185,38 +225,54 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.1.0" version: "3.1.0"
dio: dbus:
dependency: "direct main" dependency: transitive
description: description:
name: dio name: dbus
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "5.8.0+1" version: "0.7.11"
dio:
dependency: transitive
description:
name: dio
sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.9.2"
dio_web_adapter: dio_web_adapter:
dependency: transitive dependency: transitive
description: description:
name: dio_web_adapter name: dio_web_adapter
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
easy_refresh: easy_refresh:
dependency: transitive dependency: "direct main"
description: description:
name: easy_refresh name: easy_refresh
sha256: "486e30abfcaae66c0f2c2798a10de2298eb9dc5e0bb7e1dba9328308968cae0c" sha256: "486e30abfcaae66c0f2c2798a10de2298eb9dc5e0bb7e1dba9328308968cae0c"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.4.0" version: "3.4.0"
equatable:
dependency: transitive
description:
name: equatable
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.8"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
name: fake_async name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.3.2" version: "1.3.3"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
@@ -233,6 +289,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "7.0.1" version: "7.0.1"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: f8f4ea435f791ab1f817b4e338ed958cb3d04ba43d6736ffc39958d950754967
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.3.6"
file_selector_linux: file_selector_linux:
dependency: transitive dependency: transitive
description: description:
@@ -278,6 +342,29 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_cache_manager:
dependency: transitive
description:
name: flutter_cache_manager
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.4.1"
flutter_carousel_widget:
dependency: "direct main"
description:
name: flutter_carousel_widget
sha256: "6473e6df04bfafea70efd58251fe5945d5aa8d19461575c1b9d83643f08e0c77"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.0"
flutter_common:
dependency: "direct main"
description:
path: "../flutter_common"
relative: true
source: path
version: "1.0.0+1"
flutter_form_builder: flutter_form_builder:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -286,6 +373,62 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "10.0.1" version: "10.0.1"
flutter_image_compress:
dependency: transitive
description:
name: flutter_image_compress
sha256: "51d23be39efc2185e72e290042a0da41aed70b14ef97db362a6b5368d0523b27"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.0"
flutter_image_compress_common:
dependency: transitive
description:
name: flutter_image_compress_common
sha256: c5c5d50c15e97dd7dc72ff96bd7077b9f791932f2076c5c5b6c43f2c88607bfb
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.6"
flutter_image_compress_macos:
dependency: transitive
description:
name: flutter_image_compress_macos
sha256: "20019719b71b743aba0ef874ed29c50747461e5e8438980dfa5c2031898f7337"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.3"
flutter_image_compress_ohos:
dependency: transitive
description:
name: flutter_image_compress_ohos
sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.0.3"
flutter_image_compress_platform_interface:
dependency: transitive
description:
name: flutter_image_compress_platform_interface
sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.5"
flutter_image_compress_web:
dependency: transitive
description:
name: flutter_image_compress_web
sha256: b9b141ac7c686a2ce7bb9a98176321e1182c9074650e47bb140741a44b6f5a96
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.1.5"
flutter_input_chips:
dependency: "direct main"
description:
name: flutter_input_chips
sha256: "4b45df0c8b80a23db86850d8f892144dc796f3f6eb13ebd8bcb2d242ec921e22"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -307,22 +450,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.0.28" version: "2.0.28"
flutter_slidable: flutter_shaders:
dependency: transitive dependency: transitive
description: description:
name: flutter_slidable name: flutter_shaders
sha256: a857de7ea701f276fd6a6c4c67ae885b60729a3449e42766bb0e655171042801 sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.1.2" version: "0.1.3"
flutter_swiper_null_safety:
dependency: transitive
description:
name: flutter_swiper_null_safety
sha256: "5a855e0080d035c08e82f8b7fd2f106344943a30c9ab483b2584860a2f22eaaf"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.2"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -334,13 +469,13 @@ packages:
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
fluttertoast: fluttertoast:
dependency: "direct main" dependency: transitive
description: description:
name: fluttertoast name: fluttertoast
sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1" sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "8.2.12" version: "8.2.14"
form_builder_validators: form_builder_validators:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -365,14 +500,6 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.3" version: "2.1.3"
graphic:
dependency: "direct main"
description:
name: graphic
sha256: af3a5a967d95ce2c2c9f7dee83c21f8bd6e6a458341820920cf15c0311cdaddc
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.6.0"
graphs: graphs:
dependency: transitive dependency: transitive
description: description:
@@ -473,10 +600,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: intl name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.19.0" version: "0.20.2"
io: io:
dependency: transitive dependency: transitive
description: description:
@@ -513,26 +640,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "10.0.8" version: "11.0.2"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.9" version: "3.0.10"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_testing name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.1" version: "3.0.2"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@@ -541,6 +668,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "5.1.1" version: "5.1.1"
liquid_glass_widgets:
dependency: "direct main"
description:
name: liquid_glass_widgets
sha256: "776adcdb7d48af0b935642425936813074355830eb0a47ecca8b227549d5b057"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.16.3"
logger: logger:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -577,10 +712,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.16.0" version: "1.17.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -589,6 +724,30 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
minio:
dependency: transitive
description:
name: minio
sha256: ee2ce47766e46c7d164f960f2f5ed6a9a82844d877f6b82574f6876ec50c56d1
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.5.8"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.0"
octo_image:
dependency: transitive
description:
name: octo_image
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
@@ -621,6 +780,30 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.19"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.2"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@@ -645,6 +828,22 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.0"
photo_view:
dependency: "direct main"
description:
name: photo_view
sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.15.0"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@@ -669,6 +868,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.5.1" version: "1.5.1"
provider:
dependency: "direct main"
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.5+1"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@@ -685,30 +892,70 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.5.0" version: "1.5.0"
shared_preferences: rive:
dependency: "direct main" dependency: transitive
description: description:
name: shared_preferences name: rive
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" sha256: "84a640f48122679e48397935ed1b971db19ef2e44bfcbae60e42fb5ce29a0a4e"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.5.3" version: "0.14.9"
rive_native:
dependency: transitive
description:
name: rive_native
sha256: "85dc8413398f7540eab9b73c6d704ecd5051ab9c57e539eed85ccd2a69f119aa"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.1.9"
rxdart:
dependency: transitive
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.28.0"
share_plus:
dependency: "direct main"
description:
name: share_plus
sha256: d7dc0630a923883c6328ca31b89aa682bacbf2f8304162d29f7c6aaff03a27a1
url: "https://pub.flutter-io.cn"
source: hosted
version: "11.1.0"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.0"
shared_preferences:
dependency: transitive
description:
name: shared_preferences
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.5.5"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.4.10" version: "2.4.23"
shared_preferences_foundation: shared_preferences_foundation:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_foundation name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.5.4" version: "2.5.6"
shared_preferences_linux: shared_preferences_linux:
dependency: transitive dependency: transitive
description: description:
@@ -721,10 +968,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_platform_interface name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.4.1" version: "2.4.2"
shared_preferences_web: shared_preferences_web:
dependency: transitive dependency: transitive
description: description:
@@ -794,6 +1041,46 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.10.1" version: "1.10.1"
sqflite:
dependency: transitive
description:
name: sqflite
sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.2+1"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.2+3"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "1581ffbf7a0e333b380d6a30737d78516b826cb35beb7fb0bf8a3ea0c678b465"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.5.8"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.2"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.0"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -826,22 +1113,38 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.4.1" version: "1.4.1"
syncfusion_flutter_charts:
dependency: transitive
description:
name: syncfusion_flutter_charts
sha256: "68fdb029dad34a46e4c9cfad8ad66fe29db7b303bd96849261ab2b23a168d0e8"
url: "https://pub.flutter-io.cn"
source: hosted
version: "30.2.7"
syncfusion_flutter_core:
dependency: transitive
description:
name: syncfusion_flutter_core
sha256: bfd026c0f9822b49ff26fed11cd3334519acb6a6ad4b0c81d9cd18df6af1c4c0
url: "https://pub.flutter-io.cn"
source: hosted
version: "30.2.7"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.4.0"
table_calendar: table_calendar:
dependency: "direct main" dependency: "direct main"
description: description:
name: table_calendar name: table_calendar
sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63 sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.1.3" version: "3.2.0"
tdesign_flutter:
dependency: "direct main"
description:
name: tdesign_flutter
sha256: b36b6f939f7a585184665202b6b3acf1922728962312b65182dbbff64c3b62f2
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.3"
tdesign_flutter_adaptation: tdesign_flutter_adaptation:
dependency: "direct overridden" dependency: "direct overridden"
description: description:
@@ -862,10 +1165,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.7.4" version: "0.7.7"
timelines_plus: timelines_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -882,6 +1185,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.0.2" version: "1.0.2"
toggle_switch:
dependency: "direct main"
description:
name: toggle_switch
sha256: dca04512d7c23ed320d6c5ede1211a404f177d54d353bf785b07d15546a86ce5
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.0"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -890,14 +1201,54 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.2.1"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.4"
uuid:
dependency: transitive
description:
name: uuid
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.5.2"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
name: vector_math name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.4" version: "2.2.0"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:
@@ -938,6 +1289,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.3" version: "3.0.3"
win32:
dependency: transitive
description:
name: win32
sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.13.0"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@@ -946,6 +1305,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.5.0"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:
@@ -955,5 +1322,5 @@ packages:
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.7.0 <4.0.0" dart: ">=3.10.0 <4.0.0"
flutter: ">=3.29.0" flutter: ">=3.38.0"

View File

@@ -1,113 +1 @@
name: food_hub_app name: food_hub_app
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: ^3.7.0
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
# The following adds the Cupertino Icons fonts to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
dio: ^5.7.0
timelines_plus: ^1.0.7
table_calendar: ^3.1.3
flutter_form_builder: ^10.0.0
form_builder_validators: ^11.1.2
graphic: ^2.6.0
intl: ^0.19.0
tdesign_flutter: ^0.2.3
json_annotation: ^4.9.0
fluttertoast: ^8.2.0
shared_preferences: ^2.3.0
logger: ^2.6.0
dependency_overrides:
tdesign_flutter_adaptation: 3.16.0
image_picker: 1.0.8
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
# flutter pub run build_runner build
build_runner: ^2.4.5
json_serializable: ^6.7.1
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons fonts is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the fonts family name, and a "fonts" key with a
# list giving the asset and other descriptors for the fonts. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
fonts:
- family: CustomFont
fonts:
- asset: fonts/custom.ttf

View File

@@ -6,9 +6,15 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h> #include <rive_native/rive_native_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar( RiveNativePluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows")); registry->GetRegistrarForPlugin("RiveNativePlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }

View File

@@ -3,7 +3,9 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows rive_native
share_plus
url_launcher_windows
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@@ -11,7 +11,7 @@ namespace {
/// ///
/// Redefined in case the developer's machine has a Windows SDK older than /// Redefined in case the developer's machine has a Windows SDK older than
/// version 10.0.22000.0. /// version 10.0.22000.0.
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute /// See: https://docs.microsoft.com/windows/win32/apis/dwmapi/ne-dwmapi-dwmwindowattribute
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 #define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#endif #endif