Compare commits
28 Commits
6bba9a488c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| a5b3053920 | |||
| 780526ddf4 | |||
| 9d3a3f2e80 | |||
| 613e50b0da | |||
| c10e8e5ffb | |||
| a340ba424e | |||
| a5e788eee6 | |||
| ed0ceb29d3 | |||
| 8cff5228ae | |||
| 97e7a6f44b | |||
| 245edd9d27 | |||
| 6308169013 | |||
| 1ac504f3d7 | |||
| 41fe9d6a24 | |||
| 515021781f | |||
| 4f033a4d0a | |||
| a9f2e102c5 | |||
| eb4f7a3637 | |||
| e6ee934a3e | |||
| 5151430132 | |||
| 07180965cc | |||
| 8acad9c63a | |||
| 625a872d2f | |||
| 7ede1c2d89 | |||
| b6995d8dd1 | |||
| 9a8e1a7419 | |||
| df26790389 | |||
| 21ff0ad94f |
15
.metadata
15
.metadata
@@ -15,21 +15,6 @@ migration:
|
||||
- platform: root
|
||||
create_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
|
||||
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
||||
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<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
|
||||
android:label="food_hub_app"
|
||||
android:label="食光集"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
|
||||
@@ -27,7 +27,7 @@ pluginManagement {
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
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")
|
||||
|
||||
@@ -1,28 +1,38 @@
|
||||
import 'package:flutter_common/utils/convert_utils.dart';
|
||||
import 'package:flutter_common/utils/http_utils.dart';
|
||||
import 'package:food_hub_app/config/app_config.dart';
|
||||
import 'package:food_hub_app/models/moment.dart';
|
||||
import 'package:food_hub_app/utils/http_util.dart';
|
||||
import 'package:food_hub_app/utils/index.dart';
|
||||
|
||||
final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl);
|
||||
|
||||
Future<bool> addMomentApi(Moment moment) {
|
||||
return HttpUtil().post<bool>("/moment", data: moment);
|
||||
return httpUtil.post<bool>("/moment", data: moment);
|
||||
}
|
||||
|
||||
Future<bool> updateMomentApi(int id, Moment moment) {
|
||||
return HttpUtil().put<bool>("/moment/$id", data: moment);
|
||||
return httpUtil.put<bool>("/moment/$id", data: moment);
|
||||
}
|
||||
|
||||
Future<bool> deleteMomentApi(int id) {
|
||||
return HttpUtil().delete<bool>("/moment/$id");
|
||||
return httpUtil.delete<bool>("/moment/$id");
|
||||
}
|
||||
|
||||
Future<List<Moment>> queryMomentListApi() {
|
||||
return HttpUtil().get<List<Moment>>(
|
||||
return httpUtil.get<List<Moment>>(
|
||||
"/moment",
|
||||
converter: (data) => convertListResponse<Moment>(data, Moment.fromJson),
|
||||
converter: (data) => convertList<Moment>(data, Moment.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
Future<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>(
|
||||
return httpUtil.get<PageMoment>(
|
||||
"/moment/page",
|
||||
queryParameters: {"currentPage": currentPage, "pageSize": pageSize},
|
||||
converter: (data) => PageMoment.fromJson(data),
|
||||
@@ -30,13 +40,13 @@ Future<PageMoment> queryMomentByPageApi(int currentPage, int pageSize) {
|
||||
}
|
||||
|
||||
Future<bool> addMomentCommentApi(int id, String content) {
|
||||
return HttpUtil().post<bool>("/moment/$id/comment", data: content);
|
||||
return httpUtil.post<bool>("/moment/$id/comment", data: content);
|
||||
}
|
||||
|
||||
Future<bool> addMomentLikeApi(int id) {
|
||||
return HttpUtil().post<bool>("/food/moment/$id/like");
|
||||
return httpUtil.post<bool>("/food/moment/$id/like");
|
||||
}
|
||||
|
||||
Future<bool> deleteMomentLikeApi(int id) {
|
||||
return HttpUtil().delete<bool>("/food/moment/$id/like");
|
||||
return httpUtil.delete<bool>("/food/moment/$id/like");
|
||||
}
|
||||
|
||||
@@ -1,100 +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/http_util.dart';
|
||||
import 'package:food_hub_app/utils/index.dart';
|
||||
|
||||
Future<RecipeDetail> queryRecipeByIdApi(int id) {
|
||||
return HttpUtil().get<RecipeDetail>(
|
||||
final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl);
|
||||
|
||||
Future<Recipe> queryRecipeByIdApi(int id) {
|
||||
return httpUtil.get<Recipe>(
|
||||
"/food/recipe/$id",
|
||||
converter: (data) => RecipeDetail.fromJson(data),
|
||||
converter: (data) => Recipe.fromJson(data),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> addRecipeApi(Recipe recipe) {
|
||||
return HttpUtil().post<bool>("/food/recipe", data: recipe);
|
||||
return httpUtil.post<bool>("/food/recipe", data: recipe);
|
||||
}
|
||||
|
||||
Future<bool> updateRecipeApi(int id, Recipe recipe) {
|
||||
return HttpUtil().put<bool>("/food/recipe/$id", data: recipe);
|
||||
return httpUtil.put<bool>("/food/recipe/$id", data: recipe);
|
||||
}
|
||||
|
||||
Future<bool> deleteRecipeApi(int id) {
|
||||
return HttpUtil().delete<bool>("/food/recipe/$id");
|
||||
return httpUtil.delete<bool>("/food/recipe/$id");
|
||||
}
|
||||
|
||||
Future<List<Recipe>> queryRecipeByUserApi(int id) {
|
||||
return HttpUtil().get<List<Recipe>>(
|
||||
Future<List<RecipeSummary>> queryRecipeByUserApi(int id) {
|
||||
return httpUtil.get<List<RecipeSummary>>(
|
||||
"/food/recipe/user/$id",
|
||||
converter: (data) => convertListResponse<Recipe>(data, Recipe.fromJson),
|
||||
converter: (data) => convertList<RecipeSummary>(data, RecipeSummary.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Recipe>> queryRecipeUserFavouriteApi() {
|
||||
return HttpUtil().get<List<Recipe>>(
|
||||
return httpUtil.get<List<Recipe>>(
|
||||
"/food/recipe/user/favourite",
|
||||
converter: (data) => convertListResponse<Recipe>(data, Recipe.fromJson),
|
||||
converter: (data) => convertList<Recipe>(data, Recipe.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<RecipeSummary>> queryRecipeApi(RecipeQuery recipeQuery) {
|
||||
return HttpUtil().get<List<RecipeSummary>>(
|
||||
return httpUtil.get<List<RecipeSummary>>(
|
||||
"/food/recipe",
|
||||
queryParameters: recipeQuery.toJson(),
|
||||
converter: (data) => convertListResponse<RecipeSummary>(data, RecipeSummary.fromJson),
|
||||
converter:
|
||||
(data) => convertList<RecipeSummary>(data, RecipeSummary.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<String>> queryFoodNameListApi() {
|
||||
return HttpUtil().get<List<String>>(
|
||||
return httpUtil.get<List<String>>(
|
||||
"/food/recipe/name",
|
||||
converter:
|
||||
(data) => convertListResponse<String>(data, (json) => json.toString()),
|
||||
converter: (data) => convertStringList(data),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> addRecordApi(Record record) {
|
||||
return HttpUtil().post<bool>("/food/record", data: record);
|
||||
Future<bool> addRecordApi(FoodRecord record) {
|
||||
return httpUtil.post<bool>("/food/record", data: record);
|
||||
}
|
||||
|
||||
Future<bool> updateRecordApi(int id, Record record) {
|
||||
return HttpUtil().put<bool>("/food/record/$id", data: record);
|
||||
Future<bool> 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");
|
||||
return httpUtil.delete<bool>("/food/record/$id");
|
||||
}
|
||||
|
||||
Future<bool> addRecipeCommentApi(int id, String content) {
|
||||
return HttpUtil().post<bool>("/food/recipe/$id/comment", data: content);
|
||||
return httpUtil.post<bool>("/food/recipe/$id/comment", data: content);
|
||||
}
|
||||
|
||||
Future<bool> addRecipeLikeApi(int id) {
|
||||
return HttpUtil().post<bool>("/food/recipe/$id/like");
|
||||
return httpUtil.post<bool>("/food/recipe/$id/like");
|
||||
}
|
||||
|
||||
Future<bool> deleteRecipeLikeApi(int id) {
|
||||
return HttpUtil().delete<bool>("/food/recipe/$id/like");
|
||||
return httpUtil.delete<bool>("/food/recipe/$id/like");
|
||||
}
|
||||
|
||||
Future<bool> addRecipeFavouriteApi(int id) {
|
||||
return HttpUtil().post<bool>("/food/recipe/$id/favourite");
|
||||
return httpUtil.post<bool>("/food/recipe/$id/favourite");
|
||||
}
|
||||
|
||||
Future<bool> deleteRecipeFavouriteApi(int id) {
|
||||
return HttpUtil().delete<bool>("/food/recipe/$id/like");
|
||||
return httpUtil.delete<bool>("/food/recipe/$id/like");
|
||||
}
|
||||
|
||||
Future<List<Record>> queryRecordApi(String startDate, String endDate) {
|
||||
return HttpUtil().get<List<Record>>(
|
||||
Future<List<FoodRecord>> queryRecordApi(String startDate, String endDate) {
|
||||
return httpUtil.get<List<FoodRecord>>(
|
||||
"/food/record",
|
||||
queryParameters: {"startDate": startDate, "endDate": endDate},
|
||||
converter: (data) => convertListResponse<Record>(data, Record.fromJson),
|
||||
converter: (data) => convertList<FoodRecord>(data, FoodRecord.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<String>> queryCategoryApi() {
|
||||
return HttpUtil().get<List<String>>(
|
||||
return httpUtil.get<List<String>>(
|
||||
"/food/category",
|
||||
converter:
|
||||
(data) => convertListResponse<String>(data, (json) => json.toString()),
|
||||
converter: (data) => convertListStringResponse(data),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import 'package:flutter_common/utils/http_utils.dart';
|
||||
import 'package:food_hub_app/config/app_config.dart';
|
||||
import 'package:food_hub_app/models/session.dart';
|
||||
import 'package:food_hub_app/utils/http_util.dart';
|
||||
|
||||
final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl);
|
||||
|
||||
Future<Session> loginApi(String username, String password) {
|
||||
return HttpUtil().post<Session>(
|
||||
return httpUtil.post<Session>(
|
||||
"/session",
|
||||
queryParameters: {"username": username, "password": password},
|
||||
converter: (data) => Session.fromJson(data),
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
import 'package:flutter_common/models/common_model.dart';
|
||||
import 'package:flutter_common/utils/convert_utils.dart';
|
||||
import 'package:flutter_common/utils/http_utils.dart';
|
||||
import 'package:food_hub_app/config/app_config.dart';
|
||||
import 'package:food_hub_app/models/stats.dart';
|
||||
import 'package:food_hub_app/utils/http_util.dart';
|
||||
import 'package:food_hub_app/utils/index.dart';
|
||||
|
||||
final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl);
|
||||
|
||||
Future<SummaryStats> queryStatsApi() {
|
||||
return HttpUtil().get<SummaryStats>(
|
||||
return httpUtil.get<SummaryStats>(
|
||||
"/food/stats",
|
||||
converter: (data) => SummaryStats.fromJson(data),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ChartData>> queryRecordStatsApi() {
|
||||
return HttpUtil().get<List<ChartData>>(
|
||||
return httpUtil.get<List<ChartData>>(
|
||||
"/food/stats/record",
|
||||
converter: (data) => convertListResponse<ChartData>(data, ChartData.fromJson),
|
||||
converter: (data) => convertList<ChartData>(data, ChartData.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ChartData>> queryCategoryStatsApi() {
|
||||
return HttpUtil().get<List<ChartData>>(
|
||||
return httpUtil.get<List<ChartData>>(
|
||||
"/food/stats/category",
|
||||
converter: (data) => convertListResponse<ChartData>(data, ChartData.fromJson),
|
||||
converter: (data) => convertList<ChartData>(data, ChartData.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ChartData>> queryRankStatsApi() {
|
||||
return HttpUtil().get<List<ChartData>>(
|
||||
return httpUtil.get<List<ChartData>>(
|
||||
"/food/stats/rank",
|
||||
converter: (data) => convertListResponse<ChartData>(data, ChartData.fromJson),
|
||||
converter: (data) => convertList<ChartData>(data, ChartData.fromJson),
|
||||
);
|
||||
}
|
||||
16
lib/apis/user.dart
Normal file
16
lib/apis/user.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:flutter_common/utils/http_utils.dart';
|
||||
import 'package:food_hub_app/config/app_config.dart';
|
||||
import 'package:food_hub_app/models/session.dart';
|
||||
|
||||
final httpUtil = HttpUtil(baseUrl: AppConfig.baseApiUrl);
|
||||
|
||||
Future<User> queryUserApi(int number) {
|
||||
return httpUtil.get<User>(
|
||||
"/user/$number",
|
||||
converter: (data) => User.fromJson(data),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> updateUserApi(int id, User user) {
|
||||
return httpUtil.put<bool>("/user/$id", data: user);
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
/// 应用信息
|
||||
class AppConfig {
|
||||
// 网络配置
|
||||
// http://192.168.1.3:8100
|
||||
// http://14.103.235.151:81/food-service
|
||||
// static const String baseApiUrl = "http://14.103.235.151:81/food-service";
|
||||
// static const String baseApiUrl = "http://192.168.1.3:8100";
|
||||
static const String baseApiUrl = "https://cxx0822.iepose.cn/food-api";
|
||||
// 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
105
lib/layout/app_actions.dart
Normal file
@@ -0,0 +1,105 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:food_hub_app/provider/food_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class AppActions extends StatefulWidget {
|
||||
final int pageIndex;
|
||||
|
||||
const AppActions({super.key, required this.pageIndex});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => AppActionsState();
|
||||
}
|
||||
|
||||
class AppActionsState extends State<AppActions> {
|
||||
Widget _buildBottomSheetBody(FoodProvider provider) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final provider = context.watch<FoodProvider>();
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'请选择操作',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (widget.pageIndex == 0)
|
||||
ListTile(
|
||||
leading: Icon(Icons.book, color: colors.primary),
|
||||
title: const Text('新增菜谱'),
|
||||
onTap: () => _handleAddRecipe(provider),
|
||||
),
|
||||
if (widget.pageIndex == 0)
|
||||
ListTile(
|
||||
leading: Icon(Icons.note_add, color: colors.primary),
|
||||
title: const Text('新增记录'),
|
||||
onTap: () => _handleAddRecord(provider),
|
||||
),
|
||||
if (widget.pageIndex == 2)
|
||||
ListTile(
|
||||
leading: Icon(Icons.group, color: colors.primary),
|
||||
title: const Text('发布朋友圈'),
|
||||
onTap: () => _handleAddMoment(provider),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _onPressAdd(FoodProvider provider) {
|
||||
if (widget.pageIndex == 0) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) => _buildBottomSheetBody(provider),
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.pageIndex == 2) {
|
||||
_handleAddMoment(provider);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleAddRecipe(FoodProvider provider) {
|
||||
// provider.resetRecordForm();
|
||||
Navigator.pop(context);
|
||||
Navigator.pushNamed(context, "/recipeForm");
|
||||
}
|
||||
|
||||
void _handleAddRecord(FoodProvider provider) {
|
||||
provider.resetRecordForm();
|
||||
provider.isEditing = false;
|
||||
Navigator.pop(context);
|
||||
Navigator.pushNamed(context, "/recordForm");
|
||||
}
|
||||
|
||||
void _handleAddMoment(FoodProvider provider) {
|
||||
provider.resetMomentForm();
|
||||
provider.isEditing = false;
|
||||
Navigator.pushNamed(context, "/momentForm");
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<FoodProvider>();
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.search, color: Colors.white),
|
||||
onPressed: () {
|
||||
// 搜索功能
|
||||
},
|
||||
),
|
||||
if (widget.pageIndex == 0 || widget.pageIndex == 2)
|
||||
IconButton(
|
||||
icon: Icon(Icons.add, color: Colors.white),
|
||||
onPressed: () => _onPressAdd(provider),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
44
lib/layout/app_drawer.dart
Normal file
44
lib/layout/app_drawer.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/layout/theme_layout.dart';
|
||||
|
||||
class AppDrawer extends StatefulWidget {
|
||||
const AppDrawer({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => AppDrawerState();
|
||||
}
|
||||
|
||||
class AppDrawerState extends State<AppDrawer> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// final themeProvider = Provider.of<ThemeProvider>(context);
|
||||
|
||||
return Drawer(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 100),
|
||||
child: DrawerHeader(
|
||||
decoration: BoxDecoration(color: Theme.of(context).primaryColor),
|
||||
child: const Text(
|
||||
'设置',
|
||||
style: TextStyle(color: Colors.white, fontSize: 24),
|
||||
),
|
||||
),
|
||||
),
|
||||
ThemeLayout(),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info),
|
||||
title: const Text('关于我们'),
|
||||
onTap: () {
|
||||
Navigator.pop(context); // 关闭抽屉
|
||||
// 跳转到关于页面
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NavBar extends StatelessWidget {
|
||||
final int currentIndex;
|
||||
final List<BottomNavigationBarItem> navItems;
|
||||
final Function(int) onTap;
|
||||
|
||||
const NavBar({
|
||||
super.key,
|
||||
required this.currentIndex,
|
||||
required this.onTap,
|
||||
required this.navItems,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(top: BorderSide(color: Theme.of(context).primaryColor)),
|
||||
),
|
||||
child: 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(BuildContext context) {
|
||||
return [
|
||||
IconButton(
|
||||
icon: Icon(Icons.search, color: Colors.white),
|
||||
onPressed: () {
|
||||
// 搜索功能
|
||||
},
|
||||
),
|
||||
// IconButton(
|
||||
// icon: Icon(Icons.add, color: Colors.white),
|
||||
// onPressed: () {
|
||||
// Navigator.pushNamed(context, '/recordForm');
|
||||
// },
|
||||
// ),
|
||||
];
|
||||
}
|
||||
@@ -1,18 +1,48 @@
|
||||
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: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/login.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:shared_preferences/shared_preferences.dart';
|
||||
import 'package:liquid_glass_widgets/liquid_glass_setup.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
FormBuilderLocalizations.delegate.load(const Locale('zh', 'CN'));
|
||||
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 {
|
||||
@@ -21,9 +51,53 @@ class MyApp extends StatelessWidget {
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final themeProvider = context.watch<ThemeProvider>();
|
||||
final brightness = MediaQuery.of(context).platformBrightness;
|
||||
themeProvider.isDarkMode = brightness == Brightness.dark;
|
||||
|
||||
return MaterialApp(
|
||||
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 [
|
||||
Locale('en', 'US'), // 英语
|
||||
Locale('zh', 'CN'), // 中文
|
||||
@@ -46,8 +120,14 @@ class MyApp extends StatelessWidget {
|
||||
home: LoginPage(),
|
||||
routes: {
|
||||
'/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(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 导航栏
|
||||
class NavItem {
|
||||
class PageInfo {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final IconData activeIcon;
|
||||
final Widget page;
|
||||
|
||||
const NavItem({
|
||||
const PageInfo({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.activeIcon,
|
||||
|
||||
@@ -4,13 +4,13 @@ part 'moment.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class Comment {
|
||||
final int id;
|
||||
final String username;
|
||||
final String avatar;
|
||||
final String content;
|
||||
final String date;
|
||||
int id;
|
||||
String username;
|
||||
String avatar;
|
||||
String content;
|
||||
String date;
|
||||
|
||||
const Comment({
|
||||
Comment({
|
||||
required this.id,
|
||||
required this.username,
|
||||
required this.avatar,
|
||||
@@ -25,15 +25,15 @@ class Comment {
|
||||
|
||||
@JsonSerializable()
|
||||
class Moment {
|
||||
final int? id;
|
||||
final int? userId;
|
||||
final String? username;
|
||||
final String? avatar;
|
||||
final String content;
|
||||
final List<String> imageList;
|
||||
final String? date;
|
||||
final List<int>? likeList;
|
||||
final List<Comment>? commentList;
|
||||
int? id;
|
||||
int? userId;
|
||||
String? username;
|
||||
String? avatar;
|
||||
String content;
|
||||
List<String> imageList;
|
||||
String? date;
|
||||
List<int>? likeList;
|
||||
List<Comment>? commentList;
|
||||
|
||||
Moment({
|
||||
this.id,
|
||||
@@ -50,6 +50,14 @@ class Moment {
|
||||
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()
|
||||
|
||||
@@ -5,11 +5,17 @@ part 'recipe.g.dart';
|
||||
/// 食材信息
|
||||
@JsonSerializable()
|
||||
class RecipeMaterial {
|
||||
int id;
|
||||
String type;
|
||||
String name;
|
||||
String amount;
|
||||
|
||||
RecipeMaterial({required this.type, required this.name, required this.amount});
|
||||
RecipeMaterial({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.name,
|
||||
required this.amount,
|
||||
});
|
||||
|
||||
factory RecipeMaterial.fromJson(Map<String, dynamic> json) =>
|
||||
_$RecipeMaterialFromJson(json);
|
||||
@@ -20,13 +26,20 @@ class RecipeMaterial {
|
||||
/// 步骤信息
|
||||
@JsonSerializable()
|
||||
class RecipeStep {
|
||||
int id;
|
||||
int sort;
|
||||
String content;
|
||||
String imageUrl;
|
||||
|
||||
RecipeStep({required this.sort, required this.content, required this.imageUrl});
|
||||
RecipeStep({
|
||||
required this.id,
|
||||
required this.sort,
|
||||
required this.content,
|
||||
required this.imageUrl,
|
||||
});
|
||||
|
||||
factory RecipeStep.fromJson(Map<String, dynamic> json) => _$RecipeStepFromJson(json);
|
||||
factory RecipeStep.fromJson(Map<String, dynamic> json) =>
|
||||
_$RecipeStepFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$RecipeStepToJson(this);
|
||||
}
|
||||
@@ -56,7 +69,7 @@ class RecipeComment {
|
||||
|
||||
/// 成果信息
|
||||
@JsonSerializable()
|
||||
class Record {
|
||||
class FoodRecord {
|
||||
int? id;
|
||||
String name;
|
||||
String category;
|
||||
@@ -64,7 +77,7 @@ class Record {
|
||||
String date;
|
||||
String imageUrl;
|
||||
|
||||
Record({
|
||||
FoodRecord({
|
||||
this.id,
|
||||
required this.name,
|
||||
required this.category,
|
||||
@@ -73,20 +86,31 @@ class Record {
|
||||
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()
|
||||
class RecipeQuery {
|
||||
String category;
|
||||
|
||||
RecipeQuery({
|
||||
required this.category
|
||||
});
|
||||
RecipeQuery({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);
|
||||
}
|
||||
@@ -102,16 +126,12 @@ class Recipe {
|
||||
bool isShare;
|
||||
int userId;
|
||||
String username;
|
||||
String avatar;
|
||||
List<RecipeMaterial> materialList;
|
||||
List<RecipeStep> stepList;
|
||||
List<Record> recordList;
|
||||
List<FoodRecord> recordList;
|
||||
List<int> likeList;
|
||||
int likeCount;
|
||||
List<int> favouriteList;
|
||||
int favouriteCount;
|
||||
List<RecipeComment> commentList;
|
||||
int commentCount;
|
||||
|
||||
Recipe({
|
||||
required this.id,
|
||||
@@ -122,21 +142,36 @@ class Recipe {
|
||||
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.likeCount,
|
||||
required this.favouriteList,
|
||||
required this.favouriteCount,
|
||||
required this.commentList,
|
||||
required this.commentCount,
|
||||
});
|
||||
|
||||
factory Recipe.fromJson(Map<String, dynamic> json) => _$RecipeFromJson(json);
|
||||
|
||||
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()
|
||||
@@ -146,13 +181,12 @@ class RecipeSummary {
|
||||
String category;
|
||||
double recommendRate;
|
||||
bool isShare;
|
||||
List<Record> recordList;
|
||||
List<FoodRecord> recordList;
|
||||
int likeCount;
|
||||
int favouriteCount;
|
||||
int commentCount;
|
||||
int userId;
|
||||
String username;
|
||||
String avatar;
|
||||
|
||||
RecipeSummary({
|
||||
required this.id,
|
||||
@@ -162,14 +196,14 @@ class RecipeSummary {
|
||||
required this.isShare,
|
||||
required this.userId,
|
||||
required this.username,
|
||||
required this.avatar,
|
||||
required this.recordList,
|
||||
required this.likeCount,
|
||||
required this.favouriteCount,
|
||||
required this.commentCount,
|
||||
});
|
||||
|
||||
factory RecipeSummary.fromJson(Map<String, dynamic> json) => _$RecipeSummaryFromJson(json);
|
||||
factory RecipeSummary.fromJson(Map<String, dynamic> json) =>
|
||||
_$RecipeSummaryFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$RecipeSummaryToJson(this);
|
||||
}
|
||||
@@ -187,7 +221,7 @@ class RecipeDetail {
|
||||
String avatar;
|
||||
List<RecipeMaterial> materialList;
|
||||
List<RecipeStep> stepList;
|
||||
List<Record> recordList;
|
||||
List<FoodRecord> recordList;
|
||||
List<int> likeList;
|
||||
List<int> favouriteList;
|
||||
List<RecipeComment> commentList;
|
||||
@@ -210,9 +244,30 @@ class RecipeDetail {
|
||||
required this.commentList,
|
||||
});
|
||||
|
||||
factory RecipeDetail.fromJson(Map<String, dynamic> json) => _$RecipeDetailFromJson(json);
|
||||
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 }
|
||||
|
||||
@@ -8,6 +8,7 @@ part of 'recipe.dart';
|
||||
|
||||
RecipeMaterial _$RecipeMaterialFromJson(Map<String, dynamic> json) =>
|
||||
RecipeMaterial(
|
||||
id: (json['id'] as num).toInt(),
|
||||
type: json['type'] as String,
|
||||
name: json['name'] as String,
|
||||
amount: json['amount'] as String,
|
||||
@@ -15,12 +16,14 @@ RecipeMaterial _$RecipeMaterialFromJson(Map<String, dynamic> json) =>
|
||||
|
||||
Map<String, dynamic> _$RecipeMaterialToJson(RecipeMaterial instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'type': instance.type,
|
||||
'name': instance.name,
|
||||
'amount': instance.amount,
|
||||
};
|
||||
|
||||
RecipeStep _$RecipeStepFromJson(Map<String, dynamic> json) => RecipeStep(
|
||||
id: (json['id'] as num).toInt(),
|
||||
sort: (json['sort'] as num).toInt(),
|
||||
content: json['content'] as String,
|
||||
imageUrl: json['imageUrl'] as String,
|
||||
@@ -28,6 +31,7 @@ RecipeStep _$RecipeStepFromJson(Map<String, dynamic> json) => RecipeStep(
|
||||
|
||||
Map<String, dynamic> _$RecipeStepToJson(RecipeStep instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'sort': instance.sort,
|
||||
'content': instance.content,
|
||||
'imageUrl': instance.imageUrl,
|
||||
@@ -51,7 +55,7 @@ Map<String, dynamic> _$RecipeCommentToJson(RecipeComment instance) =>
|
||||
'date': instance.date,
|
||||
};
|
||||
|
||||
Record _$RecordFromJson(Map<String, dynamic> json) => Record(
|
||||
FoodRecord _$FoodRecordFromJson(Map<String, dynamic> json) => FoodRecord(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
name: json['name'] as String,
|
||||
category: json['category'] as String,
|
||||
@@ -60,14 +64,15 @@ Record _$RecordFromJson(Map<String, dynamic> json) => Record(
|
||||
imageUrl: json['imageUrl'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RecordToJson(Record instance) => <String, dynamic>{
|
||||
Map<String, dynamic> _$FoodRecordToJson(FoodRecord instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'category': instance.category,
|
||||
'person': instance.person,
|
||||
'date': instance.date,
|
||||
'imageUrl': instance.imageUrl,
|
||||
};
|
||||
};
|
||||
|
||||
RecipeQuery _$RecipeQueryFromJson(Map<String, dynamic> json) =>
|
||||
RecipeQuery(category: json['category'] as String);
|
||||
@@ -84,7 +89,6 @@ Recipe _$RecipeFromJson(Map<String, dynamic> json) => Recipe(
|
||||
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>))
|
||||
@@ -95,23 +99,20 @@ Recipe _$RecipeFromJson(Map<String, dynamic> json) => Recipe(
|
||||
.toList(),
|
||||
recordList:
|
||||
(json['recordList'] as List<dynamic>)
|
||||
.map((e) => Record.fromJson(e as Map<String, dynamic>))
|
||||
.map((e) => FoodRecord.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
likeList:
|
||||
(json['likeList'] as List<dynamic>)
|
||||
.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
likeCount: (json['likeCount'] as num).toInt(),
|
||||
favouriteList:
|
||||
(json['favouriteList'] as List<dynamic>)
|
||||
.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
favouriteCount: (json['favouriteCount'] as num).toInt(),
|
||||
commentList:
|
||||
(json['commentList'] as List<dynamic>)
|
||||
.map((e) => RecipeComment.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
commentCount: (json['commentCount'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{
|
||||
@@ -123,16 +124,12 @@ Map<String, dynamic> _$RecipeToJson(Recipe instance) => <String, dynamic>{
|
||||
'isShare': instance.isShare,
|
||||
'userId': instance.userId,
|
||||
'username': instance.username,
|
||||
'avatar': instance.avatar,
|
||||
'materialList': instance.materialList,
|
||||
'stepList': instance.stepList,
|
||||
'recordList': instance.recordList,
|
||||
'likeList': instance.likeList,
|
||||
'likeCount': instance.likeCount,
|
||||
'favouriteList': instance.favouriteList,
|
||||
'favouriteCount': instance.favouriteCount,
|
||||
'commentList': instance.commentList,
|
||||
'commentCount': instance.commentCount,
|
||||
};
|
||||
|
||||
RecipeSummary _$RecipeSummaryFromJson(Map<String, dynamic> json) =>
|
||||
@@ -144,10 +141,9 @@ RecipeSummary _$RecipeSummaryFromJson(Map<String, dynamic> json) =>
|
||||
isShare: json['isShare'] as bool,
|
||||
userId: (json['userId'] as num).toInt(),
|
||||
username: json['username'] as String,
|
||||
avatar: json['avatar'] as String,
|
||||
recordList:
|
||||
(json['recordList'] as List<dynamic>)
|
||||
.map((e) => Record.fromJson(e as Map<String, dynamic>))
|
||||
.map((e) => FoodRecord.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
likeCount: (json['likeCount'] as num).toInt(),
|
||||
favouriteCount: (json['favouriteCount'] as num).toInt(),
|
||||
@@ -167,7 +163,6 @@ Map<String, dynamic> _$RecipeSummaryToJson(RecipeSummary instance) =>
|
||||
'commentCount': instance.commentCount,
|
||||
'userId': instance.userId,
|
||||
'username': instance.username,
|
||||
'avatar': instance.avatar,
|
||||
};
|
||||
|
||||
RecipeDetail _$RecipeDetailFromJson(Map<String, dynamic> json) => RecipeDetail(
|
||||
@@ -190,7 +185,7 @@ RecipeDetail _$RecipeDetailFromJson(Map<String, dynamic> json) => RecipeDetail(
|
||||
.toList(),
|
||||
recordList:
|
||||
(json['recordList'] as List<dynamic>)
|
||||
.map((e) => Record.fromJson(e as Map<String, dynamic>))
|
||||
.map((e) => FoodRecord.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
likeList:
|
||||
(json['likeList'] as List<dynamic>)
|
||||
|
||||
@@ -30,14 +30,16 @@ class SaTokenInfo {
|
||||
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);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class User {
|
||||
int? id;
|
||||
String? username;
|
||||
String username;
|
||||
int? gender;
|
||||
String? phoneNumber;
|
||||
String? email;
|
||||
@@ -51,7 +53,7 @@ class User {
|
||||
|
||||
User({
|
||||
this.id,
|
||||
this.username,
|
||||
required this.username,
|
||||
this.gender,
|
||||
this.phoneNumber,
|
||||
this.email,
|
||||
@@ -65,7 +67,24 @@ class User {
|
||||
});
|
||||
|
||||
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
|
||||
|
||||
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()
|
||||
@@ -75,6 +94,8 @@ class Session {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ Map<String, dynamic> _$SaTokenInfoToJson(SaTokenInfo instance) =>
|
||||
|
||||
User _$UserFromJson(Map<String, dynamic> json) => User(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
username: json['username'] as String?,
|
||||
username: json['username'] as String,
|
||||
gender: (json['gender'] as num?)?.toInt(),
|
||||
phoneNumber: json['phoneNumber'] as String?,
|
||||
email: json['email'] as String?,
|
||||
|
||||
@@ -19,16 +19,3 @@ class SummaryStats {
|
||||
|
||||
Map<String, dynamic> toJson() => _$SummaryStatsToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class ChartData {
|
||||
final String name;
|
||||
final double value;
|
||||
|
||||
const ChartData({required this.name, required this.value});
|
||||
|
||||
factory ChartData.fromJson(Map<String, dynamic> json) =>
|
||||
_$ChartDataFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$ChartDataToJson(this);
|
||||
}
|
||||
|
||||
@@ -18,13 +18,3 @@ Map<String, dynamic> _$SummaryStatsToJson(SummaryStats instance) =>
|
||||
'categoryCount': instance.categoryCount,
|
||||
'workCount': instance.workCount,
|
||||
};
|
||||
|
||||
ChartData _$ChartDataFromJson(Map<String, dynamic> json) => ChartData(
|
||||
name: json['name'] as String,
|
||||
value: (json['value'] as num).toDouble(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ChartDataToJson(ChartData instance) => <String, dynamic>{
|
||||
'name': instance.name,
|
||||
'value': instance.value,
|
||||
};
|
||||
|
||||
8
lib/provider/app_provider.dart
Normal file
8
lib/provider/app_provider.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppProvider with ChangeNotifier {
|
||||
String appVersion = '1.0.0';
|
||||
String buildNumber = '1';
|
||||
|
||||
String get fullVersion => '$appVersion+$buildNumber';
|
||||
}
|
||||
517
lib/provider/food_provider.dart
Normal file
517
lib/provider/food_provider.dart
Normal file
@@ -0,0 +1,517 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/models/common_model.dart';
|
||||
import 'package:food_hub_app/apis/moment.dart';
|
||||
import 'package:food_hub_app/apis/recipe.dart';
|
||||
import 'package:food_hub_app/apis/stats.dart';
|
||||
import 'package:food_hub_app/models/moment.dart';
|
||||
import 'package:food_hub_app/models/recipe.dart';
|
||||
import 'package:food_hub_app/models/stats.dart';
|
||||
import 'package:food_hub_app/utils/date_util.dart';
|
||||
import 'package:food_hub_app/utils/index.dart';
|
||||
import 'package:food_hub_app/views/record.dart';
|
||||
|
||||
class FoodProvider with ChangeNotifier {
|
||||
late RecordTab currentRecordTab = RecordTab.recipe;
|
||||
late FoodRecord recordFormItem;
|
||||
late Recipe recipeFormItem;
|
||||
late Moment momentFormItem;
|
||||
|
||||
late bool isEditing;
|
||||
bool isLoading = false;
|
||||
String? error;
|
||||
|
||||
String queryCategory = '全部菜系';
|
||||
late List<String> categoryList = [];
|
||||
late List<RecipeSummary> recipeSummaryList = [];
|
||||
|
||||
int selectYear = DateTime.now().year;
|
||||
DateTime selectedDay = DateTime.now();
|
||||
DateTime focusedDay = DateTime.now();
|
||||
|
||||
late List<FoodRecord> recordList = [];
|
||||
late List<FoodRecord> selectRecordList = [];
|
||||
|
||||
late SummaryStats summaryStats = SummaryStats(
|
||||
recipeCount: 0,
|
||||
categoryCount: 0,
|
||||
workCount: 0,
|
||||
);
|
||||
late double averageRecordCount = 0;
|
||||
late List<ChartData> recordStats = [];
|
||||
late List<ChartData> categoryStats = [];
|
||||
late List<ChartData> rankStats = [];
|
||||
|
||||
List<Moment> momentList = [];
|
||||
int currentPage = 1;
|
||||
final int pageSize = 5;
|
||||
bool hasMore = true;
|
||||
|
||||
void resetRecordForm() {
|
||||
recordFormItem = FoodRecord.getEmpty();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void initRecordForm(FoodRecord record) {
|
||||
recordFormItem = record;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void resetRecipeForm() {
|
||||
recipeFormItem = Recipe.getEmpty();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void initRecipeForm(Recipe recipe) {
|
||||
recipeFormItem = recipe;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateRecipeFormItem({
|
||||
String? name,
|
||||
String? category,
|
||||
double? recommendRate,
|
||||
String? remark,
|
||||
bool? isShare,
|
||||
}) {
|
||||
if (name != null) recipeFormItem.name = name;
|
||||
if (category != null) recipeFormItem.category = category;
|
||||
if (recommendRate != null) recipeFormItem.recommendRate = recommendRate;
|
||||
if (remark != null) recipeFormItem.remark = remark;
|
||||
if (isShare != null) recipeFormItem.isShare = isShare;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void addRecipeMaterial() {
|
||||
recipeFormItem.materialList.add(
|
||||
RecipeMaterial(
|
||||
id: DateTime.now().microsecondsSinceEpoch,
|
||||
type: '主料',
|
||||
name: '',
|
||||
amount: '',
|
||||
),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateRecipeMaterial(
|
||||
int index, {
|
||||
String? type,
|
||||
String? name,
|
||||
String? amount,
|
||||
}) {
|
||||
final material = recipeFormItem.materialList[index];
|
||||
recipeFormItem.materialList[index] = RecipeMaterial(
|
||||
id: material.id,
|
||||
type: type ?? material.type,
|
||||
name: name ?? material.name,
|
||||
amount: amount ?? material.amount,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void removeRecipeMaterial(int index) {
|
||||
recipeFormItem.materialList.removeAt(index);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void addRecipeStep() {
|
||||
recipeFormItem.stepList.add(
|
||||
RecipeStep(
|
||||
id: DateTime.now().microsecondsSinceEpoch,
|
||||
sort: recipeFormItem.stepList.length,
|
||||
content: '',
|
||||
imageUrl: '',
|
||||
),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateRecipeStep(int index, {String? content, String? imageUrl}) {
|
||||
final step = recipeFormItem.stepList[index];
|
||||
recipeFormItem.stepList[index] = RecipeStep(
|
||||
id: step.id,
|
||||
sort: step.sort,
|
||||
content: content ?? step.content,
|
||||
imageUrl: imageUrl ?? step.imageUrl,
|
||||
);
|
||||
}
|
||||
|
||||
void removeRecipeStep(int index) {
|
||||
recipeFormItem.stepList.removeAt(index);
|
||||
|
||||
for (int i = 0; i < recipeFormItem.stepList.length; i++) {
|
||||
recipeFormItem.stepList[i].sort = i;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateRecordFormItem({String? name, String? date, String? imageUrl}) {
|
||||
if (name != null) recordFormItem.name = name;
|
||||
if (date != null) recordFormItem.date = date;
|
||||
if (imageUrl != null) recordFormItem.imageUrl = imageUrl;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateMomentFormItem({String? content}) {
|
||||
if (content != null) momentFormItem.content = content;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void addMomentFormImage(List<String> imageUrls) {
|
||||
momentFormItem.imageList.addAll(imageUrls);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void removeMomentFormImage(int index) {
|
||||
momentFormItem.imageList.removeAt(index);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void resetMomentForm() {
|
||||
momentFormItem = Moment.getEmpty();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void initMomentForm(Moment moment) {
|
||||
momentFormItem = moment;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> onRecordTabChange() async {
|
||||
switch (currentRecordTab) {
|
||||
case RecordTab.recipe:
|
||||
await refreshRecipeList();
|
||||
break;
|
||||
case RecordTab.calendar:
|
||||
case RecordTab.timeline:
|
||||
await refreshRecordList();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> queryCategoryList() async {
|
||||
try {
|
||||
error = null;
|
||||
notifyListeners();
|
||||
|
||||
final result = await queryCategoryApi();
|
||||
categoryList.clear();
|
||||
categoryList.add('全部菜系');
|
||||
categoryList.addAll(result);
|
||||
} catch (e) {
|
||||
error = '加载数据失败: $e';
|
||||
debugPrint('加载数据失败: $e');
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> queryRecipeByUserId(int userId) async {
|
||||
if (isLoading) return;
|
||||
|
||||
try {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
notifyListeners();
|
||||
|
||||
final result = await queryRecipeByUserApi(userId);
|
||||
recipeSummaryList = result;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
error = '加载数据失败: $e';
|
||||
debugPrint('加载数据失败: $e');
|
||||
} finally {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshRecipeList() async {
|
||||
if (isLoading) return;
|
||||
|
||||
try {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
notifyListeners();
|
||||
|
||||
final category = queryCategory == '全部菜系' ? '' : queryCategory;
|
||||
final result = await queryRecipeApi(RecipeQuery(category: category));
|
||||
recipeSummaryList = result;
|
||||
} catch (e) {
|
||||
error = '加载数据失败: $e';
|
||||
debugPrint('加载数据失败: $e');
|
||||
} finally {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> handleRecipe() async {
|
||||
if (isLoading) return true;
|
||||
|
||||
try {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
notifyListeners();
|
||||
|
||||
if (isEditing) {
|
||||
await updateRecipeApi(recipeFormItem.id!, recipeFormItem);
|
||||
} else {
|
||||
await addRecipeApi(recipeFormItem);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
error = '处理数据失败: $e';
|
||||
debugPrint('处理数据失败: $e');
|
||||
return false;
|
||||
} finally {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshRecordList() async {
|
||||
if (isLoading) return;
|
||||
|
||||
if (currentRecordTab == RecordTab.recipe) return;
|
||||
|
||||
try {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
notifyListeners();
|
||||
|
||||
late String startDate;
|
||||
late String endDate;
|
||||
|
||||
switch (currentRecordTab) {
|
||||
case RecordTab.calendar:
|
||||
startDate = getFirstDayOfMonth(focusedDay);
|
||||
endDate = getLastDayOfMonth(focusedDay);
|
||||
break;
|
||||
case RecordTab.timeline:
|
||||
startDate = '$selectYear-01-01';
|
||||
endDate = '$selectYear-12-31';
|
||||
break;
|
||||
case RecordTab.recipe:
|
||||
break;
|
||||
}
|
||||
|
||||
final result = await queryRecordApi(startDate, endDate);
|
||||
|
||||
recordList = result;
|
||||
|
||||
if (recordList.isNotEmpty) {
|
||||
selectedDay = DateTime.parse(recordList.last.date);
|
||||
} else {
|
||||
selectedDay = focusedDay;
|
||||
}
|
||||
|
||||
refreshSelectRecord();
|
||||
} catch (e) {
|
||||
error = '加载数据失败: $e';
|
||||
debugPrint('加载数据失败: $e');
|
||||
} finally {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> handleRecord() async {
|
||||
if (isLoading) return true;
|
||||
|
||||
try {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
notifyListeners();
|
||||
|
||||
if (isEditing) {
|
||||
await updateRecordApi(recordFormItem.id!, recordFormItem);
|
||||
} else {
|
||||
await addRecordApi(recordFormItem);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
error = '处理数据失败: $e';
|
||||
debugPrint('处理数据失败: $e');
|
||||
return false;
|
||||
} finally {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> deleteRecord() async {
|
||||
if (isLoading) return true;
|
||||
|
||||
try {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
notifyListeners();
|
||||
|
||||
await deleteRecordApi(recordFormItem.id!);
|
||||
return true;
|
||||
} catch (e) {
|
||||
error = '处理数据失败: $e';
|
||||
debugPrint('处理数据失败: $e');
|
||||
return false;
|
||||
} finally {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void refreshSelectRecord() {
|
||||
selectRecordList =
|
||||
recordList
|
||||
.where((record) => record.date == formatDateTime(selectedDay))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<void> refreshFoodStats() async {
|
||||
if (isLoading) return;
|
||||
|
||||
try {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
notifyListeners();
|
||||
|
||||
final statsResult = await queryStatsApi();
|
||||
final recordStatsResult = await queryRecordStatsApi();
|
||||
final categoryStatsResult = await queryCategoryStatsApi();
|
||||
final rankStatsResult = await queryRankStatsApi();
|
||||
|
||||
summaryStats = statsResult;
|
||||
recordStats = recordStatsResult;
|
||||
categoryStats = categoryStatsResult;
|
||||
rankStats = rankStatsResult.take(15).toList();
|
||||
|
||||
if (recordStats.isNotEmpty) {
|
||||
double sumValue = recordStats.fold(
|
||||
0.0,
|
||||
(sum, item) => sum + item.value,
|
||||
);
|
||||
averageRecordCount = sumValue / recordStats.length;
|
||||
}
|
||||
} catch (e) {
|
||||
error = '加载数据失败: $e';
|
||||
debugPrint('加载数据失败: $e');
|
||||
} finally {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> queryMomentByPage({bool isRefresh = true}) async {
|
||||
if (isLoading) return;
|
||||
|
||||
try {
|
||||
if (currentPage == 1) {
|
||||
isLoading = true;
|
||||
}
|
||||
|
||||
error = null;
|
||||
notifyListeners();
|
||||
|
||||
// 如果是刷新,重置页码
|
||||
if (isRefresh) {
|
||||
currentPage = 1;
|
||||
}
|
||||
|
||||
final result = await queryMomentByPageApi(currentPage, pageSize);
|
||||
|
||||
// 更新数据
|
||||
if (isRefresh) {
|
||||
momentList = result.records;
|
||||
} else {
|
||||
momentList.addAll(result.records);
|
||||
}
|
||||
|
||||
// 判断是否还有更多数据
|
||||
hasMore = result.current < result.pages;
|
||||
if (hasMore) {
|
||||
currentPage++;
|
||||
}
|
||||
} catch (e) {
|
||||
error = '加载数据失败: $e';
|
||||
debugPrint('加载数据失败: $e');
|
||||
} finally {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> queryMomentByUserId(int userId) async {
|
||||
if (isLoading) return;
|
||||
|
||||
try {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
notifyListeners();
|
||||
|
||||
final result = await queryMomentListByUserIdApi(userId);
|
||||
momentList = result;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
error = '加载数据失败: $e';
|
||||
debugPrint('加载数据失败: $e');
|
||||
} finally {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadMoreMomentList() async {
|
||||
if (hasMore && !isLoading) {
|
||||
await queryMomentByPage(isRefresh: false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshMomentList() async {
|
||||
await queryMomentByPage(isRefresh: true);
|
||||
}
|
||||
|
||||
Future<bool> handleMoment() async {
|
||||
if (isLoading) return true;
|
||||
|
||||
try {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
notifyListeners();
|
||||
|
||||
if (isEditing) {
|
||||
await updateMomentApi(momentFormItem.id!, momentFormItem);
|
||||
} else {
|
||||
await addMomentApi(momentFormItem);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
error = '处理数据失败: $e';
|
||||
debugPrint('处理数据失败: $e');
|
||||
return false;
|
||||
} finally {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> deleteMoment() async {
|
||||
if (isLoading) return true;
|
||||
|
||||
try {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
notifyListeners();
|
||||
|
||||
await deleteMomentApi(momentFormItem.id!);
|
||||
return true;
|
||||
} catch (e) {
|
||||
error = '处理数据失败: $e';
|
||||
debugPrint('处理数据失败: $e');
|
||||
return false;
|
||||
} finally {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
104
lib/provider/user_provider.dart
Normal file
104
lib/provider/user_provider.dart
Normal file
@@ -0,0 +1,104 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:food_hub_app/apis/moment.dart';
|
||||
import 'package:food_hub_app/apis/user.dart';
|
||||
import 'package:food_hub_app/models/moment.dart';
|
||||
import 'package:food_hub_app/models/session.dart';
|
||||
|
||||
class UserProvider with ChangeNotifier {
|
||||
late User currentUser = User.getEmpty();
|
||||
bool isLoading = false;
|
||||
String? error = '';
|
||||
List<Moment> momentList = [];
|
||||
late User userFormItem;
|
||||
|
||||
Future<void> refreshUser(int id) async {
|
||||
if (isLoading) return;
|
||||
|
||||
try {
|
||||
isLoading = true;
|
||||
error = '';
|
||||
notifyListeners();
|
||||
|
||||
final result = await queryUserApi(id);
|
||||
currentUser = result;
|
||||
} catch (e) {
|
||||
error = '加载数据失败: $e';
|
||||
debugPrint('加载数据失败: $e');
|
||||
} finally {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> queryMomentByUserId(int userId) async {
|
||||
if (isLoading) return;
|
||||
|
||||
try {
|
||||
isLoading = true;
|
||||
error = '';
|
||||
notifyListeners();
|
||||
|
||||
final result = await queryMomentListByUserIdApi(userId);
|
||||
momentList = result;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
error = '加载数据失败: $e';
|
||||
debugPrint('加载数据失败: $e');
|
||||
} finally {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void initUserForm(User user) {
|
||||
userFormItem = user;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateUserFormItem({
|
||||
String? username,
|
||||
int? gender,
|
||||
String? phoneNumber,
|
||||
String? email,
|
||||
DateTime? birthDate,
|
||||
String? avatar,
|
||||
List<String>? area,
|
||||
String? address,
|
||||
String? job,
|
||||
List<String>? tags,
|
||||
String? description
|
||||
}) {
|
||||
if (username != null) userFormItem.username = username;
|
||||
if (gender != null) userFormItem.gender = gender;
|
||||
if (phoneNumber != null) userFormItem.phoneNumber = phoneNumber;
|
||||
if (email != null) userFormItem.email = email;
|
||||
if (birthDate != null) userFormItem.birthDate = birthDate;
|
||||
if (avatar != null) userFormItem.avatar = avatar;
|
||||
if (area != null) userFormItem.area = area;
|
||||
if (address != null) userFormItem.address = address;
|
||||
if (job != null) userFormItem.job = job;
|
||||
if (tags != null) userFormItem.tags = tags;
|
||||
if (description != null) userFormItem.description = description;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<bool> handleUser() async {
|
||||
if (isLoading) return true;
|
||||
|
||||
try {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
notifyListeners();
|
||||
|
||||
await updateUserApi(userFormItem.id!, userFormItem);
|
||||
return true;
|
||||
} catch (e) {
|
||||
error = '处理数据失败: $e';
|
||||
debugPrint('处理数据失败: $e');
|
||||
return false;
|
||||
} finally {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:food_hub_app/config/app_config.dart';
|
||||
import 'package:food_hub_app/utils/sp_util.dart';
|
||||
import 'package:food_hub_app/widgets/common/index.dart';
|
||||
|
||||
import 'log_util.dart';
|
||||
|
||||
class HttpUtil {
|
||||
static final HttpUtil _instance = HttpUtil._internal();
|
||||
|
||||
factory HttpUtil() => _instance;
|
||||
|
||||
late Dio _dio;
|
||||
|
||||
// 请求头配置
|
||||
Map<String, dynamic> headers = {
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
// 超时时间
|
||||
final int timeout = 5;
|
||||
|
||||
HttpUtil._internal() {
|
||||
// 初始化Dio实例
|
||||
BaseOptions options = BaseOptions(
|
||||
baseUrl: AppConfig.baseApiUrl,
|
||||
connectTimeout: Duration(seconds: timeout),
|
||||
receiveTimeout: Duration(seconds: timeout),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
_dio = Dio(options);
|
||||
|
||||
// 添加请求拦截器
|
||||
_dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
logger.d("请求URL: ${options.uri}");
|
||||
if (options.data != null) {
|
||||
logger.d("请求参数: ${options.data}");
|
||||
}
|
||||
|
||||
if (SPUtil.getString('token').isNotEmpty) {
|
||||
options.headers['satoken'] = SPUtil.getString('token');
|
||||
}
|
||||
return handler.next(options);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// 添加响应拦截器
|
||||
_dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onResponse: (response, handler) {
|
||||
logger.d("响应状态码: ${response.statusCode}");
|
||||
// logger.d("响应数据: ${response.data}");
|
||||
return handler.next(response);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// 添加错误拦截器
|
||||
_dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onError: (DioException e, handler) {
|
||||
_handleError(e);
|
||||
return handler.next(e);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 基础请求方法(处理所有类型的请求)
|
||||
Future<T> _request<T>(
|
||||
String path, {
|
||||
required String method,
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
T Function(dynamic data)? converter,
|
||||
}) async {
|
||||
try {
|
||||
Response response = await _dio.request(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: Options(method: method),
|
||||
);
|
||||
|
||||
// 处理响应数据
|
||||
if (converter != null) {
|
||||
return converter(response.data);
|
||||
}
|
||||
|
||||
// 没有转换器时尝试直接返回(可能不安全,建议提供转换器)
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// GET请求
|
||||
Future<T> get<T>(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
T Function(dynamic data)? converter,
|
||||
}) => _request(
|
||||
path,
|
||||
method: "GET",
|
||||
queryParameters: queryParameters,
|
||||
converter: converter,
|
||||
);
|
||||
|
||||
// POST请求
|
||||
Future<T> post<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
T Function(dynamic data)? converter,
|
||||
}) => _request(
|
||||
path,
|
||||
method: "POST",
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
converter: converter,
|
||||
);
|
||||
|
||||
// PUT请求
|
||||
Future<T> put<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
T Function(dynamic data)? converter,
|
||||
}) => _request(
|
||||
path,
|
||||
method: "PUT",
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
converter: converter,
|
||||
);
|
||||
|
||||
// DELETE请求
|
||||
Future<T> delete<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
T Function(dynamic data)? converter,
|
||||
}) => _request(
|
||||
path,
|
||||
method: "DELETE",
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
converter: converter,
|
||||
);
|
||||
|
||||
// 文件上传
|
||||
Future<dynamic> upload(
|
||||
String path,
|
||||
String filePath, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
}) async {
|
||||
try {
|
||||
FormData formData = FormData.fromMap({
|
||||
'file': await MultipartFile.fromFile(filePath),
|
||||
});
|
||||
|
||||
Response response = await _dio.post(
|
||||
path,
|
||||
data: formData,
|
||||
queryParameters: queryParameters,
|
||||
);
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
_handleError(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// 错误处理
|
||||
void _handleError(dynamic error) {
|
||||
String errorMessage = '未知错误';
|
||||
if (error is DioException) {
|
||||
switch (error.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
errorMessage = '连接超时,请检查网络连接';
|
||||
break;
|
||||
case DioExceptionType.sendTimeout:
|
||||
errorMessage = '发送超时,请检查网络连接';
|
||||
break;
|
||||
case DioExceptionType.receiveTimeout:
|
||||
errorMessage = '接收超时,请检查网络连接';
|
||||
break;
|
||||
case DioExceptionType.cancel:
|
||||
errorMessage = '请求已取消';
|
||||
break;
|
||||
case DioExceptionType.badCertificate:
|
||||
errorMessage = '证书验证失败';
|
||||
break;
|
||||
case DioExceptionType.badResponse:
|
||||
// 处理HTTP响应错误(4xx, 5xx)
|
||||
final statusCode = error.response?.statusCode ?? 0;
|
||||
final responseData = error.response?.data;
|
||||
|
||||
if (responseData is Map<String, dynamic> && responseData.containsKey('message')) {
|
||||
// 服务器返回了自定义错误消息
|
||||
errorMessage = responseData['message']?.toString() ?? '未知错误';
|
||||
} else {
|
||||
errorMessage = _getHttpErrorMessage(statusCode);
|
||||
}
|
||||
break;
|
||||
case DioExceptionType.connectionError:
|
||||
errorMessage = '网络连接错误,请检查网络设置';
|
||||
break;
|
||||
case DioExceptionType.unknown:
|
||||
if (error.error != null) {
|
||||
errorMessage = '未知错误: ${error.error.toString()}';
|
||||
}
|
||||
errorMessage = '发生未知错误';
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
errorMessage = '非Dio错误: $error';
|
||||
}
|
||||
|
||||
print(errorMessage);
|
||||
showErrorToast(errorMessage);
|
||||
}
|
||||
|
||||
String _getHttpErrorMessage(int statusCode) {
|
||||
// 根据HTTP状态码返回对应的错误消息
|
||||
switch (statusCode) {
|
||||
case 400: return '错误请求,请检查参数';
|
||||
case 401: return '未授权,请登录';
|
||||
case 403: return '禁止访问,权限不足';
|
||||
case 404: return '资源不存在';
|
||||
case 405: return '方法不允许';
|
||||
case 408: return '请求超时';
|
||||
case 500: return '服务器内部错误';
|
||||
case 502: return '网关错误';
|
||||
case 503: return '服务不可用';
|
||||
case 504: return '网关超时';
|
||||
default: return 'HTTP错误: $statusCode';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,13 @@ String formatDateTime(DateTime dateTime, [String format = 'yyyy-MM-dd']) {
|
||||
return Intl.withLocale('zh_CN', () => formatter.format(dateTime));
|
||||
}
|
||||
|
||||
/// 通用列表转换函数
|
||||
List<T> convertListResponse<T>(dynamic data, T Function(Map<String, dynamic>) fromJson) {
|
||||
|
||||
|
||||
List<String> convertListStringResponse(dynamic data) {
|
||||
if (data is List) {
|
||||
return data.map((item) => fromJson(item as Map<String, dynamic>)).toList();
|
||||
return data.map((item) => item.toString()).toList();
|
||||
}
|
||||
throw FormatException('Expected a list of items for conversion, but got ${data.runtimeType}');
|
||||
throw FormatException(
|
||||
'Expected a list of items for conversion, but got ${data.runtimeType}',
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
// 全局日志实例,在整个项目中共享
|
||||
final Logger logger = Logger(
|
||||
printer: PrettyPrinter(
|
||||
methodCount: 1,
|
||||
colors: true,
|
||||
dateTimeFormat: DateTimeFormat.dateAndTime,
|
||||
),
|
||||
// 可选:配置输出到文件(需配合文件操作库)
|
||||
// output: FileOutput(file: File('logs/app.log')),
|
||||
);
|
||||
43
lib/utils/minio_utils.dart
Normal file
43
lib/utils/minio_utils.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter_common/utils/file_utils.dart';
|
||||
import 'package:food_hub_app/config/app_config.dart';
|
||||
import 'package:minio/io.dart';
|
||||
import 'package:minio/minio.dart';
|
||||
|
||||
|
||||
class MinIOHelper {
|
||||
static final MinIOHelper _instance = MinIOHelper._internal();
|
||||
|
||||
factory MinIOHelper() => _instance;
|
||||
|
||||
final String ip = AppConfig.rustfsIp;
|
||||
|
||||
MinIOHelper._internal() {
|
||||
_minio = Minio(
|
||||
endPoint: AppConfig.rustfsIp,
|
||||
port: AppConfig.rustfsPort,
|
||||
accessKey: AppConfig.rustfsAccessKey,
|
||||
secretKey: AppConfig.rustfsSecretKey,
|
||||
useSSL: false,
|
||||
);
|
||||
}
|
||||
|
||||
late Minio _minio;
|
||||
|
||||
Future<String> uploadFile({
|
||||
required PlatformFile file,
|
||||
required String bucketName,
|
||||
Function(double)? onProgress,
|
||||
}) async {
|
||||
try {
|
||||
String hashName = await generateMD5HashName(file.path!);
|
||||
String fileName = '$hashName${getFileExtension(file.name)}';
|
||||
|
||||
await _minio.fPutObject(bucketName, fileName, file.path!);
|
||||
|
||||
return fileName;
|
||||
} catch (e) {
|
||||
throw Exception('文件上传失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
124
lib/utils/screenshot_util.dart
Normal file
124
lib/utils/screenshot_util.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_common/utils/toast_util.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
/// 截图工具类
|
||||
class ScreenshotUtil {
|
||||
static double pixelRatio = 3.0;
|
||||
static Duration delay = const Duration(milliseconds: 50);
|
||||
static String watermarkText = '来自 食光集';
|
||||
|
||||
// 截图功能
|
||||
static Future<Uint8List?> captureImage({required GlobalKey globalKey}) async {
|
||||
// 等待一帧确保UI已渲染
|
||||
await Future.delayed(delay);
|
||||
final RenderObject? render = globalKey.currentContext!.findRenderObject();
|
||||
final RenderRepaintBoundary boundary = render as RenderRepaintBoundary;
|
||||
final ui.Image image = await boundary.toImage(pixelRatio: pixelRatio);
|
||||
|
||||
// 添加水印
|
||||
return await _addWatermarkToImage(image);
|
||||
}
|
||||
|
||||
// 添加水印到图片
|
||||
static Future<Uint8List> _addWatermarkToImage(ui.Image image) async {
|
||||
// 创建画布
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = Canvas(recorder);
|
||||
|
||||
// 绘制原始图片
|
||||
canvas.drawImage(image, Offset.zero, Paint());
|
||||
|
||||
// 添加文字水印
|
||||
_drawTextWatermark(canvas, image);
|
||||
|
||||
// 生成最终图片
|
||||
final picture = recorder.endRecording();
|
||||
final watermarkedImage = await picture.toImage(image.width, image.height);
|
||||
final ByteData? byteData = await watermarkedImage.toByteData(
|
||||
format: ui.ImageByteFormat.png,
|
||||
);
|
||||
|
||||
return byteData!.buffer.asUint8List();
|
||||
}
|
||||
|
||||
// 绘制文字水印
|
||||
static void _drawTextWatermark(Canvas canvas, ui.Image image) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: watermarkText,
|
||||
style: TextStyle(
|
||||
color: Color(0x99FFFFFF),
|
||||
fontSize: 28,
|
||||
fontFamily: 'CustomFont',
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
|
||||
textPainter.layout(maxWidth: image.width.toDouble());
|
||||
|
||||
// 计算水印位置(右下角,带边距)
|
||||
final double x = image.width - textPainter.width - 40;
|
||||
final double y = image.height - textPainter.height - 40;
|
||||
|
||||
// 绘制文字背景
|
||||
final backgroundPaint =
|
||||
Paint()
|
||||
..color = const ui.Color(0x4D000000)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
// 绘制圆角矩形背景
|
||||
final backgroundRect = RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(
|
||||
x - 12,
|
||||
y - 6,
|
||||
textPainter.width + 24,
|
||||
textPainter.height + 12,
|
||||
),
|
||||
Radius.circular(6),
|
||||
);
|
||||
canvas.drawRRect(backgroundRect, backgroundPaint);
|
||||
|
||||
// 绘制文字
|
||||
textPainter.paint(canvas, Offset(x, y));
|
||||
}
|
||||
|
||||
// 保存图片到临时文件
|
||||
static Future<File> saveImageToFile(Uint8List bytes, String fileName) async {
|
||||
final directory = await getTemporaryDirectory();
|
||||
final File imageFile = File('${directory.path}/$fileName');
|
||||
await imageFile.writeAsBytes(bytes);
|
||||
return imageFile;
|
||||
}
|
||||
|
||||
static Future<void> captureAndShare({
|
||||
required GlobalKey globalKey,
|
||||
required String fileName,
|
||||
required String shareText,
|
||||
}) async {
|
||||
try {
|
||||
final imageBytes = await captureImage(globalKey: globalKey);
|
||||
|
||||
if (imageBytes != null) {
|
||||
final imageFile = await saveImageToFile(imageBytes, fileName);
|
||||
await SharePlus.instance.share(
|
||||
ShareParams(files: [XFile(imageFile.path)], text: shareText),
|
||||
);
|
||||
|
||||
// 分享后删除临时文件
|
||||
await imageFile.delete();
|
||||
} else {
|
||||
ToastUtil.error('生成图片失败,请重试');
|
||||
}
|
||||
} catch (e) {
|
||||
ToastUtil.error('分享失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class SPUtil {
|
||||
static late SharedPreferences _prefs;
|
||||
|
||||
/// 初始化
|
||||
static Future<void> init() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
/// 存储数据
|
||||
static Future<bool> set(String key, dynamic value) {
|
||||
if (value is String) return _prefs.setString(key, value);
|
||||
if (value is int) return _prefs.setInt(key, value);
|
||||
if (value is bool) return _prefs.setBool(key, value);
|
||||
if (value is double) return _prefs.setDouble(key, value);
|
||||
if (value is List<String>) return _prefs.setStringList(key, value);
|
||||
throw ArgumentError('Unsupported value type: ${value.runtimeType}');
|
||||
}
|
||||
|
||||
/// 获取数据
|
||||
static dynamic get(String key, [dynamic defaultValue]) {
|
||||
if (!_prefs.containsKey(key)) return defaultValue;
|
||||
return _prefs.get(key) ?? defaultValue;
|
||||
}
|
||||
|
||||
/// 获取字符串
|
||||
static String getString(String key, [String defaultValue = '']) {
|
||||
return _prefs.getString(key) ?? defaultValue;
|
||||
}
|
||||
|
||||
/// 获取布尔值
|
||||
static bool getBool(String key, [bool defaultValue = false]) {
|
||||
return _prefs.getBool(key) ?? defaultValue;
|
||||
}
|
||||
|
||||
/// 获取整数
|
||||
static int getInt(String key, [int defaultValue = 0]) {
|
||||
return _prefs.getInt(key) ?? defaultValue;
|
||||
}
|
||||
|
||||
/// 获取浮点数
|
||||
static double getDouble(String key, [double defaultValue = 0.0]) {
|
||||
return _prefs.getDouble(key) ?? defaultValue;
|
||||
}
|
||||
|
||||
/// 获取字符串列表
|
||||
static List<String> getStringList(String key, [List<String> defaultValue = const []]) {
|
||||
return _prefs.getStringList(key) ?? defaultValue;
|
||||
}
|
||||
|
||||
/// 删除数据
|
||||
static Future<bool> remove(String key) {
|
||||
return _prefs.remove(key);
|
||||
}
|
||||
|
||||
/// 清空所有数据
|
||||
static Future<bool> clear() {
|
||||
return _prefs.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:food_hub_app/layout/index.dart';
|
||||
import 'package:food_hub_app/models/layout.dart';
|
||||
import 'package:food_hub_app/views/moment.dart';
|
||||
import 'package:food_hub_app/views/profile.dart';
|
||||
import 'package:food_hub_app/views/record.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 {
|
||||
const HomePage({super.key});
|
||||
@@ -16,151 +20,52 @@ class HomePage extends StatefulWidget {
|
||||
class _HomePage extends State<HomePage> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
final List<NavItem> navItems = [
|
||||
NavItem(
|
||||
label: "记录",
|
||||
icon: Icons.home_outlined,
|
||||
activeIcon: Icons.home,
|
||||
page: RecordPage(),
|
||||
),
|
||||
NavItem(
|
||||
label: "统计",
|
||||
icon: Icons.pie_chart_outline,
|
||||
activeIcon: Icons.pie_chart,
|
||||
page: StatsPage(),
|
||||
),
|
||||
NavItem(
|
||||
label: "朋友圈",
|
||||
icon: Icons.group_outlined,
|
||||
activeIcon: Icons.group,
|
||||
page: MomentPage(),
|
||||
),
|
||||
NavItem(
|
||||
label: "我的",
|
||||
icon: Icons.account_circle_outlined,
|
||||
activeIcon: Icons.account_circle,
|
||||
page: ProfilePage(),
|
||||
),
|
||||
final _pages = [
|
||||
const RecordPage(),
|
||||
const StatsPage(),
|
||||
const MomentPage(),
|
||||
const ProfilePage(),
|
||||
];
|
||||
|
||||
List<BottomNavigationBarItem> get bottomNavItems =>
|
||||
navItems
|
||||
.map(
|
||||
(item) => BottomNavigationBarItem(
|
||||
icon: Icon(item.icon),
|
||||
activeIcon: Icon(item.activeIcon),
|
||||
label: item.label,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
List<Widget> get tabPages => navItems.map((item) => item.page).toList();
|
||||
|
||||
// 显示底部弹窗
|
||||
void _showBottomSheet() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'请选择操作',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ListTile(
|
||||
leading: Icon(Icons.book, color: Theme.of(context).primaryColor),
|
||||
title: const Text('新增菜谱'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_handleAddRecipe();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(Icons.note_add, color: Theme.of(context).primaryColor),
|
||||
title: const Text('新增记录'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pushNamed(context, "/recordForm");
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(Icons.group, color: Theme.of(context).primaryColor),
|
||||
title: const Text('发布朋友圈'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_handleAddRecord();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 处理添加菜谱
|
||||
void _handleAddRecipe() {
|
||||
// 这里添加跳转或处理添加菜谱的逻辑
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('添加菜谱功能')),
|
||||
);
|
||||
}
|
||||
|
||||
// 处理添加记录
|
||||
void _handleAddRecord() {
|
||||
// 这里添加跳转或处理添加记录的逻辑
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('添加记录功能')),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text('Food Hub', style: TextStyle(color: Colors.white)),
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
leading: Builder(
|
||||
builder: (context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.menu),
|
||||
color: Colors.white,
|
||||
onPressed: () => Scaffold.of(context).openDrawer(),
|
||||
);
|
||||
},
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return GlassScaffold(
|
||||
statusBarStyle: GlassStatusBarStyle.auto,
|
||||
bottomBar: GlassBottomBar(
|
||||
settings: LiquidGlassSettings(
|
||||
glassColor: colors.surface
|
||||
),
|
||||
actions: homeActions(context),
|
||||
selectedIndex: _currentIndex,
|
||||
onTabSelected: (i) => setState(() => _currentIndex = i),
|
||||
indicatorColor: colors.primary,
|
||||
selectedIconColor: colors.surface,
|
||||
unselectedIconColor: Color(0xFF000000),
|
||||
tabs: const [
|
||||
GlassBottomBarTab(
|
||||
icon: Icon(Icons.home_outlined),
|
||||
activeIcon: Icon(Icons.home),
|
||||
label: '记录',
|
||||
),
|
||||
backgroundColor: Color(0xFFF5F5F5),
|
||||
drawer: const SettingsDrawer(),
|
||||
body: tabPages[_currentIndex],
|
||||
floatingActionButton: FloatingActionButton(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
onPressed: _showBottomSheet,
|
||||
shape: const CircleBorder(),
|
||||
child: const Icon(Icons.add, color: Colors.white, size: 30),
|
||||
GlassBottomBarTab(
|
||||
icon: Icon(Icons.pie_chart_outline),
|
||||
activeIcon: Icon(Icons.pie_chart),
|
||||
label: '统计',
|
||||
),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
|
||||
bottomNavigationBar: NavBar(
|
||||
navItems: bottomNavItems,
|
||||
currentIndex: _currentIndex,
|
||||
onTap: (index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
},
|
||||
GlassBottomBarTab(
|
||||
icon: Icon(Icons.group_outlined),
|
||||
activeIcon: Icon(Icons.group),
|
||||
label: '朋友圈',
|
||||
),
|
||||
GlassBottomBarTab(
|
||||
icon: Icon(Icons.account_circle_outlined),
|
||||
activeIcon: Icon(Icons.account_circle),
|
||||
label: '我的',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _pages[_currentIndex],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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:food_hub_app/apis/session.dart';
|
||||
import 'package:food_hub_app/models/session.dart';
|
||||
import 'package:food_hub_app/utils/sp_util.dart';
|
||||
import 'package:food_hub_app/widgets/common/form.dart';
|
||||
import 'package:food_hub_app/widgets/common/index.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
@@ -20,6 +22,7 @@ class _LoginPage extends State<LoginPage> {
|
||||
String _username = "", _password = "";
|
||||
bool _isRemember = false;
|
||||
bool _isObscure = true;
|
||||
bool _isLoading = false;
|
||||
Color _eyeColor = Colors.grey;
|
||||
final List _loginMethod = [
|
||||
{"title": "phone", "icon": Icons.phone_android},
|
||||
@@ -34,13 +37,22 @@ class _LoginPage extends State<LoginPage> {
|
||||
}
|
||||
|
||||
Future<void> _loadRememberState() async {
|
||||
bool? isRemember = await SPUtil.getBool('isRemember');
|
||||
if (isRemember) {
|
||||
bool? isRemember = SPUtil.getBool('isRemember');
|
||||
if (isRemember == true) {
|
||||
final username = SPUtil.getString('username');
|
||||
final password = SPUtil.getString('password');
|
||||
|
||||
if (username.isNotEmpty && password.isNotEmpty) {
|
||||
setState(() {
|
||||
_isRemember = true;
|
||||
_username = SPUtil.getString('username');
|
||||
_password = SPUtil.getString('password');
|
||||
_username = username;
|
||||
_password = password;
|
||||
});
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
loginClick();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,39 +72,53 @@ class _LoginPage extends State<LoginPage> {
|
||||
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()) {
|
||||
(_formKey.currentState as FormState).save();
|
||||
Session session = await loginApi(_username, _password);
|
||||
showSuccessToast('登录成功');
|
||||
|
||||
try {
|
||||
LoadingDialog.show(context, message: '登录中');
|
||||
Session session = await loginApi(_username, _password);
|
||||
ToastUtil.success('登录成功');
|
||||
handleRememberState();
|
||||
handleTokenState(session.saToken.tokenValue);
|
||||
handleUserInfo(session.userInfo);
|
||||
|
||||
LoadingDialog.hide(context);
|
||||
Navigator.pushNamed(context, '/home');
|
||||
} else {
|
||||
showErrorToast('请先输入信息');
|
||||
} catch (e) {
|
||||
LoadingDialog.hide(context);
|
||||
ToastUtil.error(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[100],
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
// autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Column(
|
||||
children: [
|
||||
// 可滚动的主要内容区域
|
||||
Expanded(child: buildLoginForm()),
|
||||
|
||||
// 固定在底部的其他登录选项区域
|
||||
Expanded(child: buildLoginForm(context)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 50),
|
||||
child: Column(
|
||||
children: [
|
||||
buildOtherLoginText(),
|
||||
buildOtherLoginText(context),
|
||||
const SizedBox(height: 15),
|
||||
buildOtherMethod(context),
|
||||
const SizedBox(height: 20),
|
||||
@@ -114,42 +140,36 @@ class _LoginPage extends State<LoginPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildLoginForm() {
|
||||
Widget buildLoginForm(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
children: [
|
||||
const SizedBox(height: kToolbarHeight),
|
||||
buildTitle(),
|
||||
const SizedBox(height: 60),
|
||||
buildUsernameTextField(),
|
||||
buildUsernameTextField(context),
|
||||
const SizedBox(height: 20),
|
||||
buildPasswordTextField(context),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
buildRememberPasswordCheckbox(context), // 记住密码(左对齐)
|
||||
buildForgetPasswordText(context),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20), // 调整与登录按钮的间距
|
||||
const SizedBox(height: 10),
|
||||
buildRememberPasswordCheckbox(context),
|
||||
const SizedBox(height: 20),
|
||||
buildLoginButton(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildUsernameTextField() {
|
||||
Widget buildUsernameTextField(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
formLabelText(labelText: "账号:"),
|
||||
Text("账号:", style: Theme.of(context).textTheme.bodyLarge),
|
||||
const SizedBox(height: 10),
|
||||
FormBuilderTextField(
|
||||
name: 'username',
|
||||
initialValue: _username,
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: formInputDecoration(
|
||||
decoration: buildInputDecoration(
|
||||
context: context,
|
||||
hintText: "请输入账号",
|
||||
prefixIcon: Icons.person,
|
||||
),
|
||||
@@ -164,7 +184,7 @@ class _LoginPage extends State<LoginPage> {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
formLabelText(labelText: "密码:"),
|
||||
Text("密码:", style: Theme.of(context).textTheme.bodyLarge),
|
||||
const SizedBox(height: 10),
|
||||
FormBuilderTextField(
|
||||
name: 'password',
|
||||
@@ -173,22 +193,18 @@ class _LoginPage extends State<LoginPage> {
|
||||
obscureText: _isObscure,
|
||||
onSaved: (v) => _password = v!,
|
||||
validator: FormBuilderValidators.required(),
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(Icons.lock),
|
||||
decoration: buildInputDecoration(
|
||||
context: context,
|
||||
hintText: "请输入密码",
|
||||
hintStyle: TextStyle(color: Colors.grey),
|
||||
border: OutlineInputBorder(),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
prefixIcon: Icons.lock,
|
||||
).copyWith(
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(Icons.remove_red_eye, color: _eyeColor),
|
||||
onPressed: () {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
setState(() {
|
||||
_isObscure = !_isObscure;
|
||||
_eyeColor =
|
||||
(_isObscure
|
||||
? Colors.grey
|
||||
: Theme.of(context).iconTheme.color)!;
|
||||
_eyeColor = (_isObscure ? Colors.grey : colors.surface);
|
||||
});
|
||||
},
|
||||
),
|
||||
@@ -202,6 +218,8 @@ class _LoginPage extends State<LoginPage> {
|
||||
return Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
visualDensity: VisualDensity.compact,
|
||||
value: _isRemember,
|
||||
onChanged: (bool? value) {
|
||||
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) {
|
||||
return Align(
|
||||
child: SizedBox(
|
||||
return SizedBox(
|
||||
height: 45,
|
||||
width: double.infinity,
|
||||
child: TDButton(
|
||||
text: '登录',
|
||||
size: TDButtonSize.large,
|
||||
type: TDButtonType.fill,
|
||||
shape: TDButtonShape.rectangle,
|
||||
theme: TDButtonTheme.primary,
|
||||
onTap: () => loginClick(context),
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : loginClick,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: const Text(
|
||||
'登录',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildOtherLoginText() {
|
||||
return TDDivider(text: '其他方式登录', alignment: TextAlignment.center);
|
||||
Widget buildOtherLoginText(BuildContext context) {
|
||||
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(
|
||||
alignment: MainAxisAlignment.center,
|
||||
children:
|
||||
@@ -283,16 +298,19 @@ class _LoginPage extends State<LoginPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildRegisterText(context) {
|
||||
Widget buildRegisterText(BuildContext context) {
|
||||
return Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('没有账号?', style: TextStyle(fontSize: 18)),
|
||||
Text('没有账号?', style: Theme.of(context).textTheme.bodyMedium),
|
||||
GestureDetector(
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'点击注册',
|
||||
style: TextStyle(fontSize: 18, color: Colors.green),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
print("点击注册");
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:easy_refresh/easy_refresh.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:food_hub_app/apis/moment.dart';
|
||||
import 'package:food_hub_app/models/moment.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:tdesign_flutter/tdesign_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class MomentPage extends StatefulWidget {
|
||||
const MomentPage({super.key});
|
||||
@@ -13,26 +14,23 @@ class MomentPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MomentPageState extends State<MomentPage> {
|
||||
List<Moment> momentList = [];
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 3;
|
||||
bool _hasMore = true;
|
||||
|
||||
// 初始化EasyRefresh控制器
|
||||
final EasyRefreshController _freshController = EasyRefreshController(
|
||||
controlFinishRefresh: true,
|
||||
controlFinishLoad: true,
|
||||
);
|
||||
|
||||
bool _showScrollToTop = false;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
bool _showScrollToTop = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 初始加载数据
|
||||
_loadData(isRefresh: true);
|
||||
_scrollController.addListener(_onScroll);
|
||||
|
||||
// 初始化加载数据
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<FoodProvider>().refreshMomentList();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -43,50 +41,17 @@ class _MomentPageState extends State<MomentPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// 统一的数据加载方法
|
||||
Future<void> _loadData({required bool isRefresh}) async {
|
||||
try {
|
||||
// 如果是刷新,重置页码
|
||||
if (isRefresh) {
|
||||
_currentPage = 1;
|
||||
}
|
||||
|
||||
final result = await queryMomentByPageApi(_currentPage, _pageSize);
|
||||
|
||||
setState(() {
|
||||
if (isRefresh) {
|
||||
// 刷新时直接替换数据
|
||||
momentList = result.records;
|
||||
} else {
|
||||
// 加载更多时追加数据
|
||||
momentList.addAll(result.records);
|
||||
}
|
||||
|
||||
// 判断是否还有更多数据
|
||||
_hasMore = result.current < result.pages;
|
||||
// 如果有更多数据,准备加载下一页
|
||||
if (_hasMore) {
|
||||
_currentPage++;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// 处理错误
|
||||
debugPrint('加载数据失败: $e');
|
||||
} finally {
|
||||
_freshController.finishRefresh();
|
||||
_freshController.resetFooter();
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
Future<void> _onRefresh() async {
|
||||
await _loadData(isRefresh: true);
|
||||
await context.read<FoodProvider>().refreshMomentList();
|
||||
_freshController.finishRefresh();
|
||||
}
|
||||
|
||||
// 上拉加载
|
||||
Future<void> _onLoad() async {
|
||||
if (_hasMore) {
|
||||
await _loadData(isRefresh: false);
|
||||
final provider = context.read<FoodProvider>();
|
||||
if (provider.hasMore) {
|
||||
await provider.loadMoreMomentList();
|
||||
_freshController.finishLoad(IndicatorResult.success);
|
||||
} else {
|
||||
_freshController.finishLoad(IndicatorResult.noMore);
|
||||
@@ -94,8 +59,8 @@ class _MomentPageState extends State<MomentPage> {
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
// 当滚动距离超过300时显示返回顶部按钮
|
||||
if (_scrollController.offset > 300) {
|
||||
// 当滚动距离超过时显示返回顶部按钮
|
||||
if (_scrollController.offset > 100) {
|
||||
if (!_showScrollToTop) {
|
||||
setState(() {
|
||||
_showScrollToTop = true;
|
||||
@@ -111,84 +76,62 @@ class _MomentPageState extends State<MomentPage> {
|
||||
}
|
||||
|
||||
// 滚动到顶部
|
||||
void _scrollToTop() {
|
||||
_scrollController.animateTo(
|
||||
0,
|
||||
duration: const Duration(milliseconds: 500), // 滚动动画时长
|
||||
curve: Curves.easeInOut, // 滚动动画曲线
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<FoodProvider>();
|
||||
|
||||
// 空状态显示
|
||||
if (momentList.isEmpty) {
|
||||
return EasyRefresh(
|
||||
controller: _freshController,
|
||||
onRefresh: _onRefresh,
|
||||
child: const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据'),
|
||||
);
|
||||
if (provider.momentList.isEmpty) {
|
||||
return buildEmptyData();
|
||||
}
|
||||
|
||||
// 有数据时显示列表
|
||||
return Stack(
|
||||
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: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: EasyRefresh(
|
||||
controller: _freshController,
|
||||
header: ClassicHeader(
|
||||
dragText: '下拉刷新',
|
||||
armedText: '释放刷新',
|
||||
readyText: '准备刷新',
|
||||
processingText: '刷新中...',
|
||||
processedText: '刷新完成',
|
||||
failedText: '刷新失败',
|
||||
noMoreText: '没有更多数据',
|
||||
showText: true,
|
||||
messageText: '更新于 %T',
|
||||
showMessage: true,
|
||||
),
|
||||
footer: ClassicFooter(
|
||||
dragText: '上拉加载',
|
||||
armedText: '释放加载',
|
||||
readyText: '准备加载',
|
||||
processingText: '加载中...',
|
||||
processedText: '加载完成',
|
||||
failedText: '加载失败',
|
||||
noMoreText: '没有更多数据',
|
||||
showText: true,
|
||||
messageText: '更新于 %T',
|
||||
showMessage: true,
|
||||
),
|
||||
buildEasyRefresh(
|
||||
freshController: _freshController,
|
||||
onRefresh: _onRefresh,
|
||||
onLoad: _onLoad,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: momentList.length,
|
||||
itemBuilder: (context, index) {
|
||||
return MomentCard(moment: momentList[index]);
|
||||
},
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
if (provider.isLoading)
|
||||
buildLoadingIndicator()
|
||||
else
|
||||
_buildMomentList(provider),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 返回顶部按钮
|
||||
if (_showScrollToTop)
|
||||
Positioned(
|
||||
right: 10,
|
||||
bottom: 20,
|
||||
child: FloatingActionButton(
|
||||
onPressed: _scrollToTop,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 5,
|
||||
mini: true,
|
||||
child: const Icon(
|
||||
Icons.arrow_upward
|
||||
),
|
||||
),
|
||||
),
|
||||
buildScrollToTop(context: context, scrollToTop: _scrollToTop),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
227
lib/views/moment_form.dart
Normal file
227
lib/views/moment_form.dart
Normal file
@@ -0,0 +1,227 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/utils/log_utils.dart';
|
||||
import 'package:flutter_common/utils/toast_util.dart';
|
||||
import 'package:flutter_common/widget/dialog_widget.dart';
|
||||
import 'package:flutter_common/widget/loading_widget.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:food_hub_app/config/app_config.dart';
|
||||
import 'package:food_hub_app/provider/food_provider.dart';
|
||||
import 'package:food_hub_app/utils/minio_utils.dart';
|
||||
import 'package:food_hub_app/widgets/common/form.dart';
|
||||
import 'package:food_hub_app/widgets/common/image.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class MomentForm extends StatefulWidget {
|
||||
const MomentForm({super.key});
|
||||
|
||||
@override
|
||||
State<MomentForm> createState() => _MomentFormState();
|
||||
}
|
||||
|
||||
class _MomentFormState extends State<MomentForm> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
final _focusNode = FocusNode();
|
||||
static const String _contentField = 'content';
|
||||
static const int maxContentLength = 200;
|
||||
static const int maxImageCount = 9; // 最大图片数量
|
||||
|
||||
Widget _buildContentField(FoodProvider provider) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return FormBuilderTextField(
|
||||
name: _contentField,
|
||||
initialValue: provider.momentFormItem.content,
|
||||
focusNode: _focusNode,
|
||||
maxLines: 8,
|
||||
minLines: 5,
|
||||
maxLength: maxContentLength,
|
||||
buildCounter: (
|
||||
context, {
|
||||
required currentLength,
|
||||
required isFocused,
|
||||
maxLength,
|
||||
}) {
|
||||
return Text(
|
||||
'$currentLength/$maxContentLength',
|
||||
style: TextStyle(
|
||||
color: currentLength > maxLength! ? Colors.red : colors.primary,
|
||||
),
|
||||
);
|
||||
},
|
||||
onChanged: (value) {
|
||||
provider.updateMomentFormItem(content: value);
|
||||
},
|
||||
decoration: buildInputDecoration(context: context, hintText: '请输入朋友圈内容'),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '请输入朋友圈内容';
|
||||
}
|
||||
if (value.length > maxContentLength) {
|
||||
return '内容不能超过$maxContentLength字';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageField(FoodProvider provider) {
|
||||
final imageUrls = provider.momentFormItem.imageList;
|
||||
final totalItems =
|
||||
imageUrls.length + (imageUrls.length < maxImageCount ? 1 : 0);
|
||||
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
childAspectRatio: 4 / 3,
|
||||
),
|
||||
itemCount: totalItems,
|
||||
itemBuilder: (context, index) {
|
||||
if (index < imageUrls.length) {
|
||||
return ImagePreview(
|
||||
imageUrls: imageUrls,
|
||||
index: index,
|
||||
onRemoveImage: () => _removeImage(provider, index),
|
||||
);
|
||||
}
|
||||
// 上传按钮(最后一个)
|
||||
else {
|
||||
return buildImageUploadButton(
|
||||
context: context,
|
||||
onTap: () => _pickImages(provider),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormBuilder(FoodProvider provider) {
|
||||
return FormBuilder(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buildFormLabel(context: context, text: '朋友圈内容', isRequired: true),
|
||||
const SizedBox(height: 8),
|
||||
_buildContentField(provider),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
buildFormLabel(
|
||||
context: context,
|
||||
text: '上传图片(最多9张)',
|
||||
isRequired: false,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildImageField(provider),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
buildFormButtonGroup(
|
||||
context: context,
|
||||
isShowDelete: provider.isEditing,
|
||||
onConfirm: () => _submitForm(provider),
|
||||
onDelete: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 选择多张图片
|
||||
Future<void> _pickImages(FoodProvider provider) async {
|
||||
final currentCount = provider.momentFormItem.imageList.length;
|
||||
final remainingCount = maxImageCount - currentCount;
|
||||
|
||||
if (remainingCount <= 0) {
|
||||
ToastUtil.error('最多只能上传$maxImageCount张图片');
|
||||
return;
|
||||
}
|
||||
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['jpg', 'jpeg', 'png'],
|
||||
allowMultiple: true,
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
// 限制选择数量
|
||||
final filesToUpload = result.files.take(remainingCount).toList();
|
||||
LoadingDialog.show(context, message: '上传中');
|
||||
|
||||
try {
|
||||
final newImageUrls = <String>[];
|
||||
// 逐个上传图片
|
||||
for (final file in filesToUpload) {
|
||||
final fileName = await MinIOHelper().uploadFile(
|
||||
bucketName: AppConfig.bucketName,
|
||||
file: file,
|
||||
);
|
||||
logger.i('图片名称:$fileName');
|
||||
newImageUrls.add(fileName);
|
||||
}
|
||||
|
||||
// 更新图片列表
|
||||
provider.addMomentFormImage(newImageUrls);
|
||||
} catch (e) {
|
||||
ToastUtil.error(e.toString());
|
||||
} finally {
|
||||
LoadingDialog.hide(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 移除单张图片
|
||||
void _removeImage(FoodProvider provider, int index) {
|
||||
provider.removeMomentFormImage(index);
|
||||
}
|
||||
|
||||
/// 提交表单
|
||||
void _submitForm(FoodProvider provider) async {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
LoadingDialog.show(context, message: '提交中');
|
||||
final result = await provider.handleMoment();
|
||||
if (result == true) {
|
||||
await provider.refreshMomentList();
|
||||
LoadingDialog.hide(context);
|
||||
|
||||
if (provider.isEditing) {
|
||||
showSuccessTip(context, '更新朋友圈成功');
|
||||
} else {
|
||||
showSuccessTip(context, '新增朋友圈成功');
|
||||
}
|
||||
|
||||
Future.delayed(Duration(milliseconds: 1500), () {
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
LoadingDialog.hide(context);
|
||||
if (provider.isEditing) {
|
||||
showErrorTip(context, '更新朋友圈失败');
|
||||
} else {
|
||||
showErrorTip(context, '新增朋友圈失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<FoodProvider>();
|
||||
|
||||
return AnimatedPadding(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: MediaQuery.of(context).viewInsets,
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: _buildFormBuilder(provider),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
59
lib/views/moment_user.dart
Normal file
59
lib/views/moment_user.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/utils/sp_utils.dart';
|
||||
import 'package:flutter_common/widget/common_widget.dart';
|
||||
import 'package:food_hub_app/provider/food_provider.dart';
|
||||
import 'package:food_hub_app/widgets/moment/list.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class MomentUserPage extends StatefulWidget {
|
||||
const MomentUserPage({super.key});
|
||||
|
||||
@override
|
||||
State<MomentUserPage> createState() => _MomentUserPageState();
|
||||
}
|
||||
|
||||
class _MomentUserPageState extends State<MomentUserPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// 初始化加载数据
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<FoodProvider>().queryMomentByUserId(SPUtil.getInt('userId'));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<FoodProvider>();
|
||||
|
||||
// 空状态显示
|
||||
if (provider.momentList.isEmpty) {
|
||||
return buildEmptyData();
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
title: Text('我的朋友圈', style: Theme.of(context).textTheme.titleLarge),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
body:
|
||||
provider.isLoading
|
||||
? buildLoadingIndicator()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: MomentList(
|
||||
momentList: provider.momentList,
|
||||
isUser: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,182 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_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 {
|
||||
const ProfilePage({super.key});
|
||||
|
||||
@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
|
||||
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
254
lib/views/profile_form.dart
Normal file
@@ -0,0 +1,254 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/widget/dialog_widget.dart';
|
||||
import 'package:flutter_common/widget/loading_widget.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_input_chips/flutter_input_chips.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:food_hub_app/provider/user_provider.dart';
|
||||
import 'package:food_hub_app/widgets/common/form.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ProfileForm extends StatefulWidget {
|
||||
const ProfileForm({super.key});
|
||||
|
||||
@override
|
||||
State<ProfileForm> createState() => _ProfileFormState();
|
||||
}
|
||||
|
||||
class _ProfileFormState extends State<ProfileForm> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Widget _buildFormBuilder(UserProvider provider) {
|
||||
final double sizeBoxHeight = 8;
|
||||
|
||||
return FormBuilder(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buildFormLabel(context: context, text: '用户名', isRequired: true),
|
||||
SizedBox(height: sizeBoxHeight),
|
||||
FormBuilderTextField(
|
||||
name: 'username',
|
||||
initialValue: provider.userFormItem.username,
|
||||
onChanged: (value) {
|
||||
provider.updateUserFormItem(username: value);
|
||||
},
|
||||
decoration: buildInputDecoration(
|
||||
context: context,
|
||||
hintText: '请输入用户名',
|
||||
),
|
||||
validator: FormBuilderValidators.required(errorText: '请输入用户名'),
|
||||
),
|
||||
SizedBox(height: sizeBoxHeight),
|
||||
|
||||
buildFormLabel(context: context, text: '性别', isRequired: true),
|
||||
SizedBox(height: sizeBoxHeight),
|
||||
FormBuilderDropdown<int>(
|
||||
name: 'gender',
|
||||
initialValue: provider.userFormItem.gender,
|
||||
decoration: buildInputDecoration(
|
||||
context: context,
|
||||
hintText: '请选择性别',
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 1, child: Text('男')),
|
||||
DropdownMenuItem(value: 2, child: Text('女')),
|
||||
],
|
||||
onChanged: (value) {
|
||||
provider.updateUserFormItem(gender: value);
|
||||
},
|
||||
),
|
||||
SizedBox(height: sizeBoxHeight),
|
||||
|
||||
buildFormLabel(context: context, text: '手机号', isRequired: true),
|
||||
SizedBox(height: sizeBoxHeight),
|
||||
FormBuilderTextField(
|
||||
name: 'phoneNumber',
|
||||
initialValue: provider.userFormItem.phoneNumber,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: buildInputDecoration(
|
||||
context: context,
|
||||
hintText: '请输入手机号',
|
||||
),
|
||||
onChanged: (value) {
|
||||
provider.updateUserFormItem(phoneNumber: value);
|
||||
},
|
||||
validator: FormBuilderValidators.compose([
|
||||
FormBuilderValidators.required(errorText: '请输入手机号'),
|
||||
FormBuilderValidators.match(
|
||||
RegExp(r'^1[3-9]\d{9}$'),
|
||||
errorText: '请输入正确的11位手机号',
|
||||
),
|
||||
]),
|
||||
),
|
||||
SizedBox(height: sizeBoxHeight),
|
||||
|
||||
buildFormLabel(context: context, text: '邮箱', isRequired: true),
|
||||
SizedBox(height: sizeBoxHeight),
|
||||
FormBuilderTextField(
|
||||
name: 'email',
|
||||
initialValue: provider.userFormItem.email,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: buildInputDecoration(
|
||||
context: context,
|
||||
hintText: '请输入邮箱',
|
||||
),
|
||||
onChanged: (value) {
|
||||
provider.updateUserFormItem(email: value);
|
||||
},
|
||||
validator: FormBuilderValidators.compose([
|
||||
FormBuilderValidators.email(errorText: '邮箱格式不正确'),
|
||||
]),
|
||||
),
|
||||
SizedBox(height: sizeBoxHeight),
|
||||
|
||||
buildFormLabel(context: context, text: '出生日期', isRequired: true),
|
||||
SizedBox(height: sizeBoxHeight),
|
||||
FormBuilderDateTimePicker(
|
||||
name: 'birthDate',
|
||||
initialValue: provider.userFormItem.birthDate,
|
||||
inputType: InputType.date,
|
||||
format: DateFormat('yyyy-MM-dd'),
|
||||
firstDate: DateTime(1900),
|
||||
lastDate: DateTime.now(),
|
||||
decoration: buildInputDecoration(
|
||||
context: context,
|
||||
hintText: '请选择出生日期',
|
||||
),
|
||||
onChanged: (value) {
|
||||
provider.updateUserFormItem(birthDate: value);
|
||||
},
|
||||
),
|
||||
SizedBox(height: sizeBoxHeight),
|
||||
|
||||
buildFormLabel(context: context, text: '详细地址', isRequired: false),
|
||||
SizedBox(height: sizeBoxHeight),
|
||||
FormBuilderTextField(
|
||||
name: 'address',
|
||||
initialValue: provider.userFormItem.address,
|
||||
decoration: buildInputDecoration(
|
||||
context: context,
|
||||
hintText: '请输入详细地址',
|
||||
),
|
||||
onChanged: (value) {
|
||||
provider.updateUserFormItem(address: value);
|
||||
},
|
||||
),
|
||||
SizedBox(height: sizeBoxHeight),
|
||||
|
||||
buildFormLabel(context: context, text: '职业', isRequired: false),
|
||||
SizedBox(height: sizeBoxHeight),
|
||||
FormBuilderTextField(
|
||||
name: 'job',
|
||||
initialValue: provider.userFormItem.job,
|
||||
decoration: buildInputDecoration(
|
||||
context: context,
|
||||
hintText: '请输入职业',
|
||||
),
|
||||
onChanged: (value) {
|
||||
provider.updateUserFormItem(job: value);
|
||||
},
|
||||
),
|
||||
SizedBox(height: sizeBoxHeight),
|
||||
|
||||
buildFormLabel(context: context, text: '标签', isRequired: false),
|
||||
SizedBox(height: sizeBoxHeight),
|
||||
FormBuilderField<List<String>>(
|
||||
name: 'tags',
|
||||
initialValue: provider.userFormItem.tags,
|
||||
builder: (FormFieldState<List<String>> field) {
|
||||
return FlutterInputChips(
|
||||
padding: EdgeInsets.all(0),
|
||||
initialValue: field.value ?? [],
|
||||
onChanged: (value) {
|
||||
provider.updateUserFormItem(tags: value);
|
||||
},
|
||||
inputDecoration: buildInputDecoration(
|
||||
context: context,
|
||||
hintText: '请输入标签,多个标签用回车键隔开',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(height: sizeBoxHeight),
|
||||
|
||||
buildFormLabel(context: context, text: '简介', isRequired: false),
|
||||
SizedBox(height: sizeBoxHeight),
|
||||
FormBuilderTextField(
|
||||
name: 'description',
|
||||
initialValue: provider.userFormItem.description,
|
||||
decoration: buildInputDecoration(
|
||||
context: context,
|
||||
hintText: '请输入简介',
|
||||
),
|
||||
onChanged: (value) {
|
||||
provider.updateUserFormItem(description: value);
|
||||
},
|
||||
),
|
||||
SizedBox(height: sizeBoxHeight),
|
||||
|
||||
buildFormButtonGroup(
|
||||
context: context,
|
||||
isShowDelete: false,
|
||||
onConfirm: () => _submitForm(provider),
|
||||
onDelete: () => {},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 提交表单
|
||||
void _submitForm(UserProvider provider) async {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
LoadingDialog.show(context, message: '提交中');
|
||||
|
||||
final result = await provider.handleUser();
|
||||
if (result == true) {
|
||||
LoadingDialog.hide(context);
|
||||
|
||||
showSuccessTip(context, '更新信息成功');
|
||||
|
||||
Future.delayed(Duration(milliseconds: 1500), () {
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
LoadingDialog.hide(context);
|
||||
showErrorTip(context, '更新信息失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<UserProvider>();
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('基本信息', style: Theme.of(context).textTheme.titleLarge),
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: colors.primary),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: _buildFormBuilder(provider),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
109
lib/views/profile_user.dart
Normal file
109
lib/views/profile_user.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/widget/common_widget.dart';
|
||||
import 'package:food_hub_app/provider/user_provider.dart';
|
||||
import 'package:food_hub_app/widgets/common/index.dart';
|
||||
import 'package:food_hub_app/widgets/moment/list.dart';
|
||||
import 'package:food_hub_app/widgets/profile/basic_info.dart';
|
||||
import 'package:food_hub_app/widgets/recipe/list.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
enum ProfileTab {
|
||||
profile('基础信息'),
|
||||
moment('朋友圈'),
|
||||
recipe('菜谱');
|
||||
|
||||
final String label;
|
||||
|
||||
const ProfileTab(this.label);
|
||||
}
|
||||
|
||||
class ProfileUserPage extends StatefulWidget {
|
||||
const ProfileUserPage({super.key});
|
||||
|
||||
@override
|
||||
State<ProfileUserPage> createState() => ProfileUserPageState();
|
||||
}
|
||||
|
||||
class ProfileUserPageState extends State<ProfileUserPage> {
|
||||
late ProfileTab currentTab = ProfileTab.profile;
|
||||
|
||||
void _onTabChange(ProfileTab tab) {
|
||||
setState(() {
|
||||
currentTab = tab;
|
||||
});
|
||||
|
||||
// provider.onRecordTabChange();
|
||||
}
|
||||
|
||||
Widget _buildTabs({
|
||||
required BuildContext context,
|
||||
required ProfileTab currentTab,
|
||||
required ValueChanged<ProfileTab> onTabChanged,
|
||||
}) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: buildToggleSwitch(
|
||||
context: context,
|
||||
currentTab: currentTab,
|
||||
tabValues: ProfileTab.values,
|
||||
labels: ProfileTab.values.map((e) => e.label).toList(),
|
||||
onTabChanged: onTabChanged,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(UserProvider provider) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildTabs(
|
||||
context: context,
|
||||
currentTab: currentTab,
|
||||
onTabChanged: (tab) => _onTabChange(tab),
|
||||
),
|
||||
Expanded(
|
||||
child: IndexedStack(
|
||||
index: currentTab.index,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity, child: ProfileInfo(user: provider.currentUser)
|
||||
),
|
||||
MomentList(momentList: provider.momentList, isUser: false),
|
||||
RecipeList(userId: provider.currentUser.id!),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<UserProvider>();
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'${provider.currentUser.username}的个人信息',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: colors.primary),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Stack(
|
||||
children: [
|
||||
if (provider.isLoading)
|
||||
buildLoadingIndicator()
|
||||
else
|
||||
_buildContent(provider),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
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});
|
||||
@@ -11,28 +16,11 @@ class RecipeDetailPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||
RecipeDetail recipe = RecipeDetail(
|
||||
id: 0,
|
||||
name: '',
|
||||
category: '',
|
||||
recommendRate: 0,
|
||||
remark: '',
|
||||
isShare: false,
|
||||
userId: 0,
|
||||
username: '',
|
||||
avatar: '',
|
||||
materialList: [],
|
||||
stepList: [],
|
||||
recordList: [],
|
||||
likeList: [],
|
||||
favouriteList: [],
|
||||
commentList: [],
|
||||
);
|
||||
Recipe recipe = Recipe.getEmpty();
|
||||
final GlobalKey _recipeDetailKey = GlobalKey();
|
||||
bool _isSharing = false;
|
||||
|
||||
// 定义三个分类列表
|
||||
List<RecipeMaterial> mainMaterialList = []; // 主料
|
||||
List<RecipeMaterial> auxiliaryMaterialList = []; // 配料
|
||||
List<RecipeMaterial> accessoryMaterialList = []; // 辅料
|
||||
List<RecipeMaterial> materialList = [];
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
@@ -43,8 +31,7 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||
Future<void> refreshRecipeDetail() async {
|
||||
// 获取参数
|
||||
final args = ModalRoute.of(context)?.settings.arguments as Map;
|
||||
final recipeId = args['id'];
|
||||
final result = await queryRecipeByIdApi(recipeId);
|
||||
final result = await queryRecipeByIdApi(args['id']);
|
||||
|
||||
setState(() {
|
||||
recipe = result;
|
||||
@@ -53,35 +40,78 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||
}
|
||||
|
||||
void getRecipeMaterial() {
|
||||
materialList.clear();
|
||||
|
||||
for (var material in recipe.materialList) {
|
||||
switch (material.type) {
|
||||
case '主料':
|
||||
mainMaterialList.add(material);
|
||||
break;
|
||||
case '配料':
|
||||
auxiliaryMaterialList.add(material);
|
||||
break;
|
||||
case '辅料':
|
||||
accessoryMaterialList.add(material);
|
||||
break;
|
||||
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: TextStyle(color: Colors.white)),
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
title: Text('菜谱信息', style: Theme.of(context).textTheme.titleLarge),
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: Colors.white),
|
||||
icon: Icon(Icons.arrow_back, color: colors.primary),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
backgroundColor: Color(0xFFF5F5F5),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(padding: EdgeInsets.all(5), child: _buildRecipeDetail()),
|
||||
child: Column(
|
||||
children: [
|
||||
// 用RepaintBoundary包裹要截图的部分
|
||||
RepaintBoundary(
|
||||
key: _recipeDetailKey,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: _buildRecipeDetail(),
|
||||
),
|
||||
),
|
||||
_buildButtons(provider),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -91,35 +121,47 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_buildTitleSection(),
|
||||
SizedBox(height: 10),
|
||||
SizedBox(height: 8),
|
||||
_buildInfoCard(
|
||||
context,
|
||||
icon: Icons.info,
|
||||
title: "基础信息",
|
||||
content: _buildTag(context, title: recipe.category),
|
||||
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(context, '主料', mainMaterialList),
|
||||
_buildMaterialSection(context, '辅料', auxiliaryMaterialList),
|
||||
_buildMaterialSection(context, '调料', accessoryMaterialList),
|
||||
],
|
||||
children: [_buildMaterialSection(materialList)],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
_buildInfoCard(
|
||||
context,
|
||||
icon: Icons.list,
|
||||
title: "步骤信息",
|
||||
content: Column(
|
||||
children:
|
||||
recipe.stepList.map((step) => _buildStepSection(step)).toList(),
|
||||
),
|
||||
content: Column(children: [_buildStepSection(recipe.stepList)]),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
_buildInfoCard(
|
||||
context,
|
||||
icon: Icons.note,
|
||||
@@ -133,20 +175,6 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTag(BuildContext context, {required String title}) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Color.lerp(Theme.of(context).primaryColor, Colors.white, 0.8),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(color: Theme.of(context).primaryColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTitleSection() {
|
||||
return Text(
|
||||
recipe.name,
|
||||
@@ -154,36 +182,43 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMaterialSection(
|
||||
BuildContext context,
|
||||
String title,
|
||||
List<RecipeMaterial> materials,
|
||||
) {
|
||||
Widget _buildMaterialSection(List<RecipeMaterial> materials) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8, top: 12),
|
||||
child: Text(title, style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
if (materials.isEmpty)
|
||||
_buildTag(context, title: '无')
|
||||
buildTag(context, '无')
|
||||
else
|
||||
Wrap(
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children:
|
||||
materials
|
||||
.map(
|
||||
(m) => _buildTag(context, title: '${m.name} ${m.amount}'),
|
||||
)
|
||||
.toList(),
|
||||
children: materials.map((m) => _buildMaterialItem(m)).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStepSection(RecipeStep step) {
|
||||
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: [
|
||||
@@ -201,16 +236,6 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (step.imageUrl.isNotEmpty)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
step.imageUrl,
|
||||
width: double.infinity,
|
||||
height: 180,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -224,13 +249,13 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||
required String title,
|
||||
required Widget content,
|
||||
}) {
|
||||
return cardContainer(
|
||||
Column(
|
||||
return CommonCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: Theme.of(context).primaryColor),
|
||||
Icon(icon, color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
title,
|
||||
@@ -244,4 +269,23 @@ class _RecipeDetailState extends State<RecipeDetailPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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
569
lib/views/recipe_form.dart
Normal file
@@ -0,0 +1,569 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/widget/dialog_widget.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:food_hub_app/models/recipe.dart';
|
||||
import 'package:food_hub_app/provider/food_provider.dart';
|
||||
import 'package:food_hub_app/widgets/common/form.dart';
|
||||
import 'package:food_hub_app/widgets/common/index.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:flutter_common/utils/toast_util.dart';
|
||||
import 'package:flutter_common/widget/loading_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RecipeForm extends StatefulWidget {
|
||||
const RecipeForm({super.key});
|
||||
|
||||
@override
|
||||
State<RecipeForm> createState() => _RecipeFormState();
|
||||
}
|
||||
|
||||
class _RecipeFormState extends State<RecipeForm> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
int _currentStep = 0;
|
||||
|
||||
// 表单字段名称常量
|
||||
static const String _nameField = 'name';
|
||||
static const String _categoryField = 'category';
|
||||
static const String _recommendRateField = 'recommendRate';
|
||||
static const String _remarkField = 'remark';
|
||||
static const String _isShareField = 'isShare';
|
||||
|
||||
// 分类选项
|
||||
final List<String> _categoryOptions = [
|
||||
'家常菜',
|
||||
'川菜',
|
||||
'粤菜',
|
||||
'湘菜',
|
||||
'西餐',
|
||||
'甜品',
|
||||
'汤羹',
|
||||
'其他',
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
// 构建步骤内容
|
||||
Widget _buildStepContent(FoodProvider provider) {
|
||||
switch (_currentStep) {
|
||||
case 0:
|
||||
return _buildInfo(provider);
|
||||
case 1:
|
||||
return _buildMaterials(provider);
|
||||
case 2:
|
||||
return _buildSteps(provider);
|
||||
default:
|
||||
return _buildInfo(provider);
|
||||
}
|
||||
}
|
||||
|
||||
// 第一步:基本信息
|
||||
Widget _buildInfo(FoodProvider provider) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buildFormLabel(context: context, text: '菜谱名称', isRequired: true),
|
||||
const SizedBox(height: 8),
|
||||
FormBuilderTextField(
|
||||
name: _nameField,
|
||||
initialValue: provider.recipeFormItem.name,
|
||||
decoration: buildInputDecoration(
|
||||
context: context,
|
||||
hintText: '请输入菜谱名称',
|
||||
prefixIcon: Icons.restaurant_menu,
|
||||
),
|
||||
onChanged: (value) {
|
||||
provider.updateRecipeFormItem(name: value ?? '');
|
||||
},
|
||||
validator: FormBuilderValidators.compose([
|
||||
FormBuilderValidators.required(errorText: '菜谱名称不能为空'),
|
||||
FormBuilderValidators.maxLength(50, errorText: '名称不能超过50个字符'),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
buildFormLabel(context: context, text: '分类', isRequired: true),
|
||||
const SizedBox(height: 8),
|
||||
FormBuilderDropdown(
|
||||
name: _categoryField,
|
||||
initialValue: provider.recipeFormItem.category,
|
||||
decoration: buildInputDecoration(
|
||||
context: context,
|
||||
hintText: '请选择分类',
|
||||
prefixIcon: Icons.widgets,
|
||||
),
|
||||
items:
|
||||
_categoryOptions
|
||||
.map(
|
||||
(category) => DropdownMenuItem(
|
||||
value: category,
|
||||
child: Text(category),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
provider.updateRecipeFormItem(category: value ?? '');
|
||||
},
|
||||
validator: FormBuilderValidators.required(errorText: '请选择分类'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
buildFormLabel(context: context, text: '推荐评分', isRequired: false),
|
||||
const SizedBox(height: 8),
|
||||
FormBuilderSlider(
|
||||
name: _recommendRateField,
|
||||
initialValue: provider.recipeFormItem.recommendRate,
|
||||
min: 1,
|
||||
max: 5,
|
||||
divisions: 4,
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
decoration: const InputDecoration(border: InputBorder.none),
|
||||
onChanged: (value) {
|
||||
provider.updateRecipeFormItem(recommendRate: value ?? 0);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
buildFormLabel(context: context, text: '备注', isRequired: false),
|
||||
const SizedBox(height: 8),
|
||||
FormBuilderTextField(
|
||||
name: _remarkField,
|
||||
initialValue: provider.recipeFormItem.remark,
|
||||
maxLines: 3,
|
||||
decoration: buildInputDecoration(
|
||||
context: context,
|
||||
hintText: '请输入菜谱的特别说明或小贴士',
|
||||
),
|
||||
onChanged: (value) {
|
||||
provider.updateRecipeFormItem(remark: value ?? '');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
FormBuilderCheckbox(
|
||||
name: _isShareField,
|
||||
initialValue: provider.recipeFormItem.isShare,
|
||||
title: const Text('公开分享此菜谱'),
|
||||
onChanged: (value) {
|
||||
provider.updateRecipeFormItem(isShare: value ?? false);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 第二步:食材清单
|
||||
Widget _buildMaterials(FoodProvider provider) {
|
||||
final materials = provider.recipeFormItem.materialList;
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text('食材清单', style: Theme.of(context).textTheme.titleMedium),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: () => provider.addRecipeMaterial(),
|
||||
tooltip: '添加食材',
|
||||
),
|
||||
],
|
||||
),
|
||||
if (materials.isEmpty)
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
margin: EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(Icons.inventory_2, size: 48, color: Colors.grey[400]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'暂无食材,请点击添加按钮添加食材',
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Column(
|
||||
children:
|
||||
materials.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final material = entry.value;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildMaterialItem(index, material, provider),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMaterialItem(
|
||||
int index,
|
||||
RecipeMaterial material,
|
||||
FoodProvider provider,
|
||||
) {
|
||||
return Column(
|
||||
key: ValueKey(material.id),
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: material.name,
|
||||
decoration: buildInputDecoration(
|
||||
context: context,
|
||||
hintText: '请输入名称',
|
||||
prefixIcon: Icons.content_paste,
|
||||
),
|
||||
onChanged: (value) {
|
||||
provider.updateRecipeMaterial(index, name: value);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: material.amount,
|
||||
decoration: buildInputDecoration(
|
||||
context: context,
|
||||
hintText: '请输入用量',
|
||||
prefixIcon: Icons.scale,
|
||||
),
|
||||
onChanged: (value) {
|
||||
provider.updateRecipeMaterial(index, amount: value);
|
||||
},
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.red, size: 20),
|
||||
onPressed: () => provider.removeRecipeMaterial(index),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 第三步:制作步骤
|
||||
Widget _buildSteps(FoodProvider provider) {
|
||||
final steps = provider.recipeFormItem.stepList;
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text('制作步骤', style: Theme.of(context).textTheme.titleMedium),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: () => provider.addRecipeStep(),
|
||||
tooltip: '添加步骤',
|
||||
),
|
||||
],
|
||||
),
|
||||
if (steps.isEmpty)
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
margin: EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(Icons.list_alt, size: 48, color: Colors.grey[400]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'暂无步骤,请点击添加按钮添加制作步骤',
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Column(
|
||||
children:
|
||||
steps.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final step = entry.value;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildStepItem(index, step, provider),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStepItem(int index, RecipeStep step, FoodProvider provider) {
|
||||
return Row(
|
||||
key: ValueKey(step.id),
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// 步骤编号
|
||||
Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Text(
|
||||
'${index + 1}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 步骤内容
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: step.content,
|
||||
decoration: buildInputDecoration(
|
||||
context: context,
|
||||
hintText: '请输入内容',
|
||||
prefixIcon: Icons.content_paste,
|
||||
),
|
||||
onChanged: (value) {
|
||||
provider.updateRecipeStep(index, content: value);
|
||||
},
|
||||
),
|
||||
),
|
||||
// 删除按钮
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
onPressed: () => provider.removeRecipeStep(index),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 步骤指示器
|
||||
Widget _buildStepIndicator() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_buildStepCircle(0),
|
||||
SizedBox(width: 10),
|
||||
_buildStepConnector(0),
|
||||
SizedBox(width: 10),
|
||||
_buildStepCircle(1),
|
||||
SizedBox(width: 10),
|
||||
_buildStepConnector(1),
|
||||
SizedBox(width: 10),
|
||||
_buildStepCircle(2),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_buildStepTitle(0, '基本信息'),
|
||||
SizedBox(width: 40),
|
||||
_buildStepTitle(1, '食材清单'),
|
||||
SizedBox(width: 40),
|
||||
_buildStepTitle(2, '制作步骤'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 只构建圆圈,不包含标题文字
|
||||
Widget _buildStepCircle(int stepIndex) {
|
||||
final isActive = _currentStep == stepIndex;
|
||||
final isCompleted = _currentStep > stepIndex;
|
||||
final boxColor =
|
||||
isActive || isCompleted
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.grey;
|
||||
|
||||
return Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(color: boxColor, shape: BoxShape.circle),
|
||||
child: Center(
|
||||
child:
|
||||
isCompleted
|
||||
? const Icon(Icons.check, color: Colors.white, size: 16)
|
||||
: Text(
|
||||
'${stepIndex + 1}',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 箭头连接器
|
||||
Widget _buildStepConnector(int stepIndex) {
|
||||
final isActive = _currentStep > stepIndex;
|
||||
return SizedBox(
|
||||
width: 40,
|
||||
child: Icon(
|
||||
Icons.arrow_forward,
|
||||
size: 20,
|
||||
color: isActive ? Theme.of(context).colorScheme.primary : Colors.grey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStepTitle(int stepIndex, String title) {
|
||||
final isActive = _currentStep == stepIndex;
|
||||
final isCompleted = _currentStep > stepIndex;
|
||||
final textColor =
|
||||
isActive || isCompleted
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.grey;
|
||||
|
||||
return Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
|
||||
color: textColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 导航按钮
|
||||
Widget _buildNavigationButtons(FoodProvider provider) {
|
||||
final isLastStep = _currentStep == 2;
|
||||
final isFirstStep = _currentStep == 0;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
if (!isFirstStep)
|
||||
Expanded(
|
||||
child: buildInfoButton(
|
||||
context: context,
|
||||
text: '上一步',
|
||||
onPressed: _previousStep,
|
||||
),
|
||||
),
|
||||
if (!isFirstStep) const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: buildPrimaryButton(
|
||||
context: context,
|
||||
text: isLastStep ? '提交' : '下一步',
|
||||
onPressed: () => _nextStep(provider),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _nextStep(FoodProvider provider) {
|
||||
if (_currentStep < 2) {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
setState(() {
|
||||
_currentStep++;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_submitForm(provider);
|
||||
}
|
||||
}
|
||||
|
||||
void _previousStep() {
|
||||
if (_currentStep > 0) {
|
||||
setState(() {
|
||||
_currentStep--;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _submitForm(FoodProvider provider) async {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
LoadingDialog.show(context, message: '提交中');
|
||||
final result = await provider.handleRecipe();
|
||||
if (result == true) {
|
||||
LoadingDialog.hide(context);
|
||||
|
||||
if (provider.isEditing) {
|
||||
showSuccessTip(context, '更新菜谱成功');
|
||||
} else {
|
||||
showSuccessTip(context, '新增菜谱成功');
|
||||
}
|
||||
|
||||
Future.delayed(Duration(milliseconds: 1500), () {
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
LoadingDialog.hide(context);
|
||||
if (provider.isEditing) {
|
||||
showErrorTip(context, '更新菜谱失败');
|
||||
} else {
|
||||
showErrorTip(context, '新增菜谱失败');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ToastUtil.error('请检查表单填写是否正确');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<FoodProvider>();
|
||||
|
||||
return AnimatedPadding(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: MediaQuery.of(context).viewInsets,
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildStepIndicator(),
|
||||
FormBuilder(key: _formKey, child: _buildStepContent(provider)),
|
||||
_buildNavigationButtons(provider),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:food_hub_app/provider/food_provider.dart';
|
||||
import 'package:food_hub_app/views/moment_form.dart';
|
||||
import 'package:food_hub_app/views/recipe_form.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:tdesign_flutter/tdesign_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
enum RecordTab {
|
||||
recipe('菜谱'),
|
||||
calendar('日历'),
|
||||
timeline('时间轴');
|
||||
|
||||
final String label;
|
||||
|
||||
const RecordTab(this.label);
|
||||
}
|
||||
|
||||
class RecordPage extends StatefulWidget {
|
||||
const RecordPage({super.key});
|
||||
@@ -12,18 +26,11 @@ class RecordPage extends StatefulWidget {
|
||||
State<StatefulWidget> createState() => _RecordPageState();
|
||||
}
|
||||
|
||||
class _RecordPageState extends State<RecordPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
|
||||
late final TabController _tabController = TabController(
|
||||
length: 3,
|
||||
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)),
|
||||
class _RecordPageState extends State<RecordPage> {
|
||||
final List<Widget> _tabPages = [
|
||||
RecipeList(userId: 0),
|
||||
RecipeCalendar(),
|
||||
RecipeTimeline(),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -33,34 +40,176 @@ class _RecordPageState extends State<RecordPage>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.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
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
TDTabBar(
|
||||
tabs: tabs,
|
||||
controller: _tabController,
|
||||
backgroundColor: Colors.white,
|
||||
showIndicator: true,
|
||||
final provider = context.watch<FoodProvider>();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
title: Text('记录', style: Theme.of(context).textTheme.titleLarge),
|
||||
centerTitle: true,
|
||||
leading: Builder(
|
||||
builder:
|
||||
(context) => IconButton(
|
||||
icon: Icon(Icons.menu, color: Theme.of(context).primaryColor),
|
||||
onPressed: () => {},
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(5),
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
),
|
||||
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: [
|
||||
RecipeList(),
|
||||
RecipeCalendar(),
|
||||
RecipeTimeline(),
|
||||
_buildTabs(
|
||||
context: context,
|
||||
currentTab: provider.currentRecordTab,
|
||||
onTabChanged: (tab) => _onTabChange(tab, provider),
|
||||
),
|
||||
Expanded(child: _tabPages[provider.currentRecordTab.index]),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,99 +1,180 @@
|
||||
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:image_picker/image_picker.dart';
|
||||
import 'package:food_hub_app/widgets/common/image.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'dart:io';
|
||||
|
||||
class RecordFormPage extends StatefulWidget {
|
||||
const RecordFormPage({super.key});
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RecordForm extends StatefulWidget {
|
||||
const RecordForm({super.key});
|
||||
|
||||
@override
|
||||
State<RecordFormPage> createState() => _RecordFormPageState();
|
||||
State<RecordForm> createState() => _RecordFormState();
|
||||
}
|
||||
|
||||
class _RecordFormPageState extends State<RecordFormPage> {
|
||||
class _RecordFormState extends State<RecordForm> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
final _focusNode = FocusNode();
|
||||
static const String _nameField = 'name';
|
||||
static const String _dateField = 'date';
|
||||
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
File? imageUrl; // 改为单张图片变量
|
||||
List<String> _foodNameList = [];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
void initState() {
|
||||
super.initState();
|
||||
refreshCategoryList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
Future<void> refreshCategoryList() async {
|
||||
final result = await queryFoodNameListApi();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('新增记录', style: TextStyle(color: Colors.white)),
|
||||
backgroundColor: theme.primaryColor,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
color: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: _buildFormBuilder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_foodNameList = result;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildFormBuilder() {
|
||||
Widget _buildFormBuilder(FoodProvider provider) {
|
||||
return FormBuilder(
|
||||
key: _formKey,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buildFormLabel('菜谱名称', required: true),
|
||||
const SizedBox(height: 8),
|
||||
_buildNameTextField(),
|
||||
const SizedBox(height: 10),
|
||||
buildFormLabel(context: context, text: '菜谱名称', isRequired: true),
|
||||
const SizedBox(height: 12),
|
||||
_buildRecipeField(provider),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
buildFormLabel('完成时间', required: true),
|
||||
const SizedBox(height: 8),
|
||||
_buildDateTextField(),
|
||||
const SizedBox(height: 10),
|
||||
buildFormLabel(context: context, text: '完成时间', isRequired: true),
|
||||
const SizedBox(height: 12),
|
||||
_buildDateField(provider),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
buildFormLabel('上传图片', required: true),
|
||||
const SizedBox(height: 10),
|
||||
_buildImageUploadArea(),
|
||||
buildFormLabel(context: context, text: '上传图片', isRequired: true),
|
||||
const SizedBox(height: 12),
|
||||
_buildImageField(provider),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
buildFormButtonGroup(context: context, onConfirm: () => _submitForm),
|
||||
buildFormButtonGroup(
|
||||
context: context,
|
||||
isShowDelete: provider.isEditing,
|
||||
onConfirm: () => _submitForm(provider),
|
||||
onDelete: () => _deleteForm(provider),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 菜谱名称输入框
|
||||
Widget _buildNameTextField() {
|
||||
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,
|
||||
focusNode: _focusNode,
|
||||
decoration: buildInputDecoration(context: context, hintText: '请输入菜谱名称'),
|
||||
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 '请输入菜谱名称';
|
||||
@@ -101,23 +182,21 @@ class _RecordFormPageState extends State<RecordFormPage> {
|
||||
return null;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 完成时间选择框
|
||||
Widget _buildDateTextField() {
|
||||
Widget _buildDateField(FoodProvider provider) {
|
||||
return FormBuilderTextField(
|
||||
name: _dateField,
|
||||
readOnly: true,
|
||||
initialValue: provider.recordFormItem.date,
|
||||
decoration: buildInputDecoration(
|
||||
context: context,
|
||||
hintText: '请选择完成时间',
|
||||
prefixIcon: const Icon(
|
||||
Icons.calendar_month,
|
||||
size: 20,
|
||||
color: Color(0xFF86909C),
|
||||
prefixIcon: Icons.calendar_month,
|
||||
),
|
||||
),
|
||||
onTap: () => _onSelectDate(),
|
||||
onTap: () => _onSelectDate(provider),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '请选择完成时间';
|
||||
@@ -127,207 +206,141 @@ class _RecordFormPageState extends State<RecordFormPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 图片上传区域(预览+上传按钮)
|
||||
Widget _buildImageUploadArea() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (imageUrl != null)
|
||||
_buildImagePreviewItem()
|
||||
else
|
||||
_buildImageUploadButton(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImagePreviewItem() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => _showImagePreview(),
|
||||
child: Image(
|
||||
image: FileImage(imageUrl!),
|
||||
width: 120,
|
||||
height: 120,
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
color: Colors.grey[100],
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: 6,
|
||||
right: 6,
|
||||
child: GestureDetector(
|
||||
onTap: _removeImage,
|
||||
child: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 2,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(Icons.close, color: Colors.white, size: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showImagePreview() {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierColor: Colors.black87, // 半透明黑色背景
|
||||
builder:
|
||||
(context) => Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
insetPadding: const EdgeInsets.all(16),
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.pop(context), // 点击空白处关闭
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
// 预览图添加轻微阴影
|
||||
boxShadow: [BoxShadow(color: Colors.black38, blurRadius: 10)],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.file(
|
||||
imageUrl!,
|
||||
fit: BoxFit.contain, // 保持图片比例
|
||||
height: MediaQuery.of(context).size.height * 0.7, // 限制最大高度
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 图片上传按钮
|
||||
Widget _buildImageUploadButton() {
|
||||
return InkWell(
|
||||
onTap: _pickImage,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
width: 96,
|
||||
height: 96,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF2F3F5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFDCDFE6), width: 1),
|
||||
),
|
||||
child: const Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.add, color: Color(0xFF86909C), size: 24),
|
||||
SizedBox(height: 6),
|
||||
Text(
|
||||
'添加图片',
|
||||
style: TextStyle(color: Color(0xFF86909C), fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 选择日期
|
||||
Future<void> _onSelectDate() async {
|
||||
Future<void> _onSelectDate(FoodProvider provider) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2100),
|
||||
builder:
|
||||
(context, child) => Theme(
|
||||
data: ThemeData.light().copyWith(
|
||||
primaryColor: Theme.of(context).primaryColor,
|
||||
colorScheme: ColorScheme.light(
|
||||
primary: Theme.of(context).primaryColor,
|
||||
),
|
||||
buttonTheme: const ButtonThemeData(
|
||||
textTheme: ButtonTextTheme.primary,
|
||||
),
|
||||
),
|
||||
child: child!,
|
||||
),
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
_formKey.currentState?.fields[_dateField]?.didChange(
|
||||
DateFormat('yyyy-MM-dd').format(picked),
|
||||
);
|
||||
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() async {
|
||||
final XFile? image = await _picker.pickImage(source: ImageSource.gallery);
|
||||
if (image != null) {
|
||||
setState(() {
|
||||
imageUrl = File(image.path); // 直接覆盖现有图片
|
||||
});
|
||||
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() {
|
||||
setState(() {
|
||||
imageUrl = null;
|
||||
});
|
||||
void _removeImage(FoodProvider provider) {
|
||||
provider.updateRecordFormItem(imageUrl: '');
|
||||
}
|
||||
|
||||
/// 提交表单
|
||||
void _submitForm() {
|
||||
void _submitForm(FoodProvider provider) async {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
if (imageUrl == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('请上传图片'),
|
||||
backgroundColor: Colors.red,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
if (provider.recordFormItem.imageUrl.isEmpty) {
|
||||
ToastUtil.error('请上传图片');
|
||||
return;
|
||||
}
|
||||
|
||||
final formData = {
|
||||
..._formKey.currentState!.value,
|
||||
'imageUrl': imageUrl?.path, // 单张图片路径
|
||||
};
|
||||
LoadingDialog.show(context, message: '提交中');
|
||||
final result = await provider.handleRecord();
|
||||
if (result == true) {
|
||||
await provider.onRecordTabChange();
|
||||
LoadingDialog.hide(context);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('提交成功!'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
print('表单数据: $formData');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:food_hub_app/apis/stats.dart';
|
||||
import 'package:food_hub_app/models/stats.dart';
|
||||
import 'package:food_hub_app/widgets/common/chart.dart';
|
||||
import 'package:food_hub_app/widgets/stats/card.dart';
|
||||
import 'package:flutter_common/widget/chart.dart';
|
||||
import 'package:flutter_common/widget/common_widget.dart';
|
||||
import 'package:food_hub_app/provider/food_provider.dart';
|
||||
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class StatsPage extends StatefulWidget {
|
||||
const StatsPage({super.key});
|
||||
@@ -14,204 +15,146 @@ class StatsPage extends StatefulWidget {
|
||||
class _StatsPage extends State<StatsPage> {
|
||||
final double chartHeight = 400;
|
||||
|
||||
SummaryStats summaryStats = SummaryStats(
|
||||
recipeCount: 0,
|
||||
categoryCount: 0,
|
||||
workCount: 0,
|
||||
);
|
||||
|
||||
late double averageRecordCount = 0;
|
||||
|
||||
List<ChartData> recordStats = [];
|
||||
List<ChartData> categoryStats = [];
|
||||
List<ChartData> rankStats = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
refreshStats();
|
||||
}
|
||||
|
||||
Future<void> refreshStats() async {
|
||||
final result1 = await queryStatsApi();
|
||||
final result2 = await queryRecordStatsApi();
|
||||
final result3 = await queryCategoryStatsApi();
|
||||
final result4 = await queryRankStatsApi();
|
||||
|
||||
setState(() {
|
||||
summaryStats = result1;
|
||||
recordStats = result2;
|
||||
categoryStats = result3;
|
||||
rankStats = result4;
|
||||
|
||||
if (recordStats.isNotEmpty) {
|
||||
double sumValue = recordStats.fold(0.0, (sum, item) => sum + item.value);
|
||||
averageRecordCount = sumValue / recordStats.length;
|
||||
}
|
||||
// 初始化加载数据
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<FoodProvider>().refreshFoodStats();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildSummaryStats(),
|
||||
SizedBox(
|
||||
height: chartHeight,
|
||||
child: Card(
|
||||
color: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: _buildRecordStats(),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: chartHeight,
|
||||
child: Card(
|
||||
color: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: _buildCategoryStats(),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: chartHeight,
|
||||
child: Card(
|
||||
color: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: _buildRankStats(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Widget _buildSummaryStats(BuildContext context, FoodProvider provider) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
Widget _buildSummaryStats() {
|
||||
return GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
// 禁用网格自身滚动
|
||||
crossAxisCount: 2,
|
||||
// crossAxisSpacing: 5,
|
||||
// mainAxisSpacing: 5,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
childAspectRatio: 2,
|
||||
children: [
|
||||
StatisticCard(
|
||||
GlassCard(
|
||||
useOwnLayer: true,
|
||||
settings: LiquidGlassSettings(glassColor: colors.surface),
|
||||
padding: EdgeInsetsGeometry.all(10),
|
||||
child: StatsCard(
|
||||
icon: Icons.restaurant_menu,
|
||||
color: Colors.blue,
|
||||
title: '菜谱总数',
|
||||
value: summaryStats.recipeCount,
|
||||
value: provider.summaryStats.recipeCount.toString(),
|
||||
unit: '个',
|
||||
),
|
||||
StatisticCard(
|
||||
),
|
||||
GlassCard(
|
||||
useOwnLayer: true,
|
||||
settings: LiquidGlassSettings(glassColor: colors.surface),
|
||||
padding: EdgeInsetsGeometry.all(10),
|
||||
child: StatsCard(
|
||||
icon: Icons.grid_view_rounded,
|
||||
color: Colors.green,
|
||||
title: '菜谱类别',
|
||||
value: summaryStats.categoryCount,
|
||||
value: provider.summaryStats.categoryCount.toString(),
|
||||
unit: '种',
|
||||
),
|
||||
StatisticCard(
|
||||
),
|
||||
GlassCard(
|
||||
useOwnLayer: true,
|
||||
settings: LiquidGlassSettings(glassColor: colors.surface),
|
||||
padding: EdgeInsetsGeometry.all(10),
|
||||
child: StatsCard(
|
||||
icon: Icons.flag,
|
||||
color: Colors.orange,
|
||||
title: '做菜次数',
|
||||
value: summaryStats.workCount,
|
||||
value: provider.summaryStats.workCount.toString(),
|
||||
unit: '个',
|
||||
),
|
||||
StatisticCard(
|
||||
),
|
||||
GlassCard(
|
||||
useOwnLayer: true,
|
||||
settings: LiquidGlassSettings(glassColor: colors.surface),
|
||||
padding: EdgeInsetsGeometry.all(10),
|
||||
child: StatsCard(
|
||||
icon: Icons.show_chart,
|
||||
color: Colors.red,
|
||||
title: '平均次数',
|
||||
value: double.parse(averageRecordCount.toStringAsFixed(2)),
|
||||
value: provider.averageRecordCount.toStringAsFixed(2),
|
||||
unit: '次/月',
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecordStats() {
|
||||
Widget _buildContent(BuildContext context, FoodProvider provider) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildTitleSection('记录统计'),
|
||||
const SizedBox(height: 3),
|
||||
_buildDivider(),
|
||||
const SizedBox(height: 3),
|
||||
Expanded(
|
||||
child: lineChart(
|
||||
context: context,
|
||||
_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: recordStats,
|
||||
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: '次'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategoryStats() {
|
||||
return Column(
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<FoodProvider>();
|
||||
|
||||
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: [
|
||||
_buildTitleSection('菜谱统计'),
|
||||
const SizedBox(height: 3),
|
||||
_buildDivider(),
|
||||
const SizedBox(height: 3),
|
||||
Expanded(
|
||||
child: pieChart(context: context, unit: '个', data: categoryStats),
|
||||
if (provider.isLoading)
|
||||
buildLoadingIndicator()
|
||||
else
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 90),
|
||||
child: _buildContent(context, provider),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRankStats() {
|
||||
return Column(
|
||||
children: [
|
||||
_buildTitleSection('排行榜'),
|
||||
const SizedBox(height: 3),
|
||||
_buildDivider(),
|
||||
const SizedBox(height: 3),
|
||||
Expanded(child: rankChart(rankStats)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 构建标题区域
|
||||
Widget _buildTitleSection(String title) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(Icons.insert_chart, color: Theme.of(context).primaryColor),
|
||||
Text(title),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDivider() {
|
||||
return Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: Theme.of(context).primaryColor,
|
||||
indent: 0,
|
||||
endIndent: 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:food_hub_app/models/stats.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
|
||||
Widget lineChart({
|
||||
required BuildContext context,
|
||||
required String xAxisName,
|
||||
required String yAxisName,
|
||||
required String unit,
|
||||
required List<ChartData> data,
|
||||
}) {
|
||||
return SfCartesianChart(
|
||||
// 图表标题
|
||||
// title: ChartTitle(text: '2023年上半年销售额(万元)'),
|
||||
|
||||
// X轴配置(类别轴)
|
||||
primaryXAxis: CategoryAxis(majorGridLines: MajorGridLines(width: 0)),
|
||||
|
||||
// Y轴配置(数值轴)
|
||||
// primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName($unit)')),
|
||||
|
||||
// 启用图例
|
||||
// legend: const Legend(isVisible: true),
|
||||
|
||||
// 启用交互提示(点击数据点显示详情)
|
||||
tooltipBehavior: TooltipBehavior(
|
||||
enable: true,
|
||||
format: 'point.x: point.y $unit',
|
||||
),
|
||||
|
||||
// 折线图数据系列
|
||||
series: [
|
||||
LineSeries<ChartData, String>(
|
||||
dataSource: data,
|
||||
// X轴数据映射
|
||||
xValueMapper: (ChartData chart, _) => chart.name,
|
||||
// Y轴数据映射
|
||||
yValueMapper: (ChartData chart, _) => chart.value,
|
||||
// 线条颜色
|
||||
color: Theme.of(context).primaryColor,
|
||||
// 线条宽度
|
||||
width: 3,
|
||||
|
||||
// 数据点样式
|
||||
markerSettings: const MarkerSettings(
|
||||
isVisible: true,
|
||||
color: Colors.white,
|
||||
shape: DataMarkerType.circle,
|
||||
height: 6,
|
||||
width: 6,
|
||||
),
|
||||
|
||||
// 折线名称(会显示在图例中)
|
||||
name: yAxisName,
|
||||
|
||||
// 启用数据标签(直接显示数值)
|
||||
dataLabelSettings: const DataLabelSettings(
|
||||
isVisible: true,
|
||||
color: Colors.white,
|
||||
opacity: 0,
|
||||
),
|
||||
|
||||
// 动画效果
|
||||
animationDuration: 2000, // 动画时长(毫秒)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget barChart({
|
||||
required BuildContext context,
|
||||
required String xAxisName,
|
||||
required String yAxisName,
|
||||
required String unit,
|
||||
required List<ChartData> data,
|
||||
}) {
|
||||
return SfCartesianChart(
|
||||
// X轴配置(类别轴)
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: MajorGridLines(width: 0),
|
||||
title: AxisTitle(text: xAxisName),
|
||||
),
|
||||
|
||||
// Y轴配置(数值轴)
|
||||
primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName($unit)')),
|
||||
|
||||
// 启用交互提示
|
||||
tooltipBehavior: TooltipBehavior(
|
||||
enable: true,
|
||||
format: 'point.x: point.y $unit',
|
||||
),
|
||||
|
||||
// 柱状图数据系列
|
||||
series: [
|
||||
ColumnSeries<ChartData, String>(
|
||||
dataSource: data,
|
||||
// X轴数据映射
|
||||
xValueMapper: (ChartData chart, _) => chart.name,
|
||||
// Y轴数据映射
|
||||
yValueMapper: (ChartData chart, _) => chart.value,
|
||||
|
||||
// 名称
|
||||
name: yAxisName,
|
||||
|
||||
// 柱子颜色
|
||||
color: Theme.of(context).primaryColor,
|
||||
|
||||
// 柱子宽度(0-1之间,1表示占满类别间隔)
|
||||
width: 0.6,
|
||||
|
||||
// 柱子边框
|
||||
borderWidth: 1,
|
||||
borderColor: Colors.black12,
|
||||
|
||||
// 数据标签
|
||||
dataLabelSettings: const DataLabelSettings(
|
||||
isVisible: true,
|
||||
color: Colors.white,
|
||||
opacity: 0,
|
||||
alignment: ChartAlignment.center,
|
||||
),
|
||||
|
||||
// 动画效果
|
||||
animationDuration: 2000,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget pieChart({
|
||||
required BuildContext context,
|
||||
required String unit,
|
||||
required List<ChartData> data,
|
||||
}) {
|
||||
// 计算 value 的总和
|
||||
double sumValue = data.fold(0.0, (sum, item) => sum + item.value);
|
||||
|
||||
return SfCircularChart(
|
||||
// 饼图标题
|
||||
// title: ChartTitle(text: '菜谱类别占比分布'),
|
||||
|
||||
// 启用图例
|
||||
legend: const Legend(isVisible: true, position: LegendPosition.right),
|
||||
|
||||
// 启用交互提示(点击扇区显示详情)
|
||||
tooltipBehavior: TooltipBehavior(
|
||||
enable: true,
|
||||
format: 'point.x: point.y $unit',
|
||||
),
|
||||
|
||||
// 饼图系列配置
|
||||
series: [
|
||||
PieSeries<ChartData, String>(
|
||||
dataSource: data,
|
||||
// 类别映射(饼图扇区名称)
|
||||
xValueMapper: (ChartData data, _) => data.name,
|
||||
// 数值映射(扇区大小占比)
|
||||
yValueMapper: (ChartData data, _) => data.value,
|
||||
|
||||
// 扇区半径(0-1之间,1表示充满容器)
|
||||
// radius: '50%',
|
||||
|
||||
// 启用扇区分离效果
|
||||
explode: true,
|
||||
// 指定分离的扇区索引(这里分离第一个扇区)
|
||||
explodeIndex: 0,
|
||||
// 分离距离
|
||||
explodeOffset: '5%',
|
||||
|
||||
dataLabelMapper: (ChartData data, _) {
|
||||
final percentage = (data.value / sumValue * 100).toStringAsFixed(0);
|
||||
return '$percentage%';
|
||||
},
|
||||
|
||||
// 数据标签(显示在扇区上的文本)
|
||||
dataLabelSettings: DataLabelSettings(isVisible: true),
|
||||
|
||||
// 动画效果
|
||||
animationDuration: 2000,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
67
lib/widgets/common/easy_refresh.dart
Normal file
67
lib/widgets/common/easy_refresh.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:easy_refresh/easy_refresh.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget buildEasyRefresh({
|
||||
required EasyRefreshController freshController,
|
||||
required Future<void> Function()? onRefresh,
|
||||
required Future<void> Function()? onLoad,
|
||||
required Widget body,
|
||||
}) {
|
||||
return EasyRefresh(
|
||||
controller: freshController,
|
||||
header: ClassicHeader(
|
||||
dragText: '下拉刷新',
|
||||
armedText: '释放刷新',
|
||||
readyText: '准备刷新',
|
||||
processingText: '刷新中...',
|
||||
processedText: '刷新完成',
|
||||
failedText: '刷新失败',
|
||||
noMoreText: '没有更多数据',
|
||||
showText: true,
|
||||
messageText: '更新于 %T',
|
||||
showMessage: true,
|
||||
),
|
||||
footer: ClassicFooter(
|
||||
dragText: '上拉加载',
|
||||
armedText: '释放加载',
|
||||
readyText: '准备加载',
|
||||
processingText: '加载中...',
|
||||
processedText: '加载完成',
|
||||
failedText: '加载失败',
|
||||
noMoreText: '没有更多数据',
|
||||
showText: true,
|
||||
messageText: '更新于 %T',
|
||||
showMessage: true,
|
||||
),
|
||||
onRefresh: onRefresh,
|
||||
onLoad: onLoad,
|
||||
child: body,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildScrollToTop({
|
||||
required BuildContext context,
|
||||
required VoidCallback scrollToTop,
|
||||
}) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return Positioned(
|
||||
right: 0,
|
||||
bottom: 80,
|
||||
child: FloatingActionButton(
|
||||
onPressed: scrollToTop,
|
||||
backgroundColor: colors.primary,
|
||||
elevation: 2,
|
||||
mini: true,
|
||||
child: Icon(Icons.arrow_upward, color: colors.surface),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void scrollToTopAnimateTo(ScrollController controller) {
|
||||
controller.animateTo(
|
||||
0,
|
||||
duration: const Duration(milliseconds: 500), // 滚动动画时长
|
||||
curve: Curves.easeInOut, // 滚动动画曲线
|
||||
);
|
||||
}
|
||||
@@ -1,53 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'index.dart';
|
||||
|
||||
/// 通用输入框样式
|
||||
InputDecoration buildInputDecoration({
|
||||
required BuildContext context,
|
||||
required String hintText,
|
||||
Widget? prefixIcon,
|
||||
IconData? prefixIcon,
|
||||
}) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return InputDecoration(
|
||||
hintText: hintText,
|
||||
hintStyle: const TextStyle(
|
||||
color: Color(0xFF999999),
|
||||
fontSize: 15,
|
||||
height: 1.2,
|
||||
),
|
||||
hintStyle: Theme.of(context).textTheme.bodyMedium,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: const BorderSide(color: Color(0xFFE5E7EB)),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
fillColor: colors.surface,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 1.5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: colors.primary, width: 2),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(color: Colors.red, width: 1.5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
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,
|
||||
prefixIcon:
|
||||
prefixIcon != null
|
||||
? Icon(prefixIcon, size: 20, color: colors.primary)
|
||||
: null,
|
||||
prefixIconConstraints: const BoxConstraints(minWidth: 40),
|
||||
isDense: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildFormLabel(String text, {bool required = false}) {
|
||||
Widget buildFormLabel({
|
||||
required BuildContext context,
|
||||
required String text,
|
||||
required bool isRequired,
|
||||
}) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
text: text,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF1D2129),
|
||||
style: TextStyle(
|
||||
color: colors.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'CustomFont',
|
||||
height: 1.2,
|
||||
),
|
||||
children: [
|
||||
if (required)
|
||||
if (isRequired)
|
||||
const TextSpan(
|
||||
text: ' *',
|
||||
style: TextStyle(color: Colors.red, fontSize: 16),
|
||||
@@ -60,12 +69,19 @@ Widget buildFormLabel(String text, {bool required = false}) {
|
||||
/// 表单底部按钮组组件
|
||||
Widget buildFormButtonGroup({
|
||||
required BuildContext context,
|
||||
required bool isShowDelete,
|
||||
required VoidCallback onConfirm,
|
||||
required VoidCallback onDelete,
|
||||
}) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(child: _buildCancelButton(context)),
|
||||
const SizedBox(width: 16),
|
||||
if (isShowDelete) ...[
|
||||
SizedBox(width: 16),
|
||||
Expanded(child: _buildDeleteButton(context, onDelete)),
|
||||
],
|
||||
SizedBox(width: 16),
|
||||
Expanded(child: _buildSubmitButton(context, onConfirm)),
|
||||
],
|
||||
);
|
||||
@@ -73,30 +89,19 @@ Widget buildFormButtonGroup({
|
||||
|
||||
/// 取消按钮
|
||||
Widget _buildCancelButton(BuildContext context) {
|
||||
return ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context), // 直接使用传入的context返回
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: const Color(0xFF4E5969),
|
||||
side: const BorderSide(color: Color(0xFFDCDFE6)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text('取消'),
|
||||
return buildInfoButton(
|
||||
context: context,
|
||||
text: '取消',
|
||||
onPressed: () => Navigator.pop(context),
|
||||
);
|
||||
}
|
||||
|
||||
/// 提交按钮
|
||||
Widget _buildSubmitButton(BuildContext context, VoidCallback onConfirm) {
|
||||
return ElevatedButton(
|
||||
onPressed: onConfirm,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).primaryColor, // 使用传入的context获取主题
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text('提交', style: TextStyle(color: Colors.white)),
|
||||
);
|
||||
return buildPrimaryButton(context: context, text: '提交', onPressed: onConfirm);
|
||||
}
|
||||
|
||||
/// 删除按钮
|
||||
Widget _buildDeleteButton(BuildContext context, VoidCallback onDelete) {
|
||||
return buildErrorButton(text: '删除', onPressed: onDelete);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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';
|
||||
|
||||
@@ -19,6 +21,7 @@ class ImagePreviewPage extends StatefulWidget {
|
||||
class _ImagePreviewPageState extends State<ImagePreviewPage> {
|
||||
// 声明 PageController 并初始化初始索引
|
||||
late PageController _pageController;
|
||||
|
||||
// 记录当前显示的图片索引(用于更新页码)
|
||||
int _currentIndex = 0;
|
||||
|
||||
@@ -51,6 +54,8 @@ class _ImagePreviewPageState extends State<ImagePreviewPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final index = '${_currentIndex + 1}/${widget.images.length}';
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
@@ -61,16 +66,14 @@ class _ImagePreviewPageState extends State<ImagePreviewPage> {
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
// 显示实时更新的页码(当前索引+1 / 总数量)
|
||||
title: Text(
|
||||
'${_currentIndex + 1}/${widget.images.length}',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
title: Text(index, style: const TextStyle(color: Colors.white)),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: PhotoViewGallery(
|
||||
pageOptions: widget.images.map((url) {
|
||||
pageOptions:
|
||||
widget.images.map((url) {
|
||||
return PhotoViewGalleryPageOptions(
|
||||
imageProvider: NetworkImage(url),
|
||||
imageProvider: NetworkImage('${AppConfig.imageBaseUrl}$url'),
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
maxScale: PhotoViewComputedScale.covered * 2,
|
||||
// 点击空白处关闭预览
|
||||
@@ -86,30 +89,132 @@ class _ImagePreviewPageState extends State<ImagePreviewPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget networkImage(String url) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
url,
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
// 加载中显示占位符
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
class CommonImage extends StatelessWidget {
|
||||
final List<String> imageUrls;
|
||||
final int index;
|
||||
|
||||
const CommonImage({super.key, required this.imageUrls, required this.index});
|
||||
|
||||
Widget _buildErrorImage() {
|
||||
return Container(
|
||||
color: Colors.grey[200],
|
||||
child: const Icon(Icons.image, color: Colors.grey, size: 30),
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToImagePreview(
|
||||
BuildContext context,
|
||||
List<String> imageUrls,
|
||||
int index,
|
||||
) {
|
||||
final route = MaterialPageRoute(
|
||||
builder:
|
||||
(context) => ImagePreviewPage(images: imageUrls, initialIndex: index),
|
||||
);
|
||||
|
||||
Navigator.push(context, route);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AspectRatio(
|
||||
aspectRatio: 4 / 3,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_navigateToImagePreview(context, imageUrls, index);
|
||||
},
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: '${AppConfig.imageBaseUrl}${imageUrls[index]}',
|
||||
fit: BoxFit.cover,
|
||||
progressIndicatorBuilder:
|
||||
(context, url, downloadProgress) => CircularProgressIndicator(
|
||||
strokeWidth: 1.5,
|
||||
padding: EdgeInsetsGeometry.all(20),
|
||||
value: downloadProgress.progress,
|
||||
),
|
||||
errorWidget: (context, error, stackTrace) => _buildErrorImage(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ImagePreview extends StatelessWidget {
|
||||
final List<String> imageUrls;
|
||||
final int index;
|
||||
final VoidCallback onRemoveImage;
|
||||
|
||||
const ImagePreview({
|
||||
super.key,
|
||||
required this.imageUrls,
|
||||
required this.index,
|
||||
required this.onRemoveImage,
|
||||
});
|
||||
|
||||
Widget _buildDeleteImage() {
|
||||
return Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: GestureDetector(
|
||||
onTap: onRemoveImage,
|
||||
child: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.close, color: Colors.white, size: 16),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Stack(
|
||||
children: [
|
||||
CommonImage(imageUrls: imageUrls, index: index),
|
||||
_buildDeleteImage(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildImageUploadButton({
|
||||
required BuildContext context,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 180,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: colors.surface),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.camera_alt, color: colors.onSurface.withAlpha(100)),
|
||||
SizedBox(height: 6),
|
||||
Text('点击上传美食图片', style: Theme.of(context).textTheme.bodyMedium),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:toggle_switch/toggle_switch.dart';
|
||||
|
||||
Widget formLabelText({required String labelText, bool isRequired = false}) {
|
||||
return Text.rich(
|
||||
@@ -14,7 +14,7 @@ Widget formLabelText({required String labelText, bool isRequired = false}) {
|
||||
}
|
||||
|
||||
InputDecoration formInputDecoration({
|
||||
required String hintText, // 使用命名参数并标记为必填
|
||||
required String hintText,
|
||||
IconData? prefixIcon,
|
||||
}) {
|
||||
return InputDecoration(
|
||||
@@ -27,15 +27,32 @@ InputDecoration formInputDecoration({
|
||||
);
|
||||
}
|
||||
|
||||
Widget cardContainer(Widget content) {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: content,
|
||||
),
|
||||
Widget buildToggleSwitch<T extends Enum>({
|
||||
required BuildContext context,
|
||||
required T currentTab,
|
||||
required List<T> tabValues,
|
||||
required List<String> labels,
|
||||
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]);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,43 +61,82 @@ Widget circleIconButton({
|
||||
required IconData icon,
|
||||
required VoidCallback onPressed,
|
||||
required BuildContext context,
|
||||
bool? isSmall,
|
||||
}) {
|
||||
final double size = isSmall == true ? 24 : 36;
|
||||
|
||||
return ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
fixedSize: Size(32, 32),
|
||||
fixedSize: Size(size, size),
|
||||
shape: CircleBorder(),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
padding: EdgeInsets.zero,
|
||||
minimumSize: const Size(0, 0),
|
||||
),
|
||||
child: Icon(icon, color: Colors.white, size: 18),
|
||||
child: Icon(icon, color: Colors.white),
|
||||
);
|
||||
}
|
||||
|
||||
/// 确认按钮样式
|
||||
ButtonStyle primaryButtonStyle() {
|
||||
return ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(Colors.green),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
Widget buildTag(BuildContext context, String title) {
|
||||
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)),
|
||||
);
|
||||
}
|
||||
|
||||
/// 取消按钮样式
|
||||
ButtonStyle cancelButtonStyle() {
|
||||
return ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(Colors.grey),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
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)),
|
||||
);
|
||||
}
|
||||
|
||||
Text buttonText({required String text}) {
|
||||
return 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),
|
||||
);
|
||||
}
|
||||
|
||||
/// 图片错误显示
|
||||
@@ -92,29 +148,3 @@ Widget errorImageContainer(double height) {
|
||||
child: const Icon(Icons.image, color: Colors.grey, size: 30),
|
||||
);
|
||||
}
|
||||
|
||||
/// 成功消息
|
||||
void showSuccessToast(String message) {
|
||||
Fluttertoast.showToast(
|
||||
msg: message,
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
gravity: ToastGravity.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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:food_hub_app/widgets/common/index.dart';
|
||||
|
||||
class YearSelector extends StatefulWidget {
|
||||
final int initialYear;
|
||||
final int? minYear;
|
||||
final int? maxYear;
|
||||
final Function(int) onYearChanged;
|
||||
|
||||
const YearSelector({
|
||||
super.key,
|
||||
required this.initialYear,
|
||||
required this.onYearChanged,
|
||||
this.minYear,
|
||||
this.maxYear,
|
||||
});
|
||||
|
||||
@override
|
||||
State<YearSelector> createState() => _YearSelectorState();
|
||||
}
|
||||
|
||||
// SingleTickerProviderStateMixin 动画控制器
|
||||
class _YearSelectorState extends State<YearSelector>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late int _currentYear;
|
||||
|
||||
// 用于动画效果
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentYear = widget.initialYear;
|
||||
|
||||
// 初始化动画控制器
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
|
||||
// 缩放动画
|
||||
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.1).animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 切换到上一年
|
||||
void _previousYear() {
|
||||
if (widget.minYear == null || _currentYear > widget.minYear!) {
|
||||
_animateYearChange(() {
|
||||
setState(() {
|
||||
_currentYear--;
|
||||
});
|
||||
widget.onYearChanged(_currentYear);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 切换到下一年
|
||||
void _nextYear() {
|
||||
if (widget.maxYear == null || _currentYear < widget.maxYear!) {
|
||||
_animateYearChange(() {
|
||||
setState(() {
|
||||
_currentYear++;
|
||||
});
|
||||
widget.onYearChanged(_currentYear);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 年份变化时的动画效果
|
||||
void _animateYearChange(VoidCallback onComplete) {
|
||||
_animationController.forward().then((_) {
|
||||
onComplete();
|
||||
_animationController.reverse();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
circleIconButton(
|
||||
context: context,
|
||||
icon: Icons.chevron_left,
|
||||
onPressed: () => _previousYear(),
|
||||
),
|
||||
// 年份显示
|
||||
AnimatedBuilder(
|
||||
animation: _scaleAnimation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(scale: _scaleAnimation.value, child: child);
|
||||
},
|
||||
child: Text(
|
||||
'$_currentYear',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
circleIconButton(
|
||||
context: context,
|
||||
icon: Icons.chevron_right,
|
||||
onPressed: () => _nextYear(),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,90 +1,111 @@
|
||||
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:tdesign_flutter/tdesign_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class MomentCard extends StatelessWidget {
|
||||
class MomentCard extends StatefulWidget {
|
||||
final Moment moment;
|
||||
final bool isUser;
|
||||
|
||||
const MomentCard({super.key, required this.moment});
|
||||
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 Card(
|
||||
elevation: 0,
|
||||
color: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
return CommonCard(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildAvatar(context),
|
||||
InkWell(
|
||||
onTap: () => _onTapUser(context),
|
||||
child: _buildAvatar(context),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildNickname(),
|
||||
_buildNickname(context),
|
||||
const SizedBox(height: 5),
|
||||
_buildContent(),
|
||||
if (moment.imageList.isNotEmpty) ...[
|
||||
_buildContent(context),
|
||||
if (widget.moment.imageList.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
_buildPostImages(context),
|
||||
_buildPostImages(),
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
_buildTime(),
|
||||
_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 (moment.avatar!.isEmpty) {
|
||||
return TDAvatar(
|
||||
size: TDAvatarSize.medium,
|
||||
type: TDAvatarType.customText,
|
||||
shape: TDAvatarShape.circle,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
text: moment.username?[0],
|
||||
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 TDAvatar(
|
||||
size: TDAvatarSize.medium,
|
||||
type: TDAvatarType.normal,
|
||||
fit: BoxFit.contain,
|
||||
avatarUrl: '${AppConfig.baseApiUrl}/${moment.avatar}',
|
||||
return CircleAvatar(
|
||||
radius: 18,
|
||||
backgroundImage: NetworkImage('${AppConfig.imageBaseUrl}/${widget.moment.avatar}')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建昵称组件
|
||||
Widget _buildNickname() {
|
||||
Widget _buildNickname(BuildContext context) {
|
||||
return Text(
|
||||
moment.username ?? "",
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
widget.moment.username ?? "",
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
);
|
||||
}
|
||||
|
||||
// 构建内容组件
|
||||
Widget _buildContent() {
|
||||
Widget _buildContent(BuildContext context) {
|
||||
return Text(
|
||||
moment.content,
|
||||
style: const TextStyle(fontSize: 15, height: 1.3),
|
||||
widget.moment.content,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
);
|
||||
}
|
||||
|
||||
// 构建朋友圈图片列表(网格布局)
|
||||
Widget _buildPostImages(BuildContext context) {
|
||||
final imageCount = moment.imageList.length;
|
||||
Widget _buildPostImages() {
|
||||
final imageCount = widget.moment.imageList.length;
|
||||
|
||||
// 计算网格列数
|
||||
int crossAxisCount;
|
||||
@@ -104,20 +125,7 @@ class MomentCard extends StatelessWidget {
|
||||
|
||||
// 生成图片URL列表(用于预览时切换)
|
||||
final List<String> imageUrls =
|
||||
moment.imageList
|
||||
.map((path) => '${AppConfig.baseApiUrl}/$path')
|
||||
.toList();
|
||||
|
||||
void imageTapClick(int index) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder:
|
||||
(context) =>
|
||||
ImagePreviewPage(images: imageUrls, initialIndex: index),
|
||||
),
|
||||
);
|
||||
}
|
||||
widget.moment.imageList.map((path) => path).toList();
|
||||
|
||||
return GridView.count(
|
||||
shrinkWrap: true,
|
||||
@@ -127,29 +135,41 @@ class MomentCard extends StatelessWidget {
|
||||
mainAxisSpacing: 4,
|
||||
childAspectRatio: itemAspectRatio,
|
||||
children: List.generate(imageCount, (index) {
|
||||
// 单个图片项:添加点击事件
|
||||
return GestureDetector(
|
||||
// 点击图片时,跳转到预览页面
|
||||
onTap: () => imageTapClick(index),
|
||||
// 原图片组件
|
||||
child: networkImage(imageUrls[index]),
|
||||
);
|
||||
return CommonImage(imageUrls: imageUrls, index: index);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// 构建时间组件
|
||||
Widget _buildTime() {
|
||||
Widget _buildTime(BuildContext context) {
|
||||
return Text(
|
||||
moment.date ?? "",
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
|
||||
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,
|
||||
@@ -157,14 +177,14 @@ class MomentCard extends StatelessWidget {
|
||||
alignment: Alignment.bottomLeft,
|
||||
children: [
|
||||
Icon(Icons.thumb_up_alt_outlined),
|
||||
Positioned(
|
||||
left: 20,
|
||||
bottom: 15,
|
||||
child: TDBadge(
|
||||
TDBadgeType.message,
|
||||
count: (moment.likeList?.length ?? 0).toString(),
|
||||
),
|
||||
),
|
||||
// Positioned(
|
||||
// left: 20,
|
||||
// bottom: 15,
|
||||
// child: TDBadge(
|
||||
// TDBadgeType.message,
|
||||
// count: (widget.moment.likeList?.length ?? 0).toString(),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -175,42 +195,69 @@ class MomentCard extends StatelessWidget {
|
||||
alignment: Alignment.bottomLeft,
|
||||
children: [
|
||||
Icon(Icons.comment_outlined),
|
||||
Positioned(
|
||||
left: 20,
|
||||
bottom: 15,
|
||||
child: TDBadge(
|
||||
TDBadgeType.message,
|
||||
count: (moment.commentList?.length ?? 0).toString(),
|
||||
),
|
||||
),
|
||||
// Positioned(
|
||||
// left: 20,
|
||||
// bottom: 15,
|
||||
// child: TDBadge(
|
||||
// TDBadgeType.message,
|
||||
// count: (widget.moment.commentList?.length ?? 0).toString(),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
// _buildActionButton(
|
||||
// icon: Icons.thumb_up_alt_outlined,
|
||||
// count: moment.likeList?.length ?? 0,
|
||||
// ),
|
||||
// const SizedBox(width: 20),
|
||||
// _buildActionButton(
|
||||
// icon: Icons.comment_outlined,
|
||||
// count: moment.commentList?.length ?? 0,
|
||||
// ),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 构建带数字标记的动作按钮
|
||||
Widget _buildActionButton({required IconData icon, required int count}) {
|
||||
return Row(
|
||||
Widget _buildCommentItem(Comment comment) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, color: Colors.grey[500], size: 18),
|
||||
const SizedBox(width: 4),
|
||||
if (count > 0)
|
||||
Text(
|
||||
count.toString(),
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: comment.username,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
fontFamily: 'CustomFont',
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: ': ', style: TextStyle(color: Colors.black)),
|
||||
TextSpan(
|
||||
text: comment.content,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'CustomFont',
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCommentList() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: widget.moment.commentList!.length,
|
||||
separatorBuilder: (context, index) => SizedBox(height: 8),
|
||||
itemBuilder: (context, index) {
|
||||
return _buildCommentItem(widget.moment.commentList![index]);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
21
lib/widgets/moment/list.dart
Normal file
21
lib/widgets/moment/list.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:food_hub_app/models/moment.dart';
|
||||
import 'package:food_hub_app/widgets/moment/card.dart';
|
||||
|
||||
class MomentList extends StatelessWidget {
|
||||
final List<Moment> momentList;
|
||||
final bool isUser;
|
||||
|
||||
const MomentList({super.key, required this.momentList, required this.isUser});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.separated(
|
||||
itemCount: momentList.length,
|
||||
separatorBuilder: (context, index) => SizedBox(height: 8),
|
||||
itemBuilder: (context, index) {
|
||||
return MomentCard(moment: momentList[index], isUser: isUser);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
117
lib/widgets/profile/basic_info.dart
Normal file
117
lib/widgets/profile/basic_info.dart
Normal file
@@ -0,0 +1,117 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/utils/date_utils.dart';
|
||||
import 'package:food_hub_app/config/app_config.dart';
|
||||
import 'package:food_hub_app/models/session.dart';
|
||||
import 'package:food_hub_app/widgets/common/index.dart';
|
||||
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
|
||||
import 'package:liquid_glass_widgets/widgets/containers/glass_card.dart';
|
||||
|
||||
class ProfileInfo extends StatelessWidget {
|
||||
final User user;
|
||||
|
||||
const ProfileInfo({super.key, required this.user});
|
||||
|
||||
Widget _buildAvatar(BuildContext context, User user) {
|
||||
if (user.avatar == null) {
|
||||
return CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.surface,
|
||||
child: Text(
|
||||
user.username.isNotEmpty ? user.username[0] : '?',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundImage: NetworkImage(
|
||||
'${AppConfig.imageBaseUrl}/${user.avatar}',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildInfoItem({
|
||||
required BuildContext context,
|
||||
required IconData icon,
|
||||
required String text,
|
||||
}) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 16, color: colors.primary),
|
||||
const SizedBox(width: 6),
|
||||
Text(text, style: Theme.of(context).textTheme.bodySmall),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return GlassCard(
|
||||
padding: EdgeInsetsGeometry.all(10),
|
||||
useOwnLayer: true,
|
||||
settings: LiquidGlassSettings(glassColor: colors.surface),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildAvatar(context, user),
|
||||
const SizedBox(height: 8),
|
||||
Text(user.username, style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
if (user.tags != null && user.tags!.isNotEmpty)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children:
|
||||
user.tags!.map((tag) => buildTag(context, tag)).toList(),
|
||||
),
|
||||
if (user.tags != null && user.tags!.isNotEmpty)
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
if (user.area != null && user.area!.length >= 2)
|
||||
_buildInfoItem(
|
||||
context: context,
|
||||
icon: Icons.location_on,
|
||||
text: '${user.area![0]}-${user.area![1]}',
|
||||
),
|
||||
if (user.birthDate != null)
|
||||
_buildInfoItem(
|
||||
context: context,
|
||||
icon: Icons.cake,
|
||||
text: formatDate(user.birthDate!),
|
||||
),
|
||||
if (user.job != null && user.job!.isNotEmpty)
|
||||
_buildInfoItem(
|
||||
context: context,
|
||||
icon: Icons.work,
|
||||
text: user.job!,
|
||||
),
|
||||
if (user.phoneNumber != null && user.phoneNumber!.isNotEmpty)
|
||||
_buildInfoItem(
|
||||
context: context,
|
||||
icon: Icons.phone_android,
|
||||
text: user.phoneNumber!,
|
||||
),
|
||||
if (user.email != null && user.email!.isNotEmpty)
|
||||
_buildInfoItem(
|
||||
context: context,
|
||||
icon: Icons.email,
|
||||
text: user.email!,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (user.description != null && user.description!.isNotEmpty)
|
||||
const SizedBox(height: 8),
|
||||
if (user.description != null && user.description!.isNotEmpty)
|
||||
Text(user.description!),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:food_hub_app/apis/recipe.dart';
|
||||
import 'package:flutter_common/widget/common_widget.dart';
|
||||
import 'package:food_hub_app/models/recipe.dart';
|
||||
import 'package:food_hub_app/utils/date_util.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:tdesign_flutter/tdesign_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RecipeCalendar extends StatefulWidget {
|
||||
const RecipeCalendar({super.key});
|
||||
@@ -15,141 +16,129 @@ class RecipeCalendar extends StatefulWidget {
|
||||
}
|
||||
|
||||
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() {
|
||||
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: _focusedDay,
|
||||
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
|
||||
focusedDay: provider.focusedDay,
|
||||
selectedDayPredicate: (day) => isSameDay(provider.selectedDay, day),
|
||||
onDaySelected: (selectedDay, focusedDay) {
|
||||
if (!isSameDay(_selectedDay, selectedDay)) {
|
||||
if (!isSameDay(provider.selectedDay, selectedDay)) {
|
||||
setState(() {
|
||||
_selectedDay = selectedDay;
|
||||
_focusedDay = focusedDay;
|
||||
refreshSelectRecord();
|
||||
provider.selectedDay = selectedDay;
|
||||
provider.focusedDay = focusedDay;
|
||||
provider.refreshSelectRecord();
|
||||
});
|
||||
}
|
||||
},
|
||||
onPageChanged: (focusedDay) {
|
||||
_focusedDay = focusedDay;
|
||||
refreshRecord();
|
||||
provider.focusedDay = focusedDay;
|
||||
provider.refreshRecordList();
|
||||
},
|
||||
// 自定义日期单元格构建器
|
||||
calendarBuilders: CalendarBuilders(
|
||||
defaultBuilder:
|
||||
(context, day, focusedDay) => _buildDateWidget(day, false, false),
|
||||
(context, day, focusedDay) =>
|
||||
_buildDateItem(day, false, false, provider),
|
||||
selectedBuilder:
|
||||
(context, day, focusedDay) => _buildDateWidget(day, true, false),
|
||||
(context, day, focusedDay) =>
|
||||
_buildDateItem(day, true, false, provider),
|
||||
todayBuilder:
|
||||
(context, day, focusedDay) => _buildDateWidget(day, false, true),
|
||||
(context, day, focusedDay) =>
|
||||
_buildDateItem(day, false, true, provider),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget dailyItem() {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: Colors.white,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(5),
|
||||
child: Column(
|
||||
Widget _dailyItem(BuildContext context, FoodProvider provider) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(formatDateTime(_selectedDay, 'MM月dd日 EEEE')),
|
||||
if (selectRecordList.isEmpty)
|
||||
TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据')
|
||||
Text(formatDateTime(provider.selectedDay, 'MM月dd日 EEEE')),
|
||||
SizedBox(height: 4),
|
||||
if (provider.selectRecordList.isEmpty)
|
||||
SizedBox(height: 100, child: buildEmptyData())
|
||||
else
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemCount: selectRecordList.length,
|
||||
itemBuilder: (context, index) {
|
||||
return recipeRecordItem(selectRecordList[index]);
|
||||
},
|
||||
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 recipeRecordItem(Record record) {
|
||||
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: Color(0xFFF5F5DC),
|
||||
color: colors.primary.withAlpha(50),
|
||||
margin: EdgeInsets.zero,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Row(
|
||||
children: [
|
||||
TDAvatar(
|
||||
size: TDAvatarSize.medium,
|
||||
type: TDAvatarType.customText,
|
||||
text: record.category[0],
|
||||
),
|
||||
_buildRecordItemAvatar(record.category[0], colors.primary),
|
||||
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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(child: _buildRecordItemBody(record.name, record.category)),
|
||||
circleIconButton(
|
||||
context: context,
|
||||
icon: Icons.chevron_right,
|
||||
onPressed: () => {}
|
||||
onPressed: () => _handleEditRecord(context, record),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -157,38 +146,47 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateWidget(DateTime day, bool isSelected, bool isToday) {
|
||||
Widget _buildDateItem(
|
||||
DateTime day,
|
||||
bool isSelected,
|
||||
bool isToday,
|
||||
FoodProvider provider,
|
||||
) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
// 检查当前日期是否在需要显示红点的列表中
|
||||
bool shouldShowRedDot = recordList.any(
|
||||
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,
|
||||
margin: EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isSelected
|
||||
? Theme.of(context).primaryColor
|
||||
: isToday
|
||||
? Colors.grey[300]
|
||||
: Colors.transparent,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
decoration: BoxDecoration(color: boxColor, shape: BoxShape.circle),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// 日期数字
|
||||
Text(
|
||||
day.day.toString(),
|
||||
style: TextStyle(color: isSelected ? Colors.white : Colors.black),
|
||||
),
|
||||
// 底部红点 - 只在指定日期显示
|
||||
// 日期数字始终居中
|
||||
Text(day.day.toString(), style: TextStyle(color: textColor)),
|
||||
|
||||
// 红点
|
||||
if (shouldShowRedDot)
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 2),
|
||||
Align(
|
||||
alignment: Alignment(0, 0.8),
|
||||
child: Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
@@ -196,6 +194,20 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
|
||||
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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -203,11 +215,12 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Card(elevation: 0, color: Colors.white, child: recipeCalendar()),
|
||||
Expanded(child: dailyItem()),
|
||||
],
|
||||
);
|
||||
final provider = context.watch<FoodProvider>();
|
||||
|
||||
if (provider.isLoading) {
|
||||
return buildLoadingIndicator();
|
||||
}
|
||||
|
||||
return _buildContent(provider);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,77 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_carousel_widget/flutter_carousel_widget.dart';
|
||||
import 'package:food_hub_app/config/app_config.dart';
|
||||
|
||||
import 'package:flutter_common/widget/common_widget.dart';
|
||||
import 'package:food_hub_app/models/recipe.dart';
|
||||
import 'package:food_hub_app/widgets/common/image.dart';
|
||||
import 'package:food_hub_app/widgets/common/index.dart';
|
||||
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
|
||||
.map((item) => '${AppConfig.baseApiUrl}/${item.imageUrl}')
|
||||
.toList();
|
||||
recipe.recordList.reversed.map((record) => record.imageUrl).toList();
|
||||
|
||||
Widget buildCarouselItem(String url) {
|
||||
return Builder(
|
||||
builder: (BuildContext context) {
|
||||
return AspectRatio(
|
||||
aspectRatio: 2,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: networkImage(url),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
Widget buildRecipeCarousel() {
|
||||
return FlutterCarousel(
|
||||
// 轮播项
|
||||
items:
|
||||
imageUrls.map((url) {
|
||||
return buildCarouselItem(url);
|
||||
}).toList(),
|
||||
// 轮播配置
|
||||
options: FlutterCarouselOptions(
|
||||
height: 300,
|
||||
autoPlay: imageUrls.length > 1,
|
||||
enableInfiniteScroll: imageUrls.length > 1,
|
||||
autoPlayInterval: const Duration(seconds: 3),
|
||||
viewportFraction: 0.9,
|
||||
showIndicator: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(color: Theme.of(context).primaryColor, width: 1.0),
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
elevation: 0,
|
||||
color: Colors.white,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
return GlassCard(
|
||||
padding: EdgeInsetsGeometry.all(10),
|
||||
useOwnLayer: true,
|
||||
settings: LiquidGlassSettings(glassColor: colors.surface),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
buildRecipeCarousel(),
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: Theme.of(context).primaryColor,
|
||||
indent: 0,
|
||||
endIndent: 0,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
CommonImage(imageUrls: imageUrls, index: 0),
|
||||
InkWell(
|
||||
onTap: () => navigatorToRecipeDetail(context),
|
||||
child: _buildRecipeContent(context),
|
||||
),
|
||||
],
|
||||
@@ -80,81 +39,43 @@ class RecipeCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
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: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
Text(
|
||||
recipe.name,
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
maxLines: 1, // 限制单行,避免挤压按钮
|
||||
style: textTheme.titleMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
circleIconButton(
|
||||
context: context,
|
||||
icon: Icons.edit,
|
||||
onPressed: () => {},
|
||||
),
|
||||
circleIconButton(
|
||||
context: context,
|
||||
icon: Icons.book,
|
||||
onPressed:
|
||||
() => Navigator.pushNamed(
|
||||
context,
|
||||
'/recipeDetail',
|
||||
arguments: {'id': recipe.id},
|
||||
),
|
||||
_buildIconText(
|
||||
icon: Icons.star,
|
||||
text: recipe.recommendRate.toString(),
|
||||
color: colors.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
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,
|
||||
color: colors.primary,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
icon: Icons.note_add,
|
||||
text: '${recipe.recordList.length.toString()}次',
|
||||
color: colors.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -166,10 +87,10 @@ class RecipeCard extends StatelessWidget {
|
||||
Widget _buildIconText({
|
||||
required IconData icon,
|
||||
required String text,
|
||||
Color color = Colors.grey, // 默认灰色
|
||||
Color color = Colors.grey,
|
||||
double iconSize = 16,
|
||||
double textSize = 16,
|
||||
double spacing = 2, // 图标与文本间距
|
||||
double spacing = 2,
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
|
||||
@@ -1,44 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:food_hub_app/apis/recipe.dart';
|
||||
import 'package:food_hub_app/models/recipe.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:tdesign_flutter/tdesign_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RecipeList extends StatefulWidget {
|
||||
const RecipeList({super.key});
|
||||
final int userId;
|
||||
|
||||
const RecipeList({super.key, required this.userId});
|
||||
|
||||
@override
|
||||
State<RecipeList> createState() => _RecipeListState();
|
||||
}
|
||||
|
||||
class _RecipeListState extends State<RecipeList> {
|
||||
List<RecipeSummary> recipeSummaryList = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
refreshRecipeList();
|
||||
|
||||
// 初始化加载数据
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (widget.userId == 0) {
|
||||
context.read<FoodProvider>().refreshRecipeList();
|
||||
} else {
|
||||
context.read<FoodProvider>().queryRecipeByUserId(widget.userId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> refreshRecipeList() async {
|
||||
final result = await queryRecipeApi(RecipeQuery(category: ""));
|
||||
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]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
recipeSummaryList = result;
|
||||
});
|
||||
Widget _buildContent(FoodProvider provider) {
|
||||
if (provider.recipeSummaryList.isEmpty) {
|
||||
return buildEmptyData();
|
||||
} else {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(child: _buildRecipeList(provider)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (recipeSummaryList.isEmpty) {
|
||||
return const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据');
|
||||
} else {
|
||||
return ListView.builder(
|
||||
itemCount: recipeSummaryList.length,
|
||||
itemBuilder: (context, index) {
|
||||
return RecipeCard(recipe: recipeSummaryList[index]);
|
||||
}
|
||||
final provider = context.watch<FoodProvider>();
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
if (provider.isLoading)
|
||||
buildLoadingIndicator()
|
||||
else
|
||||
_buildContent(provider),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:food_hub_app/apis/recipe.dart';
|
||||
import 'package:food_hub_app/config/app_config.dart';
|
||||
import 'package:flutter_common/widget/common_widget.dart';
|
||||
import 'package:flutter_common/widget/year_selector.dart';
|
||||
import 'package:food_hub_app/models/recipe.dart';
|
||||
import 'package:food_hub_app/provider/food_provider.dart';
|
||||
import 'package:food_hub_app/widgets/common/image.dart';
|
||||
import 'package:food_hub_app/widgets/common/year_selector.dart';
|
||||
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
||||
import 'package:timelines_plus/timelines_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RecipeTimeline extends StatefulWidget {
|
||||
const RecipeTimeline({super.key});
|
||||
@@ -15,121 +15,101 @@ class RecipeTimeline extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RecipeTimeline extends State<RecipeTimeline> {
|
||||
List<Record> recordList = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
refreshRecord(DateTime.now().year);
|
||||
}
|
||||
|
||||
Future<void> refreshRecord(int year) async {
|
||||
final result = await queryRecordApi("$year-01-01", "$year-12-31");
|
||||
setState(() {
|
||||
recordList = result;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget buildTimelineCard(BuildContext context, FoodRecord record) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
YearSelector(
|
||||
initialYear: DateTime.now().year,
|
||||
minYear: 2000,
|
||||
maxYear: 2100,
|
||||
onYearChanged: (year) => refreshRecord(year),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.date_range,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
Expanded(child: timelineContainer(recordList)),
|
||||
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 timelineContainer(List<Record> recordList) {
|
||||
Widget buildTimeline(BuildContext context, List<FoodRecord> recordList) {
|
||||
if (recordList.isEmpty) {
|
||||
return const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据');
|
||||
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: Theme.of(context).primaryColor,
|
||||
),
|
||||
(context, index, type) =>
|
||||
Connector.solidLine(thickness: 2, color: colors.primary),
|
||||
indicatorBuilder: (context, index) {
|
||||
return Indicator.dot(
|
||||
size: 12.0,
|
||||
color: Theme.of(context).primaryColor,
|
||||
);
|
||||
return Indicator.dot(size: 12.0, color: colors.primary);
|
||||
},
|
||||
contentsBuilder: (context, index) {
|
||||
return TimelineCard(record: recordList[index]);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 8),
|
||||
child: buildTimelineCard(context, recordList[index]),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TimelineCard extends StatelessWidget {
|
||||
final Record record;
|
||||
|
||||
const TimelineCard({super.key, required this.record});
|
||||
|
||||
Widget cardContent(BuildContext context) {
|
||||
final imageUrls = ['${AppConfig.baseApiUrl}/${record.imageUrl}'];
|
||||
|
||||
void imageTapClick() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder:
|
||||
(context) => ImagePreviewPage(images: imageUrls, initialIndex: 0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: Colors.white,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Column(
|
||||
Widget _buildContent(FoodProvider provider) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(record.name, style: TextStyle(fontSize: 16)),
|
||||
const SizedBox(height: 5),
|
||||
GestureDetector(
|
||||
onTap: () => imageTapClick(),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.5,
|
||||
child: networkImage(imageUrls[0]),
|
||||
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) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10, bottom: 5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
final provider = context.watch<FoodProvider>();
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Text(
|
||||
record.date,
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
cardContent(context),
|
||||
if (provider.isLoading)
|
||||
buildLoadingIndicator()
|
||||
else
|
||||
_buildContent(provider),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:food_hub_app/models/stats.dart';
|
||||
|
||||
class StatisticCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String title;
|
||||
final num value;
|
||||
final String unit;
|
||||
|
||||
const StatisticCard({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.unit,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
color: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Icon(icon, color: color, size: 40),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(title, style: TextStyle(fontSize: 20, color: color)),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
value.toString(),
|
||||
style: TextStyle(fontSize: 20, color: color),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Text(unit, style: TextStyle(fontSize: 20, color: color)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取排名对应的颜色
|
||||
Color _getRankColor(int index) {
|
||||
switch (index) {
|
||||
case 0: // 第1名
|
||||
return Colors.amber; // 金色
|
||||
case 1: // 第2名
|
||||
return Colors.grey; // 银色
|
||||
case 2: // 第3名
|
||||
return Colors.orange[700]!; // 铜色
|
||||
default:
|
||||
return Colors.black; // 普通颜色
|
||||
}
|
||||
}
|
||||
|
||||
Widget rankChart(List<ChartData> items) {
|
||||
return ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
final rank = index + 1;
|
||||
return SizedBox(
|
||||
height: 35,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 25,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'$rank.',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _getRankColor(index),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
item.name,
|
||||
style: TextStyle(color: _getRankColor(index)),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'${item.value.toStringAsFixed(0)} 次',
|
||||
style: TextStyle(color: _getRankColor(index)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -6,10 +6,14 @@
|
||||
|
||||
#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) {
|
||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) rive_native_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "RiveNativePlugin");
|
||||
rive_native_plugin_register_with_registrar(rive_native_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
rive_native
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
@@ -5,10 +5,20 @@
|
||||
import FlutterMacOS
|
||||
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 sqflite_darwin
|
||||
|
||||
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"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
}
|
||||
|
||||
479
pubspec.lock
479
pubspec.lock
@@ -33,6 +33,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -41,6 +49,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -105,6 +121,30 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -165,10 +205,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
version: "3.0.7"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -185,22 +225,30 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio
|
||||
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
|
||||
name: dbus
|
||||
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
|
||||
sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
easy_refresh:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -209,14 +257,22 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -233,6 +289,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -278,6 +342,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
@@ -286,6 +358,13 @@ packages:
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -294,6 +373,62 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -315,22 +450,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.28"
|
||||
flutter_slidable:
|
||||
flutter_shaders:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_slidable
|
||||
sha256: a857de7ea701f276fd6a6c4c67ae885b60729a3449e42766bb0e655171042801
|
||||
name: flutter_shaders
|
||||
sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
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"
|
||||
version: "0.1.3"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -342,13 +469,13 @@ packages:
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
fluttertoast:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fluttertoast
|
||||
sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1"
|
||||
sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "8.2.12"
|
||||
version: "8.2.14"
|
||||
form_builder_validators:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -473,10 +600,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
version: "0.20.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -513,26 +640,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "10.0.8"
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.9"
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.0.2"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -541,6 +668,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -577,10 +712,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
version: "1.17.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -589,6 +724,30 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -621,6 +780,30 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -645,6 +828,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
@@ -677,6 +868,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -693,30 +892,70 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
rive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
|
||||
name: rive
|
||||
sha256: "84a640f48122679e48397935ed1b971db19ef2e44bfcbae60e42fb5ce29a0a4e"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac"
|
||||
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.10"
|
||||
version: "2.4.23"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
|
||||
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
version: "2.5.6"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -729,10 +968,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
version: "2.4.2"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -802,6 +1041,46 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -835,37 +1114,37 @@ packages:
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
syncfusion_flutter_charts:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_charts
|
||||
sha256: c58ca79e072680af6f0554f7c4b91886c1d8808f2522b49d557f16fb5cb3bb04
|
||||
sha256: "68fdb029dad34a46e4c9cfad8ad66fe29db7b303bd96849261ab2b23a168d0e8"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "30.1.41"
|
||||
version: "30.2.7"
|
||||
syncfusion_flutter_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_core
|
||||
sha256: "536753489e168f49659261d0abd02b101b34c0b1bde27898f2869aab05f1cbb6"
|
||||
sha256: bfd026c0f9822b49ff26fed11cd3334519acb6a6ad4b0c81d9cd18df6af1c4c0
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "30.1.41"
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: table_calendar
|
||||
sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63
|
||||
sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
tdesign_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: tdesign_flutter
|
||||
sha256: b36b6f939f7a585184665202b6b3acf1922728962312b65182dbbff64c3b62f2
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.2.3"
|
||||
version: "3.2.0"
|
||||
tdesign_flutter_adaptation:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
@@ -886,10 +1165,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
version: "0.7.7"
|
||||
timelines_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -906,6 +1185,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -914,14 +1201,54 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.2.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -962,6 +1289,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -970,6 +1305,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -979,5 +1322,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.7.0 <4.0.0"
|
||||
flutter: ">=3.29.0"
|
||||
dart: ">=3.10.0 <4.0.0"
|
||||
flutter: ">=3.38.0"
|
||||
|
||||
117
pubspec.yaml
117
pubspec.yaml
@@ -1,116 +1 @@
|
||||
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
|
||||
intl: ^0.19.0
|
||||
tdesign_flutter: ^0.2.3
|
||||
json_annotation: ^4.9.0
|
||||
fluttertoast: ^8.2.0
|
||||
shared_preferences: ^2.3.0
|
||||
logger: ^2.6.0
|
||||
photo_view: ^0.15.0
|
||||
flutter_carousel_widget: ^3.1.0
|
||||
easy_refresh: ^3.4.0
|
||||
syncfusion_flutter_charts: ^30.1.41
|
||||
|
||||
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
|
||||
name: food_hub_app
|
||||
@@ -6,9 +6,15 @@
|
||||
|
||||
#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) {
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
RiveNativePluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("RiveNativePlugin"));
|
||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_windows
|
||||
rive_native
|
||||
share_plus
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
Reference in New Issue
Block a user