feat:增加加载中组件

This commit is contained in:
2025-11-19 16:35:20 +08:00
parent 625a872d2f
commit 8acad9c63a
15 changed files with 616 additions and 344 deletions

View File

@@ -181,3 +181,73 @@ Widget pieChart({
],
);
}
Widget buildChartTitle({required BuildContext context, required String title}) {
return Row(
children: [
Icon(Icons.insert_chart, color: Theme.of(context).colorScheme.primary),
Text(title),
],
);
}
Widget buildChartDivider({required BuildContext context}) {
return Divider(
height: 1,
thickness: 1,
color: Theme.of(context).colorScheme.primary,
indent: 0,
endIndent: 0,
);
}
Color _getRankColor(int index) {
switch (index) {
case 0: // 第1名
return Colors.amber; // 金色
case 1: // 第2名
return Colors.grey; // 银色
case 2: // 第3名
return Colors.orange[700]!; // 铜色
default:
return Colors.black; // 普通颜色
}
}
Widget rankChart({required String unit, required List<ChartData> data}) {
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
final item = data[index];
final rank = index + 1;
return SizedBox(
height: 35,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
width: 25,
alignment: Alignment.center,
child: Text(
'$rank.',
style: TextStyle(
fontWeight: FontWeight.bold,
color: _getRankColor(index),
),
),
),
Text(item.name, style: TextStyle(color: _getRankColor(index))),
],
),
Text(
'${item.value.toStringAsFixed(0)} $unit',
style: TextStyle(color: _getRankColor(index)),
),
],
),
);
},
);
}

View File

