From 7da3ec95073c63e72181541e0c220493bd2dfc14 Mon Sep 17 00:00:00 2001 From: Cxx0822 <1556464090@qq.com> Date: Wed, 23 Jul 2025 23:00:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=9B=B4=E6=96=B0=E6=9C=8B=E5=8F=8B?= =?UTF-8?q?=E5=9C=88=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/views/moment.dart | 8 +- lib/views/record.dart | 43 +++++------ lib/widgets/common/image_preview.dart | 87 +++++++++++++++++++++ lib/widgets/common/index.dart | 12 +-- lib/widgets/moment/card.dart | 106 ++++++++++++++------------ lib/widgets/recipe/card.dart | 1 + pubspec.lock | 16 ++++ pubspec.yaml | 2 + 8 files changed, 193 insertions(+), 82 deletions(-) create mode 100644 lib/widgets/common/image_preview.dart diff --git a/lib/views/moment.dart b/lib/views/moment.dart index f55145f..e442f40 100644 --- a/lib/views/moment.dart +++ b/lib/views/moment.dart @@ -28,17 +28,19 @@ class _MomentPage extends State { }); } - @override Widget build(BuildContext context) { if (momentList.isEmpty) { return const TDEmpty(type: TDEmptyType.plain, emptyText: '暂无数据'); } else { - return ListView.builder( + return Padding( + padding: EdgeInsets.all(5), + child: ListView.builder( itemCount: momentList.length, itemBuilder: (context, index) { return MomentCard(moment: momentList[index]); - } + }, + ), ); } } diff --git a/lib/views/record.dart b/lib/views/record.dart index 108e739..0cc0b5a 100644 --- a/lib/views/record.dart +++ b/lib/views/record.dart @@ -39,31 +39,28 @@ class _RecordPageState extends State @override Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.all(0), - child: Column( - children: [ - TDTabBar( - tabs: tabs, - controller: _tabController, - backgroundColor: Colors.white, - showIndicator: true, - ), - Expanded( - child: Padding( - padding: EdgeInsets.all(5), - child: TabBarView( - controller: _tabController, - children: [ - RecipeList(), - RecipeCalendar(), - RecipeTimeline(), - ], - ), + return Column( + children: [ + TDTabBar( + tabs: tabs, + controller: _tabController, + backgroundColor: Colors.white, + showIndicator: true, + ), + Expanded( + child: Padding( + padding: EdgeInsets.all(5), + child: TabBarView( + controller: _tabController, + children: [ + RecipeList(), + RecipeCalendar(), + RecipeTimeline(), + ], ), ), - ], - ), + ), + ], ); } } diff --git a/lib/widgets/common/image_preview.dart b/lib/widgets/common/image_preview.dart new file mode 100644 index 0000000..f9a930b --- /dev/null +++ b/lib/widgets/common/image_preview.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:photo_view/photo_view_gallery.dart'; + +class ImagePreviewPage extends StatefulWidget { + final List images; + final int initialIndex; + + const ImagePreviewPage({ + super.key, + required this.images, + required this.initialIndex, + }) : assert(initialIndex >= 0 && initialIndex < images.length); + + @override + State createState() => _ImagePreviewPageState(); +} + +class _ImagePreviewPageState extends State { + // 声明 PageController 并初始化初始索引 + late PageController _pageController; + // 记录当前显示的图片索引(用于更新页码) + int _currentIndex = 0; + + @override + void initState() { + super.initState(); + // 初始化控制器,设置初始页面 + _pageController = PageController(initialPage: widget.initialIndex); + // 初始化当前索引为初始索引 + _currentIndex = widget.initialIndex; + + // 监听页面切换事件 + _pageController.addListener(() { + // 取当前页面的整数索引(避免滑动过程中的小数) + final currentPage = _pageController.page?.round() ?? 0; + // 只有当索引变化时才更新状态 + if (currentPage != _currentIndex) { + setState(() { + _currentIndex = currentPage; + }); + } + }); + } + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.black54, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + // 显示实时更新的页码(当前索引+1 / 总数量) + title: Text( + '${_currentIndex + 1}/${widget.images.length}', + style: const TextStyle(color: Colors.white), + ), + centerTitle: true, + ), + body: PhotoViewGallery( + pageOptions: widget.images.map((url) { + return PhotoViewGalleryPageOptions( + imageProvider: NetworkImage(url), + minScale: PhotoViewComputedScale.contained, + maxScale: PhotoViewComputedScale.covered * 2, + // 点击空白处关闭预览 + onTapDown: (context, details, controllerValue) { + Navigator.pop(context); + }, + ); + }).toList(), + pageController: _pageController, + scrollDirection: Axis.horizontal, + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/common/index.dart b/lib/widgets/common/index.dart index cde3247..39d87a6 100644 --- a/lib/widgets/common/index.dart +++ b/lib/widgets/common/index.dart @@ -53,16 +53,10 @@ Text buttonText({required String text}) { Widget errorImageContainer(double height) { return Container( - height: height, + height: 200, + width: double.infinity, color: Colors.grey[200], - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.error, color: Colors.red), - const SizedBox(width: 5), - const Text("加载失败", style: TextStyle(color: Colors.red)), - ], - ), + child: const Icon(Icons.image, color: Colors.grey, size: 30), ); } diff --git a/lib/widgets/moment/card.dart b/lib/widgets/moment/card.dart index aff6975..4c35ad9 100644 --- a/lib/widgets/moment/card.dart +++ b/lib/widgets/moment/card.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:food_hub_app/config/app_config.dart'; import 'package:food_hub_app/models/moment.dart'; +import 'package:food_hub_app/widgets/common/image_preview.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; class MomentCard extends StatelessWidget { @@ -19,7 +20,7 @@ class MomentCard extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildAvatar(), + _buildAvatar(context), const SizedBox(width: 12), Expanded( child: Column( @@ -30,7 +31,7 @@ class MomentCard extends StatelessWidget { _buildContent(), if (moment.imageList.isNotEmpty) ...[ const SizedBox(height: 8), - _buildPostImages(), + _buildPostImages(context), ], const SizedBox(height: 10), _buildTime(), @@ -46,29 +47,21 @@ class MomentCard extends StatelessWidget { } // 构建头像组件 - 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 _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]); + } else { + return TDAvatar( + size: TDAvatarSize.medium, + type: TDAvatarType.normal, + fit: BoxFit.contain, + avatarUrl: '${AppConfig.baseApiUrl}/${moment.avatar}'); + } } // 构建昵称组件 @@ -78,7 +71,6 @@ class MomentCard extends StatelessWidget { style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: Colors.black87, ), ); } @@ -87,32 +79,35 @@ class MomentCard extends StatelessWidget { Widget _buildContent() { return Text( moment.content, - style: const TextStyle(fontSize: 15, color: Colors.black87, height: 1.3), + style: const TextStyle(fontSize: 15, height: 1.3), ); } // 构建朋友圈图片列表(网格布局) - Widget _buildPostImages() { - // 图片数量 + Widget _buildPostImages(BuildContext context) { final imageCount = moment.imageList.length; - // 计算网格布局 + // 计算网格列数 int crossAxisCount; if (imageCount == 1) { - crossAxisCount = 1; // 单张图片 + crossAxisCount = 1; } else if (imageCount == 2 || imageCount == 4) { - crossAxisCount = 2; // 2张或4张图片用2列 + crossAxisCount = 2; } else { - crossAxisCount = 3; // 其他数量用3列 + crossAxisCount = 3; } - // 计算图片尺寸 - double itemAspectRatio = 1.0; // 默认正方形 + // 计算宽高比 + double itemAspectRatio = 1.0; if (imageCount == 1) { - // 单张图片使用宽屏比例 itemAspectRatio = 1.5; } + // 生成图片URL列表(用于预览时切换) + final List imageUrls = moment.imageList + .map((path) => '${AppConfig.baseApiUrl}/$path') + .toList(); + return GridView.count( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -121,17 +116,34 @@ class MomentCard extends StatelessWidget { 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), - ); - }, + // 单个图片项:添加点击事件 + return GestureDetector( + // 点击图片时,跳转到预览页面 + onTap: () { + // 导航到全屏预览页面,传入所有图片URL和当前点击的索引 + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ImagePreviewPage( + images: imageUrls, + initialIndex: index, + ), + ), + ); + }, + // 原图片组件 + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + imageUrls[index], + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.grey[200], + child: const Icon(Icons.image, color: Colors.grey, size: 30), + ); + }, + ), ), ); }), diff --git a/lib/widgets/recipe/card.dart b/lib/widgets/recipe/card.dart index 581caa5..073099f 100644 --- a/lib/widgets/recipe/card.dart +++ b/lib/widgets/recipe/card.dart @@ -1,3 +1,4 @@ +import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; import 'package:food_hub_app/config/app_config.dart'; diff --git a/pubspec.lock b/pubspec.lock index ddc68c2..87717d9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -105,6 +105,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "8.10.1" + carousel_slider: + dependency: "direct main" + description: + name: carousel_slider + sha256: bcc61735345c9ab5cb81073896579e735f81e35fd588907a393143ea986be8ff + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.1.1" characters: dependency: transitive description: @@ -645,6 +653,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.3.0" + photo_view: + dependency: "direct main" + description: + name: photo_view + sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.15.0" platform: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9b38884..fcd36ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,6 +47,8 @@ dependencies: fluttertoast: ^8.2.0 shared_preferences: ^2.3.0 logger: ^2.6.0 + photo_view: ^0.15.0 + carousel_slider: ^5.1.1 dependency_overrides: tdesign_flutter_adaptation: 3.16.0