/// 时间: 2025-03-22 /// 功能: 诗词页面组件部分 /// 介绍: 包含诗词卡片、操作按钮、统计信息等组件的独立文件 /// 最新变化: 重构代码结构,使用工具类简化代码 import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../../constants/app_constants.dart'; import '../../../utils/http/poetry_api.dart'; import '../../../utils/flutter_compatibility_fix.dart'; import '../../../utils/audio_manager.dart'; import 'home_components.dart'; /// 诗词卡片组件 - 优化版本,防止拉伸和处理文本溢出 class PoetryCard extends StatefulWidget { final PoetryData poetryData; final List keywordList; final VoidCallback? onTap; final Map? sectionLoadingStates; const PoetryCard({ super.key, required this.poetryData, required this.keywordList, this.onTap, this.sectionLoadingStates, }); @override State createState() => _PoetryCardState(); } class _PoetryCardState extends State { bool _showCopyTip = true; bool _showRecommendation = false; bool _globalTipsEnabled = true; // 添加全局Tips开关状态 @override void initState() { super.initState(); _loadGlobalTipsSettings(); // 加载全局Tips设置 } String _getTimeOfDayGreeting() { final hour = DateTime.now().hour; if (hour >= 5 && hour < 7) { return '清晨了,呼吸新鲜空气'; } else if (hour >= 7 && hour < 9) { return '早上好,元气满满'; } else if (hour >= 9 && hour < 12) { return '上午好,努力工作'; } else if (hour >= 12 && hour < 14) { return '中午了,吃顿好的'; } else if (hour >= 14 && hour < 17) { return '下午了,喝杯咖啡'; } else if (hour >= 17 && hour < 19) { return '傍晚了,休息一下'; } else if (hour >= 19 && hour < 22) { return '晚上好,放松身心'; } else { return '深夜了,注意身体'; } } String _getRecommendation() { final hour = DateTime.now().hour; if (hour >= 5 && hour < 7) { return '推荐:山水田园诗'; } else if (hour >= 7 && hour < 9) { return '推荐:励志诗词'; } else if (hour >= 9 && hour < 12) { return '推荐:送别诗'; } else if (hour >= 12 && hour < 14) { return '推荐:思乡诗'; } else if (hour >= 14 && hour < 17) { return '推荐:咏史诗'; } else if (hour >= 17 && hour < 19) { return '推荐:边塞诗'; } else if (hour >= 19 && hour < 22) { return '推荐:爱情诗'; } else { return '推荐:婉约词'; } } // 加载全局Tips设置 Future _loadGlobalTipsSettings() async { try { final prefs = await SharedPreferences.getInstance(); if (mounted) { setState(() { _globalTipsEnabled = prefs.getBool('global_tips_enabled') ?? true; }); } } catch (e) { // 如果加载失败,默认开启 if (mounted) { setState(() { _globalTipsEnabled = true; }); } } } @override Widget build(BuildContext context) { return GestureDetector( onTap: () async { // 播放点击音效 await AudioManager().playClickSound(); // 调用原始的onTap回调 widget.onTap?.call(); }, child: Stack( children: [ Container( margin: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withAlpha(10), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ _buildTimeBar(), Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ _buildTitleSection(), if (widget.poetryData.drtime.isNotEmpty) ...[ _buildContentSection(context), const SizedBox(height: 16), ], _buildNameSection(), const SizedBox(height: 12), if (widget.keywordList.isNotEmpty) ...[ _buildKeywordSection(), const SizedBox(height: 16), ], if (widget.poetryData.introduce.isNotEmpty) ...[ _buildIntroductionSection(context), ], ], ), ), ], ), ), if (_showCopyTip) _buildCopyTip(), ], ), ); } Widget _buildTimeBar() { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { setState(() { _showRecommendation = !_showRecommendation; }); }, child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppConstants.primaryColor.withAlpha(204), AppConstants.primaryColor, ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), ), child: Row( children: [ Icon( _showRecommendation ? Icons.lightbulb : Icons.wb_sunny, color: Colors.white, size: 20, ), const SizedBox(width: 8), Expanded( child: Text( _showRecommendation ? _getRecommendation() : _getTimeOfDayGreeting(), style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), Icon( _showRecommendation ? Icons.expand_less : Icons.expand_more, color: Colors.white70, size: 20, ), ], ), ), ); } Widget _buildCopyTip() { return Positioned( top: 56, right: 24, child: GestureDetector( onTap: () => setState(() => _showCopyTip = false), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: AppConstants.primaryColor, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: AppConstants.primaryColor.withAlpha(76), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.info_outline, color: Colors.white, size: 16), const SizedBox(width: 6), Text( _globalTipsEnabled ? '点击任意区域加载下一条,长按复制,下拉刷新' : '', style: TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.w500, ), ), const SizedBox(width: 4), const Icon(Icons.close, color: Colors.white, size: 14), ], ), ), ), ); } Widget _buildTitleSection() { final isLoading = widget.sectionLoadingStates?['title'] ?? false; return SizedBox( height: 28, child: Row( children: [ Expanded( child: Builder( builder: (context) => GestureDetector( onLongPress: () => CopyUtils.showCopyDialog( context, widget.poetryData.url, '诗人和标题', ), child: isLoading ? Row( children: [ SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( AppConstants.primaryColor, ), ), ), const SizedBox(width: 8), Text( '出处加载中...', style: TextStyle( fontSize: 14, color: Colors.grey[600], fontStyle: FontStyle.italic, ), ), ], ) : Text( "出处: ${widget.poetryData.url}", style: const TextStyle( fontSize: 16, fontWeight: FontWeight.normal, color: Colors.black, fontStyle: FontStyle.italic, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), ), ), ], ), ); } Widget _buildNameSection() { final isLoading = widget.sectionLoadingStates?['name'] ?? false; return Container( width: double.infinity, padding: const EdgeInsets.all(16), margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppConstants.primaryColor.withAlpha(26), AppConstants.primaryColor.withAlpha(13), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(12), border: Border.all( color: AppConstants.primaryColor.withAlpha(51), width: 1, ), boxShadow: [ BoxShadow( color: AppConstants.primaryColor.withAlpha(26), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.format_quote, color: AppConstants.primaryColor, size: 20, ), const SizedBox(width: 8), Expanded( child: Text( '精选诗句', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: AppConstants.primaryColor, ), ), ), ], ), const SizedBox(height: 8), GestureDetector( onLongPress: () => CopyUtils.showCopyDialog(context, widget.poetryData.name, '诗词'), child: isLoading ? Center( child: Column( children: [ SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( AppConstants.primaryColor, ), ), ), const SizedBox(height: 8), Text( '诗句加载中...', style: TextStyle( fontSize: 14, color: Colors.grey[600], ), ), ], ), ) : Text( widget.poetryData.name, style: const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.4, ), textAlign: TextAlign.center, ), ), ], ), ); } Widget _buildKeywordSection() { final isLoading = widget.sectionLoadingStates?['keywords'] ?? false; return Column( children: [ // 第一行:关键词和朝代(左边)vs 星星和点赞(右边) Row( children: [ // 左边:关键词和朝代 Expanded( child: isLoading ? Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( AppConstants.primaryColor, ), ), ), const SizedBox(width: 8), Text( '关键词加载中...', style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), ], ), ) : Wrap( spacing: 8, runSpacing: 4, children: [ if (widget.keywordList.isNotEmpty) ...widget.keywordList .map( (keyword) => Builder( builder: (context) => GestureDetector( onLongPress: () => CopyUtils.showCopyDialog( context, keyword, '关键词', ), child: Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: AppConstants.secondaryColor .withAlpha(26), borderRadius: BorderRadius.circular(8), ), child: Text( keyword, style: TextStyle( color: AppConstants.secondaryColor, fontSize: 12, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), ), ), ) .toList(), if (widget.poetryData.alias.isNotEmpty) Builder( builder: (context) => GestureDetector( onLongPress: () => CopyUtils.showCopyDialog( context, widget.poetryData.alias, '朝代', ), child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 4, ), decoration: BoxDecoration( color: AppConstants.primaryColor.withAlpha( 26, ), borderRadius: BorderRadius.circular(12), ), child: Text( widget.poetryData.alias, style: TextStyle( color: AppConstants.primaryColor, fontSize: 12, fontWeight: FontWeight.w500, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), ), ), ], ), ), // 右边:星星和点赞 if (!isLoading && (widget.poetryData.star != null || widget.poetryData.like != null || widget.poetryData.hitsTotal != null)) Row( mainAxisSize: MainAxisSize.min, children: [ if (widget.poetryData.star != null) ...[ Text( PoetryDataUtils.generateStars(widget.poetryData.star), style: const TextStyle(fontSize: 14), ), const SizedBox(width: 4), ], if (widget.poetryData.like != null) ...[ Text( PoetryDataUtils.generateLikeText(widget.poetryData.like), style: const TextStyle(fontSize: 12, color: Colors.grey), ), ], if (widget.poetryData.hitsTotal != null) ...[ Text( PoetryDataUtils.generateViewText( widget.poetryData.hitsTotal, ), style: const TextStyle(fontSize: 12, color: Colors.grey), ), ], ], ), ], ), ], ); } Widget _buildContentSection(BuildContext context) { final isLoading = widget.sectionLoadingStates?['content'] ?? false; return GestureDetector( onLongPress: () => CopyUtils.showCopyDialog(context, widget.poetryData.drtime, '原文'), child: Container( width: double.infinity, constraints: const BoxConstraints(maxHeight: 200), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFF8F8F8), borderRadius: BorderRadius.circular(12), ), child: isLoading ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( AppConstants.primaryColor, ), ), ), const SizedBox(height: 8), Text( '原文加载中...', style: TextStyle(fontSize: 14, color: Colors.grey[600]), ), ], ), ) : SingleChildScrollView( child: Text( widget.poetryData.drtime, style: const TextStyle( fontSize: 16, height: 1.6, color: Colors.black87, ), ), ), ), ); } Widget _buildIntroductionSection(BuildContext context) { final isLoading = widget.sectionLoadingStates?['introduction'] ?? false; return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( '译文', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: AppConstants.primaryColor, ), ), const SizedBox(height: 8), GestureDetector( onLongPress: () => CopyUtils.showCopyDialog( context, widget.poetryData.introduce, '译文', ), child: Container( width: double.infinity, constraints: const BoxConstraints(maxHeight: 150), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppConstants.infoColor.withAlpha(13), borderRadius: BorderRadius.circular(12), border: Border.all(color: AppConstants.infoColor.withAlpha(51)), ), child: isLoading ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( AppConstants.primaryColor, ), ), ), const SizedBox(height: 8), Text( '译文加载中...', style: TextStyle( fontSize: 14, color: Colors.grey[600], ), ), ], ), ) : SingleChildScrollView( child: Text( widget.poetryData.introduce, style: const TextStyle( fontSize: 14, height: 1.6, color: Colors.black87, ), ), ), ), ), ], ); } } /// 悬浮上一条按钮组件 class FloatingPreviousButton extends StatelessWidget { final VoidCallback onPrevious; const FloatingPreviousButton({super.key, required this.onPrevious}); @override Widget build(BuildContext context) { return Tooltip( message: 'beta功能', child: 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(); onPrevious(); }, child: const Center( child: Icon(Icons.arrow_back, color: Colors.white, size: 28), ), ), ), ), ); } } /// 悬浮下一条按钮组件 class FloatingNextButton extends StatelessWidget { final VoidCallback onNext; const FloatingNextButton({super.key, required this.onNext}); @override Widget build(BuildContext context) { return Container( width: 56, height: 56, decoration: BoxDecoration( color: AppConstants.primaryColor, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: AppConstants.primaryColor.withAlpha(76), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(28), onTap: () { HapticFeedback.lightImpact(); onNext(); }, child: const Center( child: Icon(Icons.arrow_forward, color: Colors.white, size: 28), ), ), ), ); } } /// 悬浮点赞按钮组件 class FloatingLikeButton extends StatelessWidget { final bool isLiked; final bool isLoadingLike; final VoidCallback onToggleLike; const FloatingLikeButton({ super.key, required this.isLiked, required this.isLoadingLike, required this.onToggleLike, }); @override Widget build(BuildContext context) { return Container( width: 56, height: 56, decoration: BoxDecoration( color: isLiked ? AppConstants.errorColor : AppConstants.primaryColor, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: (isLiked ? AppConstants.errorColor : AppConstants.primaryColor) .withAlpha(76), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(28), onTap: isLoadingLike ? null : () { HapticFeedback.mediumImpact(); onToggleLike(); }, child: Center( child: isLoadingLike ? const SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white70), ), ) : Icon( isLiked ? Icons.favorite : Icons.favorite_border, color: Colors.white, size: 28, ), ), ), ), ); } } /// 统计信息卡片组件 class StatsCard extends StatelessWidget { final PoetryData poetryData; const StatsCard({super.key, required this.poetryData}); @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withAlpha(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: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87, ), ), const SizedBox(height: 4), Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)), ], ), ); } }