feat:增加加载中组件
This commit is contained in:
@@ -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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user