/// 时间: 2025-03-22 /// 功能: 诗词页面组件部分 /// 介绍: 包含诗词卡片、操作按钮、统计信息等组件的独立文件 /// 最新变化: 2026-04-02 支持深色模式 import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../../constants/app_constants.dart'; import '../../../services/get/theme_controller.dart'; import '../../../utils/http/poetry_api.dart'; import '../../../utils/audio_manager.dart'; import 'set/home_components.dart'; import 'components/skeleton_widgets.dart'; /// 诗词卡片组件 - 优化版本,防止拉伸和处理文本溢出 class PoetryCard extends StatefulWidget { final PoetryData poetryData; final List keywordList; final VoidCallback? onTap; final Map? sectionLoadingStates; final GlobalKey? repaintKey; final bool isDark; const PoetryCard({ super.key, required this.poetryData, required this.keywordList, this.onTap, this.sectionLoadingStates, this.repaintKey, this.isDark = false, }); @override State createState() => _PoetryCardState(); } class _PoetryCardState extends State { bool _showCopyTip = true; bool _showRecommendation = false; bool _globalTipsEnabled = true; @override void initState() { super.initState(); _loadGlobalTipsSettings(); } 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 '推荐:婉约词'; } } 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) { final isDark = widget.isDark; final themeController = Get.find(); final themeColor = themeController.currentThemeColor; final card = GestureDetector( onTap: () { AudioManager().playClickSound(); widget.onTap?.call(); }, child: Stack( children: [ Container( margin: const EdgeInsets.all(16), decoration: BoxDecoration( color: isDark ? const Color(0xFF1E1E1E) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withAlpha(isDark ? 40 : 10), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Padding( padding: const EdgeInsets.only( left: 20, right: 20, bottom: 20, top: 50, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ _buildTitleSection(isDark, themeColor), if (widget.poetryData.drtime.isNotEmpty) ...[ _buildContentSection(context, isDark, themeColor), const SizedBox(height: 3), ], Align( alignment: Alignment.centerRight, child: Container( margin: EdgeInsets.zero, padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 2, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( '精选诗句', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: themeColor, ), ), const SizedBox(width: 4), Icon(Icons.format_quote, color: themeColor, size: 14), ], ), ), ), _buildNameSection(isDark, themeColor), const SizedBox(height: 12), if (widget.keywordList.isNotEmpty) ...[ _buildKeywordSection(isDark, themeColor), const SizedBox(height: 16), ], if (widget.poetryData.introduce.isNotEmpty) ...[ _buildIntroductionSection(context, isDark, themeColor), ], ], ), ), ), _buildTimeBar(isDark, themeColor), _buildCopyTip(isDark, themeColor), ], ), ); if (widget.repaintKey != null) { return RepaintBoundary(key: widget.repaintKey, child: card); } return card; } Widget _buildTimeBar(bool isDark, Color themeColor) { return Positioned( top: 16, left: 16, right: 16, child: 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: [themeColor.withAlpha(204), themeColor], 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(bool isDark, Color themeColor) { if (!_globalTipsEnabled || !_showCopyTip) { return const SizedBox.shrink(); } return Positioned( top: 80, right: 24, child: GestureDetector( onTap: () => setState(() => _showCopyTip = false), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: themeColor, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: themeColor.withAlpha(76), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.info_outline, color: Colors.white, size: 16), SizedBox(width: 6), Text( '点击任意区域加载下一条,长按复制,下拉刷新', style: TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.w500, ), ), SizedBox(width: 4), Icon(Icons.close, color: Colors.white, size: 14), ], ), ), ), ); } Widget _buildTitleSection(bool isDark, Color themeColor) { final isLoading = widget.sectionLoadingStates?['title'] ?? false; final baseColor = isDark ? const Color(0xFF3A3A3A) : Colors.grey[300]; final highlightColor = isDark ? const Color(0xFF4A4A4A) : Colors.grey[100]; return SizedBox( height: 28, child: Row( children: [ Expanded( child: Builder( builder: (context) => GestureDetector( onLongPress: () => CopyUtils.showCopyDialog( context, widget.poetryData.url, '诗人和标题', isDark: isDark, ), child: isLoading ? SkeletonContainer( width: double.infinity, height: 20, borderRadius: 4, baseColor: baseColor, highlightColor: highlightColor, ) : Text( "出处: ${widget.poetryData.url}", style: TextStyle( fontSize: 16, fontWeight: FontWeight.normal, color: isDark ? Colors.grey[200] : Colors.black, fontStyle: FontStyle.italic, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), ), ), ], ), ); } Widget _buildNameSection(bool isDark, Color themeColor) { final isLoading = widget.sectionLoadingStates?['name'] ?? false; final baseColor = isDark ? const Color(0xFF3A3A3A) : Colors.grey[300]; final highlightColor = isDark ? const Color(0xFF4A4A4A) : Colors.grey[100]; return Container( width: double.infinity, padding: const EdgeInsets.all(16), margin: EdgeInsets.zero, decoration: BoxDecoration( gradient: LinearGradient( colors: isDark ? [const Color(0xFF2A2A2A), const Color(0xFF252525)] : [themeColor.withAlpha(26), themeColor.withAlpha(13)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(12), border: Border.all( color: isDark ? Colors.grey[700]! : themeColor.withAlpha(51), width: 1, ), boxShadow: [ BoxShadow( color: isDark ? Colors.black.withAlpha(40) : themeColor.withAlpha(26), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( onLongPress: () => CopyUtils.showCopyDialog( context, widget.poetryData.name, '诗词', isDark: isDark, ), child: isLoading ? Center( child: Column( children: [ SkeletonContainer( width: 120, height: 28, borderRadius: 4, baseColor: baseColor, highlightColor: highlightColor, ), const SizedBox(height: 8), SkeletonContainer( width: 80, height: 16, borderRadius: 4, baseColor: baseColor, highlightColor: highlightColor, ), ], ), ) : Text( _formatPoetryText(widget.poetryData.name), style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: isDark ? Colors.grey[100] : Colors.black87, height: 1.4, ), textAlign: TextAlign.center, ), ), ], ), ); } String _formatPoetryText(String text) { if (text.length < 10) { return text; } final commaIndex = text.indexOf(','); if (commaIndex != -1) { return text.replaceFirst(',', ',\n'); } return text; } Widget _buildKeywordSection(bool isDark, Color themeColor) { final isLoading = widget.sectionLoadingStates?['keywords'] ?? false; final baseColor = isDark ? const Color(0xFF3A3A3A) : Colors.grey[300]; final highlightColor = isDark ? const Color(0xFF4A4A4A) : Colors.grey[100]; return Column( children: [ Row( children: [ Expanded( child: isLoading ? Wrap( spacing: 8, runSpacing: 8, alignment: WrapAlignment.center, children: [ SkeletonContainer( width: 60, height: 24, borderRadius: 12, baseColor: baseColor, highlightColor: highlightColor, ), SkeletonContainer( width: 80, height: 24, borderRadius: 12, baseColor: baseColor, highlightColor: highlightColor, ), SkeletonContainer( width: 70, height: 24, borderRadius: 12, baseColor: baseColor, highlightColor: highlightColor, ), ], ) : Wrap( spacing: 8, runSpacing: 4, children: [ if (widget.keywordList.isNotEmpty) ...widget.keywordList.map( (keyword) => Builder( builder: (context) => GestureDetector( onLongPress: () => CopyUtils.showCopyDialog( context, keyword, '关键词', isDark: isDark, ), child: Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: isDark ? themeColor.withAlpha(40) : themeColor.withAlpha(26), borderRadius: BorderRadius.circular(8), ), child: Text( keyword, style: TextStyle( color: themeColor, fontSize: 12, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), ), ), ), if (widget.poetryData.alias.isNotEmpty) Builder( builder: (context) => GestureDetector( onLongPress: () => CopyUtils.showCopyDialog( context, widget.poetryData.alias, '朝代', isDark: isDark, ), child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 4, ), decoration: BoxDecoration( color: isDark ? themeColor.withAlpha(40) : themeColor.withAlpha(26), borderRadius: BorderRadius.circular(12), ), child: Text( widget.poetryData.alias, style: TextStyle( color: themeColor, fontSize: 12, fontWeight: FontWeight.w500, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), ), ), ], ), ), if (!isLoading) Row( mainAxisSize: MainAxisSize.min, children: [ Text( PoetryDataUtils.generateStars(widget.poetryData.star), style: const TextStyle(fontSize: 14), ), const SizedBox(width: 4), Text( PoetryDataUtils.generateLikeText(widget.poetryData.like), style: TextStyle( fontSize: 12, color: isDark ? Colors.grey[400] : Colors.grey, ), ), const SizedBox(width: 4), Text( PoetryDataUtils.generateViewText( widget.poetryData.hitsTotal, ), style: TextStyle( fontSize: 12, color: isDark ? Colors.grey[400] : Colors.grey, ), ), ], ), ], ), ], ); } Widget _buildContentSection( BuildContext context, bool isDark, Color themeColor, ) { final isLoading = widget.sectionLoadingStates?['content'] ?? false; final baseColor = isDark ? const Color(0xFF3A3A3A) : Colors.grey[300]; final highlightColor = isDark ? const Color(0xFF4A4A4A) : Colors.grey[100]; return Builder( builder: (context) => GestureDetector( onLongPress: () => CopyUtils.showCopyDialog( context, widget.poetryData.drtime, '时间', isDark: isDark, ), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), margin: const EdgeInsets.symmetric(vertical: 4), decoration: BoxDecoration( color: isDark ? Colors.grey[800]!.withAlpha(80) : themeColor.withAlpha(20), borderRadius: BorderRadius.circular(8), ), child: isLoading ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SkeletonContainer( width: 80, height: 18, borderRadius: 4, baseColor: baseColor, highlightColor: highlightColor, ), const SizedBox(height: 12), SkeletonContainer( width: double.infinity, height: 16, borderRadius: 4, baseColor: baseColor, highlightColor: highlightColor, ), const SizedBox(height: 8), SkeletonContainer( width: double.infinity, height: 16, borderRadius: 4, baseColor: baseColor, highlightColor: highlightColor, ), const SizedBox(height: 8), SkeletonContainer( width: 150, height: 16, borderRadius: 4, baseColor: baseColor, highlightColor: highlightColor, ), ], ) : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '原文|赏析', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: isDark ? Colors.grey[300] : themeColor, ), ), const SizedBox(height: 8), Text( widget.poetryData.drtime, style: TextStyle( fontSize: 14, color: isDark ? Colors.grey[400] : Colors.grey[600], ), textAlign: TextAlign.center, ), ], ), ), ), ); } Widget _buildIntroductionSection( BuildContext context, bool isDark, Color themeColor, ) { final isLoading = widget.sectionLoadingStates?['introduction'] ?? false; return Builder( builder: (context) => GestureDetector( onLongPress: () => CopyUtils.showCopyDialog( context, widget.poetryData.introduce, '简介', isDark: isDark, ), child: Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isDark ? const Color(0xFF252525) : Colors.grey[50], borderRadius: BorderRadius.circular(8), border: Border.all( color: isDark ? Colors.grey[700]! : Colors.grey[200]!, ), ), child: isLoading ? Center( child: Column( children: [ SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(themeColor), ), ), const SizedBox(height: 8), Text( '简介加载中...', style: TextStyle( fontSize: 14, color: isDark ? Colors.grey[400] : Colors.grey[600], ), ), ], ), ) : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.info_outline, size: 16, color: isDark ? Colors.grey[400] : Colors.grey[600], ), const SizedBox(width: 4), Text( '简介', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: isDark ? Colors.grey[300] : Colors.grey[700], ), ), ], ), const SizedBox(height: 8), Text( widget.poetryData.introduce, style: TextStyle( fontSize: 14, height: 1.5, color: isDark ? Colors.grey[400] : Colors.grey[600], ), ), ], ), ), ), ); } }