From 7969bb41e32a34be4d7503b4306aebca4b76c44a Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Tue, 23 Jun 2026 20:07:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(client):=20=E5=95=86=E5=93=81=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=A1=B5=E5=9B=BE=E7=89=87=E6=94=AF=E6=8C=81=E5=8F=8C?= =?UTF-8?q?=E5=87=BB=E5=85=A8=E5=B1=8F=E6=9F=A5=E7=9C=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 抽出共享全屏查看器 fullscreen_image_viewer.dart(复用扫码页的 InteractiveViewer+PageView+黑底 modal),详情页缩略图双击打开, 扫码公开页改为复用同一组件去重。 Co-Authored-By: Claude Opus 4.8 --- .../products/product_detail_screen.dart | 43 ++++++---- .../screens/public/public_product_screen.dart | 68 +-------------- .../lib/widgets/fullscreen_image_viewer.dart | 86 +++++++++++++++++++ 3 files changed, 116 insertions(+), 81 deletions(-) create mode 100644 client/lib/widgets/fullscreen_image_viewer.dart diff --git a/client/lib/screens/products/product_detail_screen.dart b/client/lib/screens/products/product_detail_screen.dart index 27affb7..8cb6bb4 100644 --- a/client/lib/screens/products/product_detail_screen.dart +++ b/client/lib/screens/products/product_detail_screen.dart @@ -9,6 +9,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/config/app_config.dart'; import '../../core/utils/print_util.dart'; import '../../widgets/label_preview_dialog.dart'; +import '../../widgets/fullscreen_image_viewer.dart'; import '../../widgets/write_guard.dart'; import '../../core/theme/app_theme.dart'; import '../../models/product.dart'; @@ -309,9 +310,16 @@ class _ProductDetailScreenState extends ConsumerState { height: 120, child: Row( children: [ - ...p.images.map((img) => _ImageThumbnail( - url: AppConfig.baseUrl + img.url, - onDelete: () => _deleteImage(img), + ...p.images.asMap().entries.map((e) => _ImageThumbnail( + url: AppConfig.baseUrl + e.value.url, + onDelete: () => _deleteImage(e.value), + onView: () => showFullscreenImages( + context, + p.images + .map((img) => AppConfig.baseUrl + img.url) + .toList(), + initialIndex: e.key, + ), )), if (p.images.length < 5) WriteGuard( @@ -567,7 +575,9 @@ class _ProductDetailScreenState extends ConsumerState { class _ImageThumbnail extends StatelessWidget { final String url; final VoidCallback onDelete; - const _ImageThumbnail({required this.url, required this.onDelete}); + final VoidCallback onView; + const _ImageThumbnail( + {required this.url, required this.onDelete, required this.onView}); @override Widget build(BuildContext context) { @@ -575,19 +585,22 @@ class _ImageThumbnail extends StatelessWidget { padding: const EdgeInsets.only(right: 8), child: Stack( children: [ - ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Image.network( - url, - width: 110, - height: 110, - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => Container( + GestureDetector( + onDoubleTap: onView, + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Image.network( + url, width: 110, height: 110, - color: AppTheme.border, - child: const Icon(Icons.broken_image, - color: AppTheme.textSecondary), + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + width: 110, + height: 110, + color: AppTheme.border, + child: const Icon(Icons.broken_image, + color: AppTheme.textSecondary), + ), ), ), ), diff --git a/client/lib/screens/public/public_product_screen.dart b/client/lib/screens/public/public_product_screen.dart index 7154bd4..fa6e323 100644 --- a/client/lib/screens/public/public_product_screen.dart +++ b/client/lib/screens/public/public_product_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../core/config/app_config.dart'; +import '../../widgets/fullscreen_image_viewer.dart'; // ── Palette & typography constants ──────────────────────────────────── const _kPaper = Color(0xFFFAF8F5); @@ -476,12 +477,7 @@ class _HeroGalleryState extends State<_HeroGallery> { } void _openFullscreen(int index) { - Navigator.of(context).push(PageRouteBuilder( - opaque: false, - barrierColor: Colors.black87, - pageBuilder: (_, __, ___) => _FullscreenViewer( - urls: widget.imageUrls, initialIndex: index), - )); + showFullscreenImages(context, widget.imageUrls, initialIndex: index); } } @@ -534,66 +530,6 @@ class _GlassChip extends StatelessWidget { } } -// ── Full-screen image viewer ────────────────────────────────────────── - -class _FullscreenViewer extends StatefulWidget { - final List urls; - final int initialIndex; - const _FullscreenViewer({required this.urls, required this.initialIndex}); - - @override - State<_FullscreenViewer> createState() => _FullscreenViewerState(); -} - -class _FullscreenViewerState extends State<_FullscreenViewer> { - late PageController _ctrl; - - @override - void initState() { - super.initState(); - _ctrl = PageController(initialPage: widget.initialIndex); - } - - @override - void dispose() { _ctrl.dispose(); super.dispose(); } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () => Navigator.of(context).pop(), - child: Scaffold( - backgroundColor: Colors.transparent, - body: Stack( - children: [ - PageView.builder( - controller: _ctrl, - itemCount: widget.urls.length, - itemBuilder: (_, i) => Center( - child: InteractiveViewer( - child: Image.network(widget.urls[i], fit: BoxFit.contain, - errorBuilder: (_, __, ___) => - const Icon(Icons.broken_image, size: 64, color: Colors.white54)), - )), - ), - Positioned( - top: 40, right: 16, - child: GestureDetector( - onTap: () => Navigator.of(context).pop(), - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.black54, borderRadius: BorderRadius.circular(20)), - child: const Icon(Icons.close, color: Colors.white, size: 20), - ), - ), - ), - ], - ), - ), - ); - } -} - // ── Verified Ribbon ─────────────────────────────────────────────────── class _VerifiedRibbon extends StatelessWidget { diff --git a/client/lib/widgets/fullscreen_image_viewer.dart b/client/lib/widgets/fullscreen_image_viewer.dart new file mode 100644 index 0000000..da40a82 --- /dev/null +++ b/client/lib/widgets/fullscreen_image_viewer.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; + +/// 全屏图片查看器:黑底 modal + 多图左右翻页(PageView)+ 双指缩放/拖拽(InteractiveViewer)。 +/// 点击背景或右上角关闭按钮退出。纯 Flutter 内置组件,无需额外依赖。 +/// +/// 用法: +/// ```dart +/// showFullscreenImages(context, urls, initialIndex: 2); +/// ``` +void showFullscreenImages(BuildContext context, List urls, + {int initialIndex = 0}) { + if (urls.isEmpty) return; + Navigator.of(context).push(PageRouteBuilder( + opaque: false, + barrierColor: Colors.black87, + pageBuilder: (_, __, ___) => + FullscreenImageViewer(urls: urls, initialIndex: initialIndex), + )); +} + +class FullscreenImageViewer extends StatefulWidget { + final List urls; + final int initialIndex; + const FullscreenImageViewer( + {super.key, required this.urls, this.initialIndex = 0}); + + @override + State createState() => _FullscreenImageViewerState(); +} + +class _FullscreenImageViewerState extends State { + late PageController _ctrl; + + @override + void initState() { + super.initState(); + _ctrl = PageController(initialPage: widget.initialIndex); + } + + @override + void dispose() { + _ctrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Scaffold( + backgroundColor: Colors.transparent, + body: Stack( + children: [ + PageView.builder( + controller: _ctrl, + itemCount: widget.urls.length, + itemBuilder: (_, i) => Center( + child: InteractiveViewer( + child: Image.network(widget.urls[i], fit: BoxFit.contain, + errorBuilder: (_, __, ___) => const Icon( + Icons.broken_image, + size: 64, + color: Colors.white54)), + ), + ), + ), + Positioned( + top: 40, + right: 16, + child: GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(20)), + child: const Icon(Icons.close, color: Colors.white, size: 20), + ), + ), + ), + ], + ), + ), + ); + } +}