feat:增加菜谱后端接口

This commit is contained in:
2025-07-16 19:59:35 +08:00
parent c8f93f15a1
commit eec0e9f7d3
14 changed files with 327 additions and 205 deletions

View File

@@ -1,5 +1,6 @@
import 'package:food_hub_app/models/recipe.dart'; import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/utils/http_util.dart'; import 'package:food_hub_app/utils/http_util.dart';
import 'package:food_hub_app/utils/index.dart';
Future<Recipe> queryRecipeByIdApi(int id) { Future<Recipe> queryRecipeByIdApi(int id) {
return HttpUtil().get<Recipe>( return HttpUtil().get<Recipe>(
@@ -8,9 +9,36 @@ Future<Recipe> queryRecipeByIdApi(int id) {
); );
} }
Future<bool> addRecipeApi(Recipe recipe) {
return HttpUtil().post<bool>("/food/food/recipe", data: recipe);
}
Future<bool> updateRecipeApi(int id, Recipe recipe) {
return HttpUtil().put<bool>("/food/food/recipe/$id", data: recipe);
}
Future<bool> deleteRecipeApi(int id) {
return HttpUtil().delete<bool>("/food/food/recipe/$id");
}
Future<List<Recipe>> queryRecipeByUserApi(int id) { Future<List<Recipe>> queryRecipeByUserApi(int id) {
return HttpUtil().get<List<Recipe>>( return HttpUtil().get<List<Recipe>>(
"/food/recipe/user/$id", "/food/recipe/user/$id",
converter: (data) => data.map((item) => Recipe.fromJson(item)).toList() converter: (data) => convertListResponse<Recipe>(data, Recipe.fromJson),
); );
} }
Future<List<Recipe>> queryRecipeUserFavouriteApi() {
return HttpUtil().get<List<Recipe>>(
"/food/recipe/user/favourite",
converter: (data) => convertListResponse<Recipe>(data, Recipe.fromJson),
);
}
Future<List<Recipe>> queryRecipeApi(RecipeQuery recipeQuery) {
return HttpUtil().get<List<Recipe>>(
"/food/recipe",
queryParameters: recipeQuery.toJson(),
converter: (data) => convertListResponse<Recipe>(data, Recipe.fromJson),
);
}

View File

@@ -10,7 +10,7 @@ import 'package:shared_preferences/shared_preferences.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
FormBuilderLocalizations.delegate.load(const Locale('zh', 'CN')); FormBuilderLocalizations.delegate.load(const Locale('zh', 'CN'));
SPUtil.init(); await SPUtil.init();
runApp(const MyApp()); runApp(const MyApp());
} }

View File

