/// 时间: 2026-04-09 /// 功能: 情景推荐页面 /// 介绍: 展示今日诗词SDK的详细信息,包括诗词内容、环境信息等 /// 最新变化: 2026-04-09 添加骨架屏、分享功能 import 'dart:convert'; 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 '../../../services/get/theme_controller.dart'; import '../../../services/jinrishici_service.dart'; import '../../../services/network_listener_service.dart'; import '../../../models/colors/app_colors.dart'; import '../../../controllers/history_controller.dart'; import './skeleton_widgets.dart'; class PrePage extends StatefulWidget { const PrePage({super.key}); @override State createState() => _PrePageState(); } class _PrePageState extends State with SingleTickerProviderStateMixin { final ThemeController _themeController = Get.find(); final JinrishiciService _jinrishiciService = JinrishiciService(); final GlobalKey _repaintKey = GlobalKey(); late AnimationController _fadeController; late Animation _fadeAnimation; Map? _poetryData; Map? _userInfo; bool _isLoading = false; String? _errorMessage; bool _isMetadataExpanded = false; bool _isRefreshLocked = false; @override void initState() { super.initState(); _fadeController = AnimationController( duration: const Duration(milliseconds: 500), vsync: this, ); _fadeAnimation = CurvedAnimation( parent: _fadeController, curve: Curves.easeIn, ); _fadeController.forward(); _loadData(); } Future _loadData() async { if (_isRefreshLocked) return; setState(() { _isLoading = true; _errorMessage = null; _isRefreshLocked = true; }); try { final poetry = await _jinrishiciService.getTodayPoetry(); setState(() { _poetryData = poetry; _isLoading = false; }); try { final userInfo = await _jinrishiciService.getUserInfo(); setState(() { _userInfo = userInfo; }); } catch (e) { print('加载用户信息失败: $e'); } } catch (e) { setState(() { _errorMessage = e.toString(); _isLoading = false; }); } Future.delayed(const Duration(seconds: 2), () { if (mounted) { setState(() { _isRefreshLocked = false; }); } }); } Future _captureAndShare() async { try { Get.snackbar('提示', '正在生成图片...'); await Future.delayed(const Duration(milliseconds: 100)); if (!mounted) return; final boundary = _repaintKey.currentContext?.findRenderObject() as RenderRepaintBoundary?; if (boundary == null) { Get.snackbar('错误', '生成图片失败'); return; } if (boundary.debugNeedsPaint) { await Future.delayed(const Duration(milliseconds: 100)); } 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); final result = await Share.shareXFiles( [XFile(file.path)], subject: '情景诗词分享', text: '来自情景诗词App的分享', ); if (result.status == ShareResultStatus.success) { Get.snackbar('成功', '分享成功'); } else if (result.status == ShareResultStatus.dismissed) { Get.snackbar('提示', '分享已取消'); } if (await file.exists()) { await file.delete(); } } catch (e) { print('分享失败: $e'); Get.snackbar('错误', '分享失败: $e'); } } Future _createNoteFromPoetry() async { if (_poetryData == null) { Get.snackbar('提示', '暂无诗词数据'); return; } try { final data = _poetryData!['data'] as Map?; final origin = data?['origin'] as Map?; final title = origin?['title']?.toString() ?? '情景诗词笔记'; final category = '情景诗词'; final contentBuffer = StringBuffer(); if (origin != null) { if (origin['title'] != null) { contentBuffer.writeln('标题:${origin['title']}'); } if (origin['dynasty'] != null) { contentBuffer.writeln('朝代:${origin['dynasty']}'); } if (origin['author'] != null) { contentBuffer.writeln('作者:${origin['author']}'); } if (data?['content'] != null) { contentBuffer.writeln('诗句:${data!['content']}'); } if (origin['content'] != null) { final fullContent = (origin['content'] as List) .map((e) => e.toString()) .join('\n'); contentBuffer.writeln('全文:\n$fullContent'); } if (origin['translate'] != null) { final translate = (origin['translate'] as List) .map((e) => e.toString()) .join('\n'); contentBuffer.writeln('译文:\n$translate'); } } final noteId = await HistoryController.saveNote( title: title, content: contentBuffer.toString().trim(), category: category, ); if (noteId != null) { NetworkListenerService().sendSuccessEvent( NetworkEventType.noteUpdate, data: noteId, ); Get.snackbar( '成功', '已创建笔记', snackPosition: SnackPosition.BOTTOM, colorText: _themeController.currentThemeColor, ); } } catch (e) { Get.snackbar( '错误', '创建笔记失败: $e', snackPosition: SnackPosition.BOTTOM, colorText: _themeController.currentThemeColor, ); } } @override void dispose() { _fadeController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Obx(() { final isDark = _themeController.isDarkMode; final primaryColor = _themeController.currentThemeColor; return Scaffold( backgroundColor: isDark ? const Color(0xFF1A1A1A) : Colors.grey[50], appBar: AppBar( title: Text( '情景推荐', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 17, color: primaryColor, ), ), backgroundColor: isDark ? const Color(0xFF2A2A2A) : Colors.white, foregroundColor: primaryColor, elevation: 0, centerTitle: true, actions: [ GestureDetector( onTap: _isRefreshLocked ? null : _loadData, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( children: [ if (_isLoading) SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( isDark ? Colors.grey[500]! : Colors.grey[400]!, ), ), ) else Icon( Icons.refresh, color: _isRefreshLocked ? (isDark ? Colors.grey[500] : Colors.grey[400]) : primaryColor, size: 20, ), const SizedBox(width: 4), Text( '刷新', style: TextStyle( fontSize: 14, color: _isRefreshLocked ? (isDark ? Colors.grey[500] : Colors.grey[400]) : primaryColor, ), ), ], ), ), ), ], ), body: FadeTransition( opacity: _fadeAnimation, child: _buildBody(isDark, primaryColor), ), ); }); } Widget _buildBody(bool isDark, Color primaryColor) { if (_isLoading && _poetryData == null) { return _buildSkeletonBody(isDark, primaryColor); } if (_errorMessage != null && _poetryData == null) { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 64, color: Colors.red[400]), const SizedBox(height: 16), Text( '加载失败', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black87, ), ), const SizedBox(height: 8), Text( _errorMessage!, style: TextStyle( fontSize: 14, color: isDark ? Colors.grey[400] : Colors.grey[600], ), textAlign: TextAlign.center, ), const SizedBox(height: 24), ElevatedButton.icon( onPressed: _loadData, icon: const Icon(Icons.refresh), label: const Text('重试'), style: ElevatedButton.styleFrom( backgroundColor: primaryColor, foregroundColor: Colors.white, ), ), ], ), ), ); } return ListView( padding: const EdgeInsets.all(16), children: [ RepaintBoundary( key: _repaintKey, child: _buildPoetryCard(isDark, primaryColor), ), if (_userInfo != null) ...[ const SizedBox(height: 16), _buildUserInfoCard(isDark, primaryColor), ], const SizedBox(height: 100), ], ); } Widget _buildSkeletonBody(bool isDark, Color primaryColor) { final baseColor = isDark ? const Color(0xFF3A3A3A) : Colors.grey[300]; final highlightColor = isDark ? const Color(0xFF4A4A4A) : Colors.grey[100]; return ListView( padding: const EdgeInsets.all(16), children: [ Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withAlpha(isDark ? 40 : 10), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ SkeletonContainer( width: 40, height: 40, borderRadius: 12, baseColor: baseColor, highlightColor: highlightColor, ), const SizedBox(width: 12), SkeletonContainer( width: 100, height: 20, borderRadius: 4, baseColor: baseColor, highlightColor: highlightColor, ), ], ), const SizedBox(height: 16), SkeletonContainer( width: 180, height: 24, borderRadius: 4, baseColor: baseColor, highlightColor: highlightColor, ), const SizedBox(height: 12), SkeletonContainer( width: 120, height: 16, borderRadius: 4, baseColor: baseColor, highlightColor: highlightColor, ), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isDark ? const Color(0xFF1A1A1A) : Colors.grey[100], borderRadius: BorderRadius.circular(12), ), child: Column( children: [ SkeletonContainer( width: double.infinity, height: 20, borderRadius: 4, baseColor: baseColor, highlightColor: highlightColor, ), const SizedBox(height: 8), SkeletonContainer( width: double.infinity, height: 20, borderRadius: 4, baseColor: baseColor, highlightColor: highlightColor, ), const SizedBox(height: 8), SkeletonContainer( width: 150, height: 20, borderRadius: 4, baseColor: baseColor, highlightColor: highlightColor, ), ], ), ), const SizedBox(height: 16), Wrap( spacing: 8, runSpacing: 8, 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, ), ], ), ], ), ), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withAlpha(isDark ? 40 : 10), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ SkeletonContainer( width: 40, height: 40, borderRadius: 12, baseColor: baseColor, highlightColor: highlightColor, ), const SizedBox(width: 12), SkeletonContainer( width: 80, height: 20, borderRadius: 4, baseColor: baseColor, highlightColor: highlightColor, ), ], ), const SizedBox(height: 16), 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, ), ], ), ), ], ); } Widget _buildPoetryCard(bool isDark, Color primaryColor) { if (_poetryData == null) { return _buildSectionCard( '情景诗词', Icons.book, [ Center( child: Padding( padding: const EdgeInsets.all(20), child: Text( '暂无数据', style: TextStyle( color: isDark ? Colors.grey[400] : Colors.grey[600], ), ), ), ), ], isDark, primaryColor, ); } final data = _poetryData!['data'] as Map?; final origin = data?['origin'] as Map?; return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: LinearGradient( colors: [primaryColor.withAlpha(30), primaryColor.withAlpha(10)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), border: Border.all(color: primaryColor.withAlpha(50), width: 1), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: primaryColor.withAlpha(20), borderRadius: BorderRadius.circular(12), ), child: const Center( child: Text('📜', style: TextStyle(fontSize: 20)), ), ), const SizedBox(width: 12), Text( '情景诗词', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black87, ), ), ], ), Row( children: [ IconButton( onPressed: _captureAndShare, icon: Icon(Icons.share, color: primaryColor, size: 22), tooltip: '分享', ), IconButton( onPressed: _createNoteFromPoetry, icon: Icon(Icons.note_add, color: primaryColor, size: 22), tooltip: '写入笔记', ), ], ), ], ), const SizedBox(height: 16), if (origin != null) ...[ Text( origin['title'] ?? '未知标题', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black87, ), ), const SizedBox(height: 8), Row( children: [ Text( origin['dynasty'] ?? '未知朝代', style: TextStyle( fontSize: 14, color: primaryColor, fontWeight: FontWeight.w500, ), ), const SizedBox(width: 8), Text( '·', style: TextStyle( fontSize: 14, color: isDark ? Colors.grey[400] : Colors.grey[600], ), ), const SizedBox(width: 8), Text( origin['author'] ?? '未知作者', style: TextStyle( fontSize: 14, color: isDark ? Colors.grey[300] : Colors.grey[700], ), ), ], ), const SizedBox(height: 16), ], if (data != null) ...[ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isDark ? const Color(0xFF1A1A1A).withAlpha(50) : Colors.white.withAlpha(50), borderRadius: BorderRadius.circular(12), ), child: Text( data['content'] ?? '暂无内容', style: TextStyle( fontSize: 16, color: isDark ? Colors.white : Colors.black87, height: 1.8, letterSpacing: 0.5, ), textAlign: TextAlign.center, ), ), ], if (origin != null && origin['content'] != null) ...[ const SizedBox(height: 16), _buildContentBlock( '完整诗词', (origin['content'] as List).map((e) => e.toString()).join('\n'), isDark, primaryColor, ), ], if (origin != null && origin['translate'] != null) ...[ const SizedBox(height: 12), _buildContentBlock( '翻译', (origin['translate'] as List).map((e) => e.toString()).join('\n'), isDark, primaryColor, ), ], if (data != null && data['matchTags'] != null) ...[ const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: (data['matchTags'] as List) .map( (tag) => Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( color: primaryColor.withAlpha(15), borderRadius: BorderRadius.circular(16), border: Border.all(color: primaryColor.withAlpha(30)), ), child: Text( tag.toString(), style: TextStyle( fontSize: 12, color: primaryColor, fontWeight: FontWeight.w500, ), ), ), ) .toList(), ), ], const SizedBox(height: 16), if (data != null) _buildPoetryMetadata(data, isDark, primaryColor), ], ), ); } Widget _buildPoetryMetadata( Map data, bool isDark, Color primaryColor, ) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isDark ? const Color(0xFF1A1A1A).withAlpha(50) : Colors.grey[100]!.withAlpha(128), borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( onTap: () { setState(() { _isMetadataExpanded = !_isMetadataExpanded; }); }, behavior: HitTestBehavior.opaque, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Text( '流行度', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: primaryColor, ), ), const SizedBox(width: 8), Text( data['popularity'] != null ? _formatNumber(data['popularity']) : '-', style: TextStyle( fontSize: 13, color: isDark ? Colors.grey[300] : Colors.grey[700], ), ), ], ), Row( children: [ Text( '诗词信息', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: primaryColor, ), ), const SizedBox(width: 4), Icon( _isMetadataExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, color: primaryColor, size: 20, ), ], ), ], ), ), if (_isMetadataExpanded) ...[ const SizedBox(height: 8), const Divider(height: 1), const SizedBox(height: 8), if (data['id'] != null) _buildMetadataRow('ID', data['id'].toString(), isDark), if (data['cacheAt'] != null) _buildMetadataRow('缓存时间', data['cacheAt'].toString(), isDark), if (data['recommendedReason'] != null && data['recommendedReason'].toString().isNotEmpty) _buildMetadataRow( '推荐理由', data['recommendedReason'].toString(), isDark, ), if (_poetryData!['token'] != null) _buildMetadataRow( 'Token', _poetryData!['token'].toString().substring( 0, (_poetryData!['token'].toString().length > 20 ? 20 : _poetryData!['token'].toString().length), ) + '...', isDark, ), if (_poetryData!['ipAddress'] != null) _buildMetadataRow( 'IP 地址', _poetryData!['ipAddress'].toString(), isDark, ), ], ], ), ); } Widget _buildMetadataRow(String label, String value, bool isDark) { return Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 80, child: Text( label, style: TextStyle( fontSize: 12, color: isDark ? Colors.grey[400] : Colors.grey[600], ), ), ), Expanded( child: Text( value, style: TextStyle( fontSize: 12, color: isDark ? Colors.grey[300] : Colors.grey[700], ), ), ), ], ), ); } Widget _buildUserInfoCard(bool isDark, Color primaryColor) { final data = _userInfo?['data'] as Map?; if (data == null) return const SizedBox.shrink(); final weatherData = data['weatherData'] as Map?; final tags = data['tags'] as List?; return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: LinearGradient( colors: [primaryColor.withAlpha(25), primaryColor.withAlpha(10)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), border: Border.all(color: primaryColor.withAlpha(40), width: 1), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: primaryColor.withAlpha(20), borderRadius: BorderRadius.circular(12), ), child: const Center( child: Text('🌤️', style: TextStyle(fontSize: 20)), ), ), const SizedBox(width: 12), Text( '环境信息', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black87, ), ), ], ), const SizedBox(height: 16), _buildUserInfoRow('IP 地址', data['ip']?.toString() ?? '未知', isDark), _buildUserInfoRow('地区', data['region']?.toString() ?? '未知', isDark), if (data['beijingTime'] != null) _buildUserInfoRow('北京时间', data['beijingTime'].toString(), isDark), if (weatherData != null) ...[ const SizedBox(height: 12), _buildWeatherInfo(weatherData, isDark, primaryColor), ], if (tags != null && tags.isNotEmpty) ...[ const SizedBox(height: 12), _buildTags(tags, isDark, primaryColor), ], ], ), ); } Widget _buildUserInfoRow(String label, String value, bool isDark) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 80, child: Text( label, style: TextStyle( fontSize: 13, color: isDark ? Colors.grey[400] : Colors.grey[600], fontWeight: FontWeight.w500, ), ), ), Expanded( child: Text( value, style: TextStyle( fontSize: 13, color: isDark ? Colors.white : Colors.black87, ), ), ), ], ), ); } Widget _buildWeatherInfo( Map weatherData, bool isDark, Color primaryColor, ) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isDark ? const Color(0xFF1A1A1A).withAlpha(50) : Colors.white.withAlpha(50), borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.cloud, color: primaryColor, size: 16), const SizedBox(width: 6), Text( '天气信息', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: isDark ? Colors.white : Colors.black87, ), ), ], ), const SizedBox(height: 8), Wrap( spacing: 12, runSpacing: 8, children: [ _buildWeatherItem( '温度', '${weatherData['temperature']?.toString() ?? '-'}°C', isDark, ), _buildWeatherItem( '天气', weatherData['weather']?.toString() ?? '-', isDark, ), _buildWeatherItem( '湿度', '${weatherData['humidity']?.toString() ?? '-'}%', isDark, ), _buildWeatherItem( '风向', weatherData['windDirection']?.toString() ?? '-', isDark, ), _buildWeatherItem( '风力', '${weatherData['windPower']?.toString() ?? '-'}级', isDark, ), _buildWeatherItem( '能见度', weatherData['visibility']?.toString() ?? '-', isDark, ), if (weatherData['rainfall'] != null) _buildWeatherItem( '降雨量', '${weatherData['rainfall']}mm', isDark, ), if (weatherData['pm25'] != null) _buildWeatherItem('PM2.5', '${weatherData['pm25']}', isDark), ], ), ], ), ); } Widget _buildWeatherItem(String label, String value, bool isDark) { return Row( mainAxisSize: MainAxisSize.min, children: [ Text( '$label: ', style: TextStyle( fontSize: 12, color: isDark ? Colors.grey[400] : Colors.grey[600], ), ), Text( value, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: isDark ? Colors.white : Colors.black87, ), ), ], ); } Widget _buildTags(List tags, bool isDark, Color primaryColor) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.local_offer, color: primaryColor, size: 16), const SizedBox(width: 6), Text( '推荐标签', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: isDark ? Colors.white : Colors.black87, ), ), ], ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: tags.map((tag) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: primaryColor.withAlpha(15), borderRadius: BorderRadius.circular(16), border: Border.all(color: primaryColor.withAlpha(30)), ), child: Text( tag.toString(), style: TextStyle( fontSize: 12, color: primaryColor, fontWeight: FontWeight.w500, ), ), ); }).toList(), ), ], ); } Widget _buildSectionCard( String title, IconData icon, List children, bool isDark, Color primaryColor, ) { return Container( decoration: BoxDecoration( color: isDark ? const Color(0xFF2A2A2A) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: isDark ? Colors.black.withAlpha(76) : Colors.black.withAlpha(26), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Icon(icon, color: primaryColor, size: 20), const SizedBox(width: 8), Text( title, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: isDark ? Colors.white : Colors.black87, ), ), ], ), ), ...children, ], ), ); } Widget _buildContentBlock( String title, String content, bool isDark, Color primaryColor, ) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isDark ? const Color(0xFF1A1A1A).withAlpha(50) : Colors.grey[100]!.withAlpha(128), borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: primaryColor, ), ), const SizedBox(height: 8), ...(content.split('\n')).map( (line) => Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( line, style: TextStyle( fontSize: 14, color: isDark ? Colors.grey[300] : Colors.grey[700], height: 1.6, ), textAlign: TextAlign.center, ), ), ), ], ), ); } String _formatNumber(dynamic number) { if (number is int) { if (number >= 10000) { return '${(number / 10000).toStringAsFixed(1)}万'; } return number.toString(); } return number.toString(); } }