feat:增加朋友圈组件

This commit is contained in:
2025-07-23 20:03:03 +08:00
parent 5d156d3a02
commit d9f1781277
22 changed files with 393 additions and 6 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

34
lib/api/moment.dart Normal file
View File

@@ -0,0 +1,34 @@
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';
Future<bool> addMomentApi(Moment moment) {
return HttpUtil().post<bool>("/moment", data: moment);
}
Future<bool> updateMomentApi(int id, Moment moment) {
return HttpUtil().put<bool>("/moment/$id", data: moment);
}
Future<bool> deleteMomentApi(int id) {
return HttpUtil().delete<bool>("/moment/$id");
}
Future<List<Moment>> queryMomentListApi() {
return HttpUtil().get<List<Moment>>(
"/moment",
converter: (data) => convertListResponse<Moment>(data, Moment.fromJson),
);
}
Future<bool> addMomentCommentApi(int id, String content) {
return HttpUtil().post<bool>("/moment/$id/comment", data: content);
}
Future<bool> addMomentLikeApi(int id) {
return HttpUtil().post<bool>("/moment/$id/like");
}
Future<bool> deleteMomentLikeApi(int id) {
return HttpUtil().delete<bool>("/moment/$id/like");
}

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

@@ -0,0 +1,53 @@
import 'package:json_annotation/json_annotation.dart';
part 'moment.g.dart';
@JsonSerializable()
class Comment {
final int id;
final String username;
final String avatar;
final String content;
final String date;
const Comment({
required this.id,
required this.username,
required this.avatar,
required this.content,
required this.date,
});
factory Comment.fromJson(Map<String, dynamic> json) => _$CommentFromJson(json);
Map<String, dynamic> toJson() => _$CommentToJson(this);
}
@JsonSerializable()
class Moment {
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;
Moment({
this.id,
this.userId,
this.username,
this.avatar,
required this.content,
required this.imageList,
this.date,
this.likeList,
this.commentList,
});
factory Moment.fromJson(Map<String, dynamic> json) => _$MomentFromJson(json);
Map<String, dynamic> toJson() => _$MomentToJson(this);
}

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

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

View File

@@ -1,4 +1,8 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/api/moment.dart';
import 'package:food_hub_app/models/moment.dart';
import 'package:food_hub_app/widgets/moment/card.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
class MomentPage extends StatefulWidget {
const MomentPage({super.key});
@@ -8,8 +12,34 @@ class MomentPage extends StatefulWidget {
}
class _MomentPage extends State<MomentPage> {
List<Moment> momentList = [];
@override
void initState() {
super.initState();
refreshRecipeList();
}
Future<void> refreshRecipeList() async {
final result = await queryMomentListApi();
setState(() {
momentList = result;
});
}
@override
Widget build(BuildContext context) {
return const Center(child: Text("朋友圈"));
if (momentList.isEmpty) {
return const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据');
} else {
return ListView.builder(
itemCount: momentList.length,
itemBuilder: (context, index) {
return MomentCard(moment: momentList[index]);
}
);
}
}
}

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.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_timeline.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';

View File

@@ -0,0 +1,216 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/config/app_config.dart';
import 'package:food_hub_app/models/moment.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
class MomentCard extends StatelessWidget {
final Moment moment;
const MomentCard({super.key, required this.moment});
@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),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildAvatar(),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildNickname(),
const SizedBox(height: 5),
_buildContent(),
if (moment.imageList.isNotEmpty) ...[
const SizedBox(height: 8),
_buildPostImages(),
],
const SizedBox(height: 10),
_buildTime(),
const SizedBox(height: 5),
_buildActionButtons(),
],
),
),
],
),
),
);
}
// 构建头像组件
Widget _buildAvatar() {
return TDAvatar(
size: TDAvatarSize.medium,
type: TDAvatarType.normal,
fit: BoxFit.contain,
avatarUrl: '${AppConfig.baseApiUrl}/${moment.avatar}');
// return SizedBox(
// width: 40,
// height: 40,
// child: ClipRRect(
// borderRadius: BorderRadius.circular(25),
// child: Image.network(
// '${AppConfig.baseApiUrl}/${moment.avatar}',
// fit: BoxFit.contain,
// errorBuilder: (context, error, stackTrace) {
// return Container(
// color: Colors.grey[200],
// child: const Icon(Icons.person, color: Colors.grey),
// );
// },
// ),
// ),
// );
}
// 构建昵称组件
Widget _buildNickname() {
return Text(
moment.username ?? "",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
);
}
// 构建内容组件
Widget _buildContent() {
return Text(
moment.content,
style: const TextStyle(fontSize: 15, color: Colors.black87, height: 1.3),
);
}
// 构建朋友圈图片列表(网格布局)
Widget _buildPostImages() {
// 图片数量
final imageCount = moment.imageList.length;
// 计算网格布局
int crossAxisCount;
if (imageCount == 1) {
crossAxisCount = 1; // 单张图片
} else if (imageCount == 2 || imageCount == 4) {
crossAxisCount = 2; // 2张或4张图片用2列
} else {
crossAxisCount = 3; // 其他数量用3列
}
// 计算图片尺寸
double itemAspectRatio = 1.0; // 默认正方形
if (imageCount == 1) {
// 单张图片使用宽屏比例
itemAspectRatio = 1.5;
}
return GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: crossAxisCount,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
childAspectRatio: itemAspectRatio,
children: List.generate(imageCount, (index) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
'${AppConfig.baseApiUrl}/${moment.imageList[index]}',
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[200],
child: const Icon(Icons.image, color: Colors.grey, size: 30),
);
},
),
);
}),
);
}
// 构建时间组件
Widget _buildTime() {
return Text(
moment.date ?? "",
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
);
}
// 构建点赞和评论按钮
Widget _buildActionButtons() {
return Row(
children: [
SizedBox(
width: 42,
height: 36,
child: Stack(
alignment: Alignment.bottomLeft,
children: [
Icon(Icons.thumb_up_alt_outlined),
Positioned(
left: 20,
bottom: 15,
child: TDBadge(
TDBadgeType.message,
count: (moment.likeList?.length ?? 0).toString(),
),
),
],
),
),
SizedBox(
width: 42,
height: 36,
child: Stack(
alignment: Alignment.bottomLeft,
children: [
Icon(Icons.comment_outlined),
Positioned(
left: 20,
bottom: 15,
child: TDBadge(
TDBadgeType.message,
count: (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(
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]),
),
],
);
}
}

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:food_hub_app/api/recipe.dart';
import 'package:food_hub_app/models/recipe.dart';
import 'package:food_hub_app/widgets/recipe/recipe_card.dart';
import 'package:food_hub_app/widgets/recipe/card.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
class RecipeList extends StatefulWidget {