@@ -1,6 +1,7 @@
import 'dart:io';
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';
@@ -113,10 +114,13 @@ Widget buildNetworkImage(BuildContext context, String url) {
borderRadius: BorderRadius.circular(8),
child: GestureDetector(
onTap: () {
showFullScreenImage(context, NetworkImage(url));
showFullScreenImage(
context,
NetworkImage('${AppConfig.imageBaseUrl}$url'),
);
},
child: Image.network(
url,
'${AppConfig.imageBaseUrl}$url',
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;

View File

@@ -29,7 +29,9 @@ InputDecoration formInputDecoration({
}
Widget buildCard({required BuildContext context, required Widget child}) {
final colors = Theme.of(context).colorScheme;
final colors = Theme
.of(context)
.colorScheme;
return Card(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
@@ -64,7 +66,11 @@ Widget buildToggleSwitch<T extends Enum>({
initialLabelIndex: tabValues.indexOf(currentTab),
totalSwitches: tabValues.length,
labels: labels,
activeBgColor: [Theme.of(context).colorScheme.primary],
activeBgColor: [Theme
.of(context)
.colorScheme
.primary
],
activeFgColor: Colors.white,
inactiveBgColor: Colors.grey.shade200,
inactiveFgColor: Colors.grey.shade700,
@@ -93,7 +99,10 @@ Widget circleIconButton({
fixedSize: Size(size, size),
shape: CircleBorder(),
elevation: 0,
backgroundColor: Theme.of(context).colorScheme.primary,
backgroundColor: Theme
.of(context)
.colorScheme
.primary,
padding: EdgeInsets.zero,
minimumSize: const Size(0, 0),
),
@@ -102,7 +111,9 @@ Widget circleIconButton({
}
Widget buildTag(BuildContext context, String title) {
final colors = Theme.of(context).colorScheme;
final colors = Theme
.of(context)
.colorScheme;
return Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 4),
@@ -173,3 +184,41 @@ void showErrorToast(String message) {
fontSize: 16.0,
);
}
Widget buildLoadingIndicator(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Center(
child: Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(colors.primary),
),
SizedBox(height: 12),
Text(
'加载中',
style: TextStyle(
fontSize: 14,
color: colors.onSurface,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}

View File

@@ -100,9 +100,7 @@ class MomentCard extends StatelessWidget {
// 生成图片URL列表用于预览时切换
final List<String> imageUrls =
moment.imageList
.map((path) => '${AppConfig.imageBaseUrl}$path')
.toList();
moment.imageList.map((path) => path).toList();
void imageTapClick(int index) {
Navigator.push(

View File

@@ -1,8 +1,6 @@
import 'package:flutter/material.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/date_util.dart';
import 'package:food_hub_app/utils/index.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:table_calendar/table_calendar.dart';
@@ -16,46 +14,7 @@ class RecipeCalendar extends StatefulWidget {
}
class _RecipeCalendarState extends State<RecipeCalendar> {
DateTime _selectedDay = DateTime.now();
DateTime _focusedDay = DateTime.now();
List<FoodRecord> recordList = [];
List<FoodRecord> 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(
@@ -64,47 +23,53 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
),
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(null, null);
},
// 自定义日期单元格构建器
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(BuildContext context) {
Widget _dailyItem(BuildContext context, FoodProvider provider) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(formatDateTime(_selectedDay, 'MM月dd日 EEEE')),
if (selectRecordList.isEmpty)
Text(formatDateTime(provider.selectedDay, 'MM月dd日 EEEE')),
if (provider.selectRecordList.isEmpty)
buildEmptyData()
else
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: selectRecordList.length,
itemCount: provider.selectRecordList.length,
itemBuilder: (context, index) {
return _buildRecordItem(context, selectRecordList[index]);
return _buildRecordItem(
context,
provider.selectRecordList[index],
);
},
),
],
@@ -170,9 +135,14 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
);
}
Widget _buildDateWidget(DateTime day, bool isSelected, bool isToday) {
Widget _buildDateItem(
DateTime day,
bool isSelected,
bool isToday,
FoodProvider provider,
) {
// 检查当前日期是否在需要显示红点的列表中
bool shouldShowRedDot = recordList.any(
bool shouldShowRedDot = provider.recordList.any(
(item) => item.date == formatDateTime(day),
);
@@ -214,15 +184,31 @@ class _RecipeCalendarState extends State<RecipeCalendar> {
);
}
@override
Widget build(BuildContext context) {
Widget _buildContent(FoodProvider provider) {
return Column(
children: [
buildCard(context: context, child: _recipeCalendar()),
buildCard(context: context, child: _recipeCalendar(provider)),
Expanded(
child: buildCard(context: context, child: _dailyItem(context)),
child: buildCard(
context: context,
child: _dailyItem(context, provider),
),
),
],
);
}
@override
Widget build(BuildContext context) {
final provider = context.watch<FoodProvider>();
return Stack(
children: [
if (provider.isLoading)
buildLoadingIndicator(context)
else
_buildContent(provider),
],
);
}
}

View File

@@ -1,6 +1,4 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/config/app_config.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';
@@ -86,10 +84,7 @@ class RecipeCard extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildNetworkImage(
context,
'${AppConfig.imageBaseUrl}/$firstImageUrl',
),
buildNetworkImage(context, firstImageUrl),
SizedBox(height: 8),
GestureDetector(
onTap: () => navigatorToRecipeDetail(context),

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.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/widgets/common/index.dart';
import 'package:food_hub_app/widgets/recipe/card.dart';
import 'package:provider/provider.dart';
class RecipeList extends StatefulWidget {
const RecipeList({super.key});
@@ -12,33 +12,40 @@ class RecipeList extends StatefulWidget {
}
class _RecipeListState extends State<RecipeList> {
List<RecipeSummary> recipeSummaryList = [];
@override
void initState() {
super.initState();
refreshRecipeList();
// 初始化加载数据
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<FoodProvider>().refreshRecipeList();
});
}
Future<void> refreshRecipeList() async {
final result = await queryRecipeApi(RecipeQuery(category: ""));
setState(() {
recipeSummaryList = result;
});
Widget _buildContent(FoodProvider provider) {
if (provider.recipeSummaryList.isEmpty) {
return buildEmptyData();
} else {
return ListView.builder(
itemCount: provider.recipeSummaryList.length,
itemBuilder: (context, index) {
return RecipeCard(recipe: provider.recipeSummaryList[index]);
},
);
}
}
@override
Widget build(BuildContext context) {
if (recipeSummaryList.isEmpty) {
return buildEmptyData();
} 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(context)
else
_buildContent(provider),
],
);
}
}

View File

@@ -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:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/provider/food_provider.dart';
import 'package:food_hub_app/widgets/common/image.dart';
import 'package:food_hub_app/widgets/common/index.dart';
import 'package:food_hub_app/widgets/common/year_selector.dart';
import 'package:timelines_plus/timelines_plus.dart';
import 'package:provider/provider.dart';
class RecipeTimeline extends StatefulWidget {
const RecipeTimeline({super.key});
@@ -15,24 +15,7 @@ class RecipeTimeline extends StatefulWidget {
}
class _RecipeTimeline extends State<RecipeTimeline> {
List<FoodRecord> 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;
});
}
Widget buildTimelineCard(BuildContext context, FoodRecord record) {
final imageUrl = '${AppConfig.imageBaseUrl}${record.imageUrl}';
return buildCard(
context: context,
child: Column(
@@ -58,7 +41,7 @@ class _RecipeTimeline extends State<RecipeTimeline> {
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 5),
buildNetworkImage(context, imageUrl),
buildNetworkImage(context, record.imageUrl),
],
),
],
@@ -93,8 +76,7 @@ class _RecipeTimeline extends State<RecipeTimeline> {
}
}
@override
Widget build(BuildContext context) {
Widget _buildContent(FoodProvider provider) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -102,9 +84,25 @@ class _RecipeTimeline extends State<RecipeTimeline> {
initialYear: DateTime.now().year,
minYear: 2000,
maxYear: 2100,
onYearChanged: (year) => refreshRecord(year),
onYearChanged:
(year) =>
provider.refreshRecordList("$year-01-01", "$year-12-31"),
),
Expanded(child: buildTimeline(context, recordList)),
Expanded(child: buildTimeline(context, provider.recordList)),
],
);
}
@override
Widget build(BuildContext context) {
final provider = context.watch<FoodProvider>();
return Stack(
children: [
if (provider.isLoading)
buildLoadingIndicator(context)
else
_buildContent(provider),
],
);
}

View File

@@ -54,58 +54,3 @@ class StatisticCard extends StatelessWidget {
);
}
}
// 获取排名对应的颜色
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)),
),
],
),
);
},
);
}