/// 时间: 2025-03-22 /// 功能: 诗词页面通用组件和工具函数 /// 介绍: 从 home_page.dart 和 home_part.dart 中提取的公共组件,用于代码复用和简化 /// 最新变化: 2026-04-02 支持深色模式 import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; import '../../../constants/app_constants.dart'; import '../../../utils/http/poetry_api.dart'; import 'home-load.dart'; /// 加载状态组件 class LoadingWidget extends StatelessWidget { final bool isDark; const LoadingWidget({super.key, this.isDark = false}); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator( valueColor: AlwaysStoppedAnimation( AppConstants.primaryColor, ), ), const SizedBox(height: 16), Text( '加载中...', style: TextStyle( fontSize: 16, color: isDark ? Colors.grey[400] : const Color(0xFF757575), ), ), ], ), ); } } /// 截图和分享工具类 class ShareImageUtils { static Future captureAndShare( BuildContext context, GlobalKey repaintKey, { String? subject, }) async { try { Get.snackbar('提示', '正在生成图片...'); final boundary = repaintKey.currentContext?.findRenderObject() as RenderRepaintBoundary?; if (boundary == null) { Get.snackbar('错误', '生成图片失败'); return; } final image = await boundary.toImage(pixelRatio: 3.0); final byteData = await image.toByteData(format: ui.ImageByteFormat.png); if (byteData == null) { Get.snackbar('错误', '生成图片失败'); return; } final pngBytes = byteData.buffer.asUint8List(); final directory = await getTemporaryDirectory(); final file = File( '${directory.path}/poetry_${DateTime.now().millisecondsSinceEpoch}.png', ); await file.writeAsBytes(pngBytes); await Share.shareXFiles([XFile(file.path)], subject: subject ?? '诗词分享'); Get.snackbar( '成功', '分享成功!', // snackPosition: SnackPosition.TOP, // backgroundColor: Colors.transparent, // colorText: Colors.white, // duration: const Duration(seconds: 2), // margin: const EdgeInsets.all(16), // borderRadius: 20, // animationDuration: const Duration(milliseconds: 300), // maxWidth: Get.width - 32, // overlayBlur: 0.1, // overlayColor: Colors.transparent, // isDismissible: true, // padding: EdgeInsets.zero, // shouldIconPulse: false, // barBlur: 20, ); } catch (e) { Get.snackbar( '错误', '分享失败:${e.toString().substring(0, e.toString().length > 50 ? 50 : e.toString().length)}', ); } } } /// 错误状态组件 class CustomErrorWidget extends StatelessWidget { final String errorMessage; final VoidCallback onRetry; final bool isDark; const CustomErrorWidget({ super.key, required this.errorMessage, required this.onRetry, this.isDark = false, }); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.error_outline, size: 64, color: isDark ? Colors.grey[600] : Colors.grey[400], ), const SizedBox(height: 16), Text( errorMessage, style: TextStyle( fontSize: 16, color: isDark ? Colors.grey[400] : Colors.grey, ), textAlign: TextAlign.center, ), const SizedBox(height: 16), ElevatedButton( onPressed: onRetry, style: ElevatedButton.styleFrom( backgroundColor: AppConstants.primaryColor, ), child: const Text('重试'), ), ], ), ); } } /// 空状态组件 class EmptyWidget extends StatelessWidget { final bool isDark; const EmptyWidget({super.key, this.isDark = false}); @override Widget build(BuildContext context) { return Center( child: Text( '暂无诗词内容', style: TextStyle( fontSize: 16, color: isDark ? Colors.grey[400] : Colors.grey, ), ), ); } } /// 诗词状态管理工具类 class PoetryStateManager { static void setLoadingState(VoidCallback setState, bool loading) { setState(); } static void setErrorState(VoidCallback setState, String error) { setState(); } static void setSuccessState(VoidCallback setState, PoetryData data) { setState(); } static String formatErrorMessage(String error) { return error.toString().contains('HttpException') ? error.toString().replaceAll('HttpException: ', '') : '获取诗词失败'; } static void showSnackBar( BuildContext context, String message, { Color? backgroundColor, Duration? duration, }) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: backgroundColor ?? AppConstants.successColor, duration: duration ?? const Duration(seconds: 2), ), ); } static void triggerHapticFeedback() { HapticFeedback.lightImpact(); } static void triggerHapticFeedbackMedium() { HapticFeedback.mediumImpact(); } } /// 诗词数据工具类 class PoetryDataUtils { static List extractKeywords(PoetryData? poetryData) { return poetryData?.keywordList ?? []; } static String getStarDisplay(PoetryData? poetryData) { return poetryData?.starDisplay ?? ''; } static String generateStars(int? starCount) { if (starCount == null) return ''; final count = starCount > 5 ? 5 : starCount; if (count == 5) { return ' 🌟$count'; } else { return ' ⭐$count'; } } static String generateLikeText(int? likeCount) { if (likeCount == null) return ''; return ' ❤️ $likeCount'; } static String generateViewText(int? viewCount) { if (viewCount == null) return ''; return ' 🔥 $viewCount'; } static bool isValidPoetryData(PoetryData? poetryData) { return poetryData != null && poetryData.name.isNotEmpty; } } /// 动画工具类 class AnimationUtils { static AnimationController createFadeController(TickerProvider vsync) { return AnimationController( duration: AppConstants.animationDurationMedium, vsync: vsync, ); } static AnimationController createSlideController(TickerProvider vsync) { return AnimationController( duration: AppConstants.animationDurationShort, vsync: vsync, ); } static Animation createFadeAnimation(AnimationController controller) { return CurvedAnimation(parent: controller, curve: Curves.easeIn); } static Animation createSlideAnimation( AnimationController controller, ) { return Tween( begin: const Offset(0.0, 0.3), end: Offset.zero, ).animate(CurvedAnimation(parent: controller, curve: Curves.easeOutCubic)); } } /// 复制工具类 class CopyUtils { static void copyToClipboard( BuildContext context, String content, String contentType, { VoidCallback? onSuccess, bool isDark = false, }) { try { Clipboard.setData(ClipboardData(text: content)); Get.snackbar('提示', '已复制$contentType'); DebugInfoManager().showCopySuccess(); onSuccess?.call(); } catch (e) { Get.snackbar('错误', '复制失败'); DebugInfoManager().showCopyFailed(); } } static void showCopyDialog( BuildContext context, String content, String contentType, { bool isDark = false, }) { showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: isDark ? const Color(0xFF1E1E1E) : Colors.white, title: Text( '复制$contentType', style: TextStyle(color: isDark ? Colors.white : Colors.black87), ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '受隐私权限约束,频繁写入剪切板需告知用户', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: isDark ? Colors.grey[300] : Colors.black87, ), ), const SizedBox(height: 12), Text( '预览内容:', style: TextStyle( fontSize: 14, color: isDark ? Colors.grey[400] : Colors.grey[600], ), ), const SizedBox(height: 4), Container( width: double.infinity, padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: isDark ? const Color(0xFF2A2A2A) : Colors.grey[100], borderRadius: BorderRadius.circular(4), border: Border.all( color: isDark ? Colors.grey[700]! : Colors.grey[300]!, ), ), child: Text( content.length > 50 ? '${content.substring(0, 50)}...' : content, style: TextStyle( fontSize: 12, color: isDark ? Colors.grey[300] : Colors.black87, ), ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text( '返回', style: TextStyle( color: isDark ? Colors.grey[400] : Colors.grey[600], ), ), ), ElevatedButton( onPressed: () { Navigator.of(context).pop(); copyToClipboard(context, content, contentType, isDark: isDark); }, style: ElevatedButton.styleFrom( backgroundColor: AppConstants.primaryColor, foregroundColor: Colors.white, ), child: Text('复制$contentType'), ), ], ), ); } } /// 悬浮分享按钮组件 class FloatingShareButton extends StatelessWidget { final VoidCallback onShare; final bool isDark; const FloatingShareButton({ super.key, required this.onShare, this.isDark = false, }); @override Widget build(BuildContext context) { return Container( width: 56, height: 56, decoration: BoxDecoration( color: AppConstants.secondaryColor, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: AppConstants.secondaryColor.withAlpha(76), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(28), onTap: () { HapticFeedback.lightImpact(); onShare(); }, child: const Center( child: Icon(Icons.share, color: Colors.white, size: 28), ), ), ), ); } } /// 悬浮上一条按钮 class FloatingPreviousButton extends StatelessWidget { final VoidCallback onPrevious; final bool isDark; const FloatingPreviousButton({ super.key, required this.onPrevious, this.isDark = false, }); @override Widget build(BuildContext context) { return Container( width: 56, height: 56, decoration: BoxDecoration( color: isDark ? const Color(0xFF2A2A2A) : Colors.white, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withAlpha(isDark ? 40 : 20), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(28), onTap: () { HapticFeedback.lightImpact(); onPrevious(); }, child: Center( child: Icon( Icons.arrow_back, color: isDark ? Colors.grey[300] : AppConstants.primaryColor, size: 28, ), ), ), ), ); } } /// 悬浮下一条按钮 class FloatingNextButton extends StatelessWidget { final VoidCallback onNext; final bool isDark; const FloatingNextButton({ super.key, required this.onNext, this.isDark = false, }); @override Widget build(BuildContext context) { return Container( width: 56, height: 56, decoration: BoxDecoration( color: isDark ? const Color(0xFF2A2A2A) : Colors.white, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withAlpha(isDark ? 40 : 20), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(28), onTap: () { HapticFeedback.lightImpact(); onNext(); }, child: Center( child: Icon( Icons.arrow_forward, color: isDark ? Colors.grey[300] : AppConstants.primaryColor, size: 28, ), ), ), ), ); } } /// 悬浮点赞按钮 class FloatingLikeButton extends StatelessWidget { final bool isLiked; final bool isLoadingLike; final VoidCallback onToggleLike; final bool isDark; const FloatingLikeButton({ super.key, required this.isLiked, required this.isLoadingLike, required this.onToggleLike, this.isDark = false, }); @override Widget build(BuildContext context) { return Container( width: 56, height: 56, decoration: BoxDecoration( color: isLiked ? Colors.red : (isDark ? const Color(0xFF2A2A2A) : Colors.white), shape: BoxShape.circle, boxShadow: [ BoxShadow( color: (isLiked ? Colors.red : Colors.black).withAlpha( isDark ? 40 : 20, ), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(28), onTap: isLoadingLike ? null : () { HapticFeedback.lightImpact(); onToggleLike(); }, child: Center( child: isLoadingLike ? SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( isDark ? Colors.grey[300]! : AppConstants.primaryColor, ), ), ) : Icon( isLiked ? Icons.favorite : Icons.favorite_border, color: isLiked ? Colors.white : (isDark ? Colors.grey[300] : Colors.grey), size: 28, ), ), ), ), ); } } /// 统计信息卡片组件 class StatsCard extends StatelessWidget { final PoetryData poetryData; final bool isDark; const StatsCard({super.key, required this.poetryData, this.isDark = false}); @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isDark ? const Color(0xFF1E1E1E) : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withAlpha(isDark ? 40 : 5), blurRadius: 5, offset: const Offset(0, 1), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( '统计信息', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: AppConstants.primaryColor, ), ), const SizedBox(height: 12), Row( children: [ _buildStatItem('今日', poetryData.hitsDay.toString()), const SizedBox(width: 20), _buildStatItem('本月', poetryData.hitsMonth.toString()), const SizedBox(width: 20), _buildStatItem('总计', poetryData.hitsTotal.toString()), ], ), ], ), ); } Widget _buildStatItem(String label, String value) { return Expanded( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( value, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: isDark ? Colors.grey[200] : Colors.black87, ), ), const SizedBox(height: 4), Text( label, style: TextStyle( fontSize: 12, color: isDark ? Colors.grey[400] : Colors.grey, ), ), ], ), ); } }