@@ -9,11 +9,7 @@ class Material {
String name; String name;
String amount; String amount;
Material({ Material({required this.type, required this.name, required this.amount});
required this.type,
required this.name,
required this.amount,
});
factory Material.fromJson(Map<String, dynamic> json) => factory Material.fromJson(Map<String, dynamic> json) =>
_$MaterialFromJson(json); _$MaterialFromJson(json);
@@ -28,14 +24,9 @@ class Step {
String content; String content;
String imageUrl; String imageUrl;
Step({ Step({required this.sort, required this.content, required this.imageUrl});
required this.sort,
required this.content,
required this.imageUrl,
});
factory Step.fromJson(Map<String, dynamic> json) => factory Step.fromJson(Map<String, dynamic> json) => _$StepFromJson(json);
_$StepFromJson(json);
Map<String, dynamic> toJson() => _$StepToJson(this); Map<String, dynamic> toJson() => _$StepToJson(this);
} }
@@ -66,28 +57,40 @@ class Comment {
/// 成果信息 /// 成果信息
@JsonSerializable() @JsonSerializable()
class Record { class Record {
int id; int? id;
String name; String name;
String? category; String category;
int person; int person;
String date; String date;
String imageUrl; String imageUrl;
Record({ Record({
required this.id, this.id,
required this.name, required this.name,
this.category, required this.category,
required this.person, required this.person,
required this.date, required this.date,
required this.imageUrl, required this.imageUrl,
}); });
factory Record.fromJson(Map<String, dynamic> json) => factory Record.fromJson(Map<String, dynamic> json) => _$RecordFromJson(json);
_$RecordFromJson(json);
Map<String, dynamic> toJson() => _$RecordToJson(this); Map<String, dynamic> toJson() => _$RecordToJson(this);
} }
@JsonSerializable()
class RecipeQuery {
String category;
RecipeQuery({
required this.category
});
factory RecipeQuery.fromJson(Map<String, dynamic> json) => _$RecipeQueryFromJson(json);
Map<String, dynamic> toJson() => _$RecipeQueryToJson(this);
}
/// 菜谱信息 /// 菜谱信息
@JsonSerializable() @JsonSerializable()
class Recipe { class Recipe {
@@ -100,8 +103,8 @@ class Recipe {
int? userId; int? userId;
String? username; String? username;
String? avatar; String? avatar;
List<Material> materialList; List<Material>? materialList;
List<Step> stepList; List<Step>? stepList;
List<Record> recordList; List<Record> recordList;
List<int>? likeList; List<int>? likeList;
int? likeCount; int? likeCount;
@@ -120,8 +123,8 @@ class Recipe {
this.userId, this.userId,
this.username, this.username,
this.avatar, this.avatar,
required this.materialList, this.materialList,
required this.stepList, this.stepList,
required this.recordList, required this.recordList,
this.likeList, this.likeList,
this.likeCount, this.likeCount,
@@ -131,8 +134,7 @@ class Recipe {
this.commentCount, this.commentCount,
}); });
factory Recipe.fromJson(Map<String, dynamic> json) => factory Recipe.fromJson(Map<String, dynamic> json) => _$RecipeFromJson(json);
_$RecipeFromJson(json);
Map<String, dynamic> toJson() => _$RecipeToJson(this); Map<String, dynamic> toJson() => _$RecipeToJson(this);
} }

View File

@@ -47,9 +47,9 @@ Map<String, dynamic> _$CommentToJson(Comment instance) => <String, dynamic>{
}; };
Record _$RecordFromJson(Map<String, dynamic> json) => Record( Record _$RecordFromJson(Map<String, dynamic> json) => Record(
id: (json['id'] as num).toInt(), id: (json['id'] as num?)?.toInt(),
name: json['name'] as String, name: json['name'] as String,
category: json['category'] as String?, category: json['category'] as String,
person: (json['person'] as num).toInt(), person: (json['person'] as num).toInt(),
date: json['date'] as String, date: json['date'] as String,
imageUrl: json['imageUrl'] as String, imageUrl: json['imageUrl'] as String,
@@ -64,6 +64,12 @@ Map<String, dynamic> _$RecordToJson(Record instance) => <String, dynamic>{
'imageUrl': instance.imageUrl, 'imageUrl': instance.imageUrl,
}; };
RecipeQuery _$RecipeQueryFromJson(Map<String, dynamic> json) =>
RecipeQuery(category: json['category'] as String);
Map<String, dynamic> _$RecipeQueryToJson(RecipeQuery instance) =>
<String, dynamic>{'category': instance.category};
Recipe _$RecipeFromJson(Map<String, dynamic> json) => Recipe( Recipe _$RecipeFromJson(Map<String, dynamic> json) => Recipe(
id: (json['id'] as num?)?.toInt(), id: (json['id'] as num?)?.toInt(),
name: json['name'] as String, name: json['name'] as String,
@@ -75,12 +81,12 @@ Recipe _$RecipeFromJson(Map<String, dynamic> json) => Recipe(
username: json['username'] as String?, username: json['username'] as String?,
avatar: json['avatar'] as String?, avatar: json['avatar'] as String?,
materialList: materialList:
(json['materialList'] as List<dynamic>) (json['materialList'] as List<dynamic>?)
.map((e) => Material.fromJson(e as Map<String, dynamic>)) ?.map((e) => Material.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
stepList: stepList:
(json['stepList'] as List<dynamic>) (json['stepList'] as List<dynamic>?)
.map((e) => Step.fromJson(e as Map<String, dynamic>)) ?.map((e) => Step.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
recordList: recordList:
(json['recordList'] as List<dynamic>) (json['recordList'] as List<dynamic>)

View File

@@ -2,6 +2,8 @@ import 'package:dio/dio.dart';
import 'package:food_hub_app/utils/sp_util.dart'; import 'package:food_hub_app/utils/sp_util.dart';
import 'package:food_hub_app/widgets/common/index.dart'; import 'package:food_hub_app/widgets/common/index.dart';
import 'log_util.dart';
class HttpUtil { class HttpUtil {
static final HttpUtil _instance = HttpUtil._internal(); static final HttpUtil _instance = HttpUtil._internal();
@@ -34,9 +36,9 @@ class HttpUtil {
_dio.interceptors.add( _dio.interceptors.add(
InterceptorsWrapper( InterceptorsWrapper(
onRequest: (options, handler) { onRequest: (options, handler) {
print("请求URL: ${options.uri}"); logger.d("请求URL: ${options.uri}");
if (options.data != null) { if (options.data != null) {
print("请求参数: ${options.data}"); logger.d("请求参数: ${options.data}");
} }
if (SPUtil.getString('token').isNotEmpty) { if (SPUtil.getString('token').isNotEmpty) {
@@ -51,8 +53,8 @@ class HttpUtil {
_dio.interceptors.add( _dio.interceptors.add(
InterceptorsWrapper( InterceptorsWrapper(
onResponse: (response, handler) { onResponse: (response, handler) {
print("响应状态码: ${response.statusCode}"); logger.d("响应状态码: ${response.statusCode}");
print("响应数据: ${response.data}"); logger.d("响应数据: ${response.data}");
return handler.next(response); return handler.next(response);
}, },
), ),

View File

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

12
lib/utils/log_util.dart Normal file
View File

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

View File

@@ -1,13 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:food_hub_app/api/recipe.dart';
import 'package:food_hub_app/api/session.dart'; import 'package:food_hub_app/api/session.dart';
import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/models/session.dart'; import 'package:food_hub_app/models/session.dart';
import 'package:food_hub_app/utils/sp_util.dart'; import 'package:food_hub_app/utils/sp_util.dart';
import 'package:food_hub_app/widgets/common/index.dart'; import 'package:food_hub_app/widgets/common/index.dart';
import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart';
class LoginPage extends StatefulWidget { class LoginPage extends StatefulWidget {
@@ -20,7 +17,8 @@ class LoginPage extends StatefulWidget {
class _LoginPage extends State<LoginPage> { class _LoginPage extends State<LoginPage> {
final GlobalKey _formKey = GlobalKey<FormState>(); final GlobalKey _formKey = GlobalKey<FormState>();
late String _username, _password; String _username = "", _password = "";
bool _isRemember = false;
bool _isObscure = true; bool _isObscure = true;
Color _eyeColor = Colors.grey; Color _eyeColor = Colors.grey;
final List _loginMethod = [ final List _loginMethod = [
@@ -29,39 +27,51 @@ class _LoginPage extends State<LoginPage> {
{"title": "wechat", "icon": Icons.wechat}, {"title": "wechat", "icon": Icons.wechat},
]; ];
@override
void initState() {
super.initState();
_loadRememberState();
}
Future<void> _loadRememberState() async {
bool? isRemember = await SPUtil.getBool('isRemember');
if (isRemember) {
setState(() {
_isRemember = true;
_username = SPUtil.getString('username');
_password = SPUtil.getString('password');
});
}
}
void handleRememberState() {
if (_isRemember) {
SPUtil.set('isRemember', true);
SPUtil.set('username', _username);
SPUtil.set('password', _password);
} else {
SPUtil.set('isRemember', false);
SPUtil.remove('username');
SPUtil.remove('password');
}
}
void handleTokenState(String token) {
SPUtil.set('token', token);
}
void loginClick(BuildContext context) async { void loginClick(BuildContext context) async {
SPUtil.clear();
Session session = await loginApi("Cxx0822", "19940822Cxx");
SPUtil.set('token', session.saToken.tokenValue);
Recipe recipe = await queryRecipeByIdApi(614660173221957);
List<Recipe> recipeList = await queryRecipeByUserApi(691110779121733);
// SPUtil.set("token", session?.saToken?.tokenValue);
// Navigator.pushNamed(context, '/home');
return;
// 表单校验通过才会继续执行 // 表单校验通过才会继续执行
if ((_formKey.currentState as FormState).validate()) { if ((_formKey.currentState as FormState).validate()) {
(_formKey.currentState as FormState).save(); (_formKey.currentState as FormState).save();
TDMessage.showMessage( Session session = await loginApi(_username, _password);
context: context, showSuccessToast('登录成功');
visible: true,
icon: true, handleRememberState();
content: "登录成功", handleTokenState(session.saToken.tokenValue);
theme: MessageTheme.success,
duration: 3000,
);
Navigator.pushNamed(context, '/home'); Navigator.pushNamed(context, '/home');
} else { } else {
TDMessage.showMessage( showErrorToast('请先输入信息');
context: context,
visible: true,
icon: true,
content: "请先输入信息",
theme: MessageTheme.error,
duration: 3000,
);
} }
} }
@@ -135,6 +145,8 @@ class _LoginPage extends State<LoginPage> {
const SizedBox(height: 10), const SizedBox(height: 10),
FormBuilderTextField( FormBuilderTextField(
name: 'username', name: 'username',
initialValue: _username,
keyboardType: TextInputType.text,
decoration: formInputDecoration( decoration: formInputDecoration(
hintText: "请输入账号", hintText: "请输入账号",
prefixIcon: Icons.person, prefixIcon: Icons.person,
@@ -154,8 +166,9 @@ class _LoginPage extends State<LoginPage> {
const SizedBox(height: 10), const SizedBox(height: 10),
FormBuilderTextField( FormBuilderTextField(
name: 'password', name: 'password',
initialValue: _password,
keyboardType: TextInputType.visiblePassword,
obscureText: _isObscure, obscureText: _isObscure,
// 是否显示文字
onSaved: (v) => _password = v!, onSaved: (v) => _password = v!,
validator: FormBuilderValidators.required(), validator: FormBuilderValidators.required(),
decoration: InputDecoration( decoration: InputDecoration(
@@ -168,7 +181,6 @@ class _LoginPage extends State<LoginPage> {
suffixIcon: IconButton( suffixIcon: IconButton(
icon: Icon(Icons.remove_red_eye, color: _eyeColor), icon: Icon(Icons.remove_red_eye, color: _eyeColor),
onPressed: () { onPressed: () {
// 修改 state 内部变量, 且需要界面内容更新, 需要使用 setState()
setState(() { setState(() {
_isObscure = !_isObscure; _isObscure = !_isObscure;
_eyeColor = _eyeColor =
@@ -188,9 +200,11 @@ class _LoginPage extends State<LoginPage> {
return Row( return Row(
children: [ children: [
Checkbox( Checkbox(
value: false, value: _isRemember,
onChanged: (bool? value) { onChanged: (bool? value) {
print("记住密码: $value"); setState(() {
_isRemember = value ?? false;
});
}, },
), ),
const Text('记住密码', style: TextStyle(color: Colors.grey, fontSize: 14)), const Text('记住密码', style: TextStyle(color: Colors.grey, fontSize: 14)),

View File

@@ -1,10 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:food_hub_app/api/recipe.dart';
import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/widgets/recipe/recipe_calendar.dart'; import 'package:food_hub_app/widgets/recipe/recipe_calendar.dart';
import 'package:food_hub_app/widgets/recipe/recipe_list.dart'; import 'package:food_hub_app/widgets/recipe/recipe_list.dart';
import 'package:food_hub_app/widgets/recipe/recipe_timeline.dart'; import 'package:food_hub_app/widgets/recipe/recipe_timeline.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart';
import '../models/recipe.dart';
class RecordPage extends StatefulWidget { class RecordPage extends StatefulWidget {
const RecordPage({super.key}); const RecordPage({super.key});
@@ -15,33 +16,62 @@ class RecordPage extends StatefulWidget {
class _RecordPageState extends State<RecordPage> class _RecordPageState extends State<RecordPage>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
final List<Recipe> recipeList = []; List<Recipe> _recipeList = [];
final List<Record> recordList = []; final List<Record> _recordList = [];
late final TabController _tabController = TabController( late final TabController _tabController = TabController(
length: 3, length: 3,
vsync: this, vsync: this,
); );
@override
void initState() {
super.initState();
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
// 定义标签列表
final List<TDTab> tabs = [ final List<TDTab> tabs = [
const TDTab(text: '菜谱', icon: Icon(Icons.book)), const TDTab(text: '菜谱', icon: Icon(Icons.book)),
const TDTab(text: '日历', icon: Icon(Icons.calendar_month)), const TDTab(text: '日历', icon: Icon(Icons.calendar_month)),
const TDTab(text: '时间轴', icon: Icon(Icons.timeline)), const TDTab(text: '时间轴', icon: Icon(Icons.timeline)),
]; ];
@override
void initState() {
super.initState();
_getRecipeList();
_tabController.addListener(_handleTabChange);
}
@override
void dispose() {
_tabController.removeListener(_handleTabChange);
_tabController.dispose();
super.dispose();
}
void _handleTabChange() {
if (_tabController.indexIsChanging) {
return;
}
// 根据当前选中的索引执行对应接口请求
switch (_tabController.index) {
case 0:
_getRecipeList();
break;
case 1:
print("2");
break;
case 2:
print("3");
break;
}
}
Future<void> _getRecipeList() async {
final result = await queryRecipeApi(RecipeQuery(category: ""));
setState(() {
_recipeList = result;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
@@ -60,9 +90,9 @@ class _RecordPageState extends State<RecordPage>
child: TabBarView( child: TabBarView(
controller: _tabController, controller: _tabController,
children: [ children: [
RecipeList(recipeList: recipeList), RecipeList(recipeList: _recipeList),
RecipeCalendar(), RecipeCalendar(),
RecipeTimeline(recordList: recordList), RecipeTimeline(recordList: _recordList),
], ],
), ),
), ),

View File

@@ -50,18 +50,18 @@ Text buttonText({required String text}) {
return Text(text, style: TextStyle(color: Colors.white)); return Text(text, style: TextStyle(color: Colors.white));
} }
// void showSuccessToast(String message) { void showSuccessToast(String message) {
// Fluttertoast.showToast( Fluttertoast.showToast(
// msg: message, msg: message,
// toastLength: Toast.LENGTH_SHORT, toastLength: Toast.LENGTH_SHORT,
// gravity: ToastGravity.CENTER, gravity: ToastGravity.CENTER,
// timeInSecForIosWeb: 1, timeInSecForIosWeb: 1,
// backgroundColor: Colors.green, backgroundColor: Colors.green,
// textColor: Colors.white, textColor: Colors.white,
// fontSize: 16.0, fontSize: 16.0,
// ); );
// } }
//
void showErrorToast(String message) { void showErrorToast(String message) {
Fluttertoast.showToast( Fluttertoast.showToast(
msg: message, msg: message,

View File

@@ -1,4 +1,3 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:food_hub_app/models/recipe.dart'; import 'package:food_hub_app/models/recipe.dart';
@@ -18,7 +17,7 @@ class RecipeCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Image.network( Image.network(
recipe.recordList[0].imageUrl, 'http://172.29.101.108:8100/${recipe.recordList[0].imageUrl}',
height: 200, height: 200,
width: double.infinity, width: double.infinity,
fit: BoxFit.contain, fit: BoxFit.contain,
@@ -38,104 +37,108 @@ class RecipeCard extends StatelessWidget {
), ),
Padding( Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: Column( child: _buildRecipeContent(),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
recipe.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
maxLines: 1, // 限制单行,避免挤压按钮
overflow: TextOverflow.ellipsis,
),
),
Row(
children: [
IconButton(
icon: const Icon(Icons.edit, size: 14),
color: Colors.white,
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.green,
),
shape: WidgetStateProperty.all(CircleBorder()),
minimumSize: WidgetStateProperty.all(Size(20, 20)),
),
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.book, size: 14),
color: Colors.white,
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.green,
),
shape: WidgetStateProperty.all(CircleBorder()),
minimumSize: WidgetStateProperty.all(Size(20, 20)),
),
onPressed: () {},
),
],
),
],
),
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
children: [
_buildIconText(
icon: Icons.food_bank,
text: recipe.category,
color: Colors.green,
),
const SizedBox(width: 12),
_buildIconText(
icon: Icons.thumb_up_alt_outlined,
text: recipe.likeCount.toString(),
color: Colors.red,
),
const SizedBox(width: 12),
_buildIconText(
icon: Icons.star_outline,
text: recipe.favouriteCount.toString(),
color: Colors.blue,
),
const SizedBox(width: 12),
_buildIconText(
icon: Icons.comment_outlined,
text: recipe.commentCount.toString(),
color: Colors.deepOrange,
),
],
),
Row(
children: [
_buildIconText(
icon: Icons.date_range,
text: recipe.recordList[0].date,
color: Colors.red,
),
],
),
],
),
],
),
), ),
], ],
), ),
); );
} }
Widget _buildRecipeContent() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
recipe.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
maxLines: 1, // 限制单行,避免挤压按钮
overflow: TextOverflow.ellipsis,
),
),
Row(
children: [
IconButton(
icon: const Icon(Icons.edit, size: 14),
color: Colors.white,
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.green,
),
shape: WidgetStateProperty.all(CircleBorder()),
minimumSize: WidgetStateProperty.all(Size(20, 20)),
),
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.book, size: 14),
color: Colors.white,
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.green,
),
shape: WidgetStateProperty.all(CircleBorder()),
minimumSize: WidgetStateProperty.all(Size(20, 20)),
),
onPressed: () {},
),
],
),
],
),
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
children: [
_buildIconText(
icon: Icons.food_bank,
text: recipe.category,
color: Colors.green,
),
const SizedBox(width: 12),
_buildIconText(
icon: Icons.thumb_up_alt_outlined,
text: recipe.likeCount.toString(),
color: Colors.red,
),
const SizedBox(width: 12),
_buildIconText(
icon: Icons.star_outline,
text: recipe.favouriteCount.toString(),
color: Colors.blue,
),
const SizedBox(width: 12),
_buildIconText(
icon: Icons.comment_outlined,
text: recipe.commentCount.toString(),
color: Colors.deepOrange,
),
],
),
Row(
children: [
_buildIconText(
icon: Icons.date_range,
text: recipe.recordList[0].date,
color: Colors.red,
),
],
),
],
),
],
);
}
// 通用图标文本组件 // 通用图标文本组件
Widget _buildIconText({ Widget _buildIconText({
required IconData icon, required IconData icon,

View File

@@ -1,6 +1,7 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart';
import 'package:food_hub_app/models/recipe.dart'; import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/widgets/recipe/recipe_card.dart'; import 'package:food_hub_app/widgets/recipe/recipe_card.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
class RecipeList extends StatelessWidget { class RecipeList extends StatelessWidget {
final List<Recipe> recipeList; final List<Recipe> recipeList;
@@ -9,14 +10,21 @@ class RecipeList extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView.separated( if (recipeList.isEmpty) {
itemCount: recipeList.length, return const TDEmpty(
itemBuilder: (context, index) { type: TDEmptyType.plain,
return RecipeCard(recipe: recipeList[index]); emptyText: '暂无数据',
}, );
separatorBuilder: (context, index) { } else {
return const SizedBox(height: 10); return ListView.separated(
}, itemCount: recipeList.length,
); itemBuilder: (context, index) {
return RecipeCard(recipe: recipeList[index]);
},
separatorBuilder: (context, index) {
return const SizedBox(height: 10);
},
);
}
} }
} }

View File

@@ -541,6 +541,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "5.1.1" version: "5.1.1"
logger:
dependency: "direct main"
description:
name: logger
sha256: "2621da01aabaf223f8f961e751f2c943dbb374dc3559b982f200ccedadaa6999"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.6.0"
logging: logging:
dependency: transitive dependency: transitive
description: description:

View File

@@ -46,6 +46,7 @@ dependencies:
json_annotation: ^4.9.0 json_annotation: ^4.9.0
fluttertoast: ^8.2.0 fluttertoast: ^8.2.0
shared_preferences: ^2.3.0 shared_preferences: ^2.3.0
logger: ^2.6.0
dependency_overrides: dependency_overrides:
tdesign_flutter_adaptation: 3.16.0 tdesign_flutter_adaptation: 3.16.0