Initial commit: Flutter 无书应用项目

This commit is contained in:
Developer
2026-03-30 02:35:31 +08:00
commit 9175ff9905
566 changed files with 103261 additions and 0 deletions

View File

@@ -0,0 +1,337 @@
/// 时间: 2026-03-29
/// 功能: 首页自动刷新管理
/// 介绍: 管理首页诗句自动刷新功能,包括定时器、状态管理等
/// 最新变化: 新建文件
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../utils/http/poetry_api.dart';
class AutoRefreshManager {
static const String _autoRefreshKey = 'auto_refresh_enabled';
static const Duration _refreshInterval = Duration(seconds: 5);
static AutoRefreshManager? _instance;
Timer? _refreshTimer;
bool _isEnabled = false;
VoidCallback? _onRefresh;
AutoRefreshManager._internal();
factory AutoRefreshManager() {
_instance ??= AutoRefreshManager._internal();
return _instance!;
}
Future<void> init() async {
final prefs = await SharedPreferences.getInstance();
_isEnabled = prefs.getBool(_autoRefreshKey) ?? false;
_debugLog('自动刷新初始化,状态: $_isEnabled');
}
bool get isEnabled => _isEnabled;
Future<void> setEnabled(bool enabled) async {
_isEnabled = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_autoRefreshKey, enabled);
_debugLog('自动刷新状态已设置: $enabled');
if (enabled) {
_startTimer();
} else {
_stopTimer();
}
}
void setOnRefresh(VoidCallback? callback) {
_onRefresh = callback;
_debugLog('设置刷新回调');
}
void _startTimer() {
_stopTimer();
_refreshTimer = Timer.periodic(_refreshInterval, (timer) {
_debugLog('自动刷新触发');
if (_onRefresh != null) {
_onRefresh!();
}
});
_debugLog('自动刷新定时器已启动');
}
void _stopTimer() {
if (_refreshTimer != null) {
_refreshTimer!.cancel();
_refreshTimer = null;
_debugLog('自动刷新定时器已停止');
}
}
void stopTimer() {
_stopTimer();
}
void dispose() {
_stopTimer();
_onRefresh = null;
_debugLog('自动刷新管理器已释放');
}
void _debugLog(String message) {
if (kDebugMode) {
print('AutoRefreshManager: $message');
}
}
}
class DebugInfoManager {
static const String _debugInfoKey = 'debug_info_enabled';
static DebugInfoManager? _instance;
bool _isEnabled = false;
final ValueNotifier<String> _messageNotifier = ValueNotifier<String>('');
Timer? _messageTimer;
DebugInfoManager._internal();
factory DebugInfoManager() {
_instance ??= DebugInfoManager._internal();
return _instance!;
}
Future<void> init() async {
final prefs = await SharedPreferences.getInstance();
_isEnabled = prefs.getBool(_debugInfoKey) ?? false;
_debugLog('调试信息初始化,状态: $_isEnabled');
}
bool get isEnabled => _isEnabled;
ValueNotifier<String> get messageNotifier => _messageNotifier;
Future<void> setEnabled(bool enabled) async {
_isEnabled = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_debugInfoKey, enabled);
_debugLog('调试信息状态已设置: $enabled');
if (!enabled) {
_messageNotifier.value = '';
}
}
void showMessage(String message) {
if (!_isEnabled) return;
_messageNotifier.value = message;
_debugLog('显示调试信息: $message');
_messageTimer?.cancel();
_messageTimer = Timer(const Duration(seconds: 2), () {
_messageNotifier.value = '';
_debugLog('调试信息已隐藏');
});
}
void showRefreshSuccess() {
showMessage('刷新成功');
}
void showRefreshFailed() {
showMessage('刷新失败');
}
void showNextSuccess() {
showMessage('下一条成功');
}
void showNextFailed() {
showMessage('下一条失败');
}
void showPreviousSuccess() {
showMessage('上一条成功');
}
void showPreviousFailed() {
showMessage('上一条失败');
}
void showLiked() {
showMessage('已点赞');
}
void showUnliked() {
showMessage('已取消点赞');
}
void showCopySuccess() {
showMessage('复制成功');
}
void showCopyFailed() {
showMessage('复制失败');
}
void dispose() {
_messageTimer?.cancel();
_messageNotifier.value = '';
_debugLog('调试信息管理器已清理');
}
void _debugLog(String message) {
if (kDebugMode) {
print('DebugInfoManager: $message');
}
}
}
class OfflineDataManager {
static const String _offlineDataKey = 'offline_poetry_data';
static const String _onlineStatusKey = 'personal_card_online';
static OfflineDataManager? _instance;
List<Map<String, dynamic>> _cachedPoetryList = [];
int _currentIndex = 0;
OfflineDataManager._internal();
factory OfflineDataManager() {
_instance ??= OfflineDataManager._internal();
return _instance!;
}
Future<void> init() async {
await _loadCachedData();
_debugLog('离线数据管理器初始化,缓存数量: ${_cachedPoetryList.length}');
}
Future<bool> isOnline() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_onlineStatusKey) ?? true;
}
Future<bool> hasCachedData() async {
await _loadCachedData();
return _cachedPoetryList.isNotEmpty;
}
Future<PoetryData?> getNextPoetry() async {
await _loadCachedData();
if (_cachedPoetryList.isEmpty) {
return null;
}
final poetryData = _cachedPoetryList[_currentIndex];
_currentIndex = (_currentIndex + 1) % _cachedPoetryList.length;
return _convertToPoetryData(poetryData);
}
Future<PoetryData?> getPreviousPoetry() async {
await _loadCachedData();
if (_cachedPoetryList.isEmpty) {
return null;
}
_currentIndex =
(_currentIndex - 1 + _cachedPoetryList.length) %
_cachedPoetryList.length;
final poetryData = _cachedPoetryList[_currentIndex];
return _convertToPoetryData(poetryData);
}
Future<void> _loadCachedData() async {
final prefs = await SharedPreferences.getInstance();
final cachedData = prefs.getStringList(_offlineDataKey) ?? [];
_cachedPoetryList = [];
for (final item in cachedData) {
try {
// 移除 Map 两边的大括号,然后分割键值对
final cleanItem = item.substring(1, item.length - 1);
final keyValuePairs = cleanItem.split(', ');
final map = <String, dynamic>{};
for (final pair in keyValuePairs) {
final parts = pair.split(': ');
if (parts.length == 2) {
final key = parts[0].replaceAll('"', '');
var value = parts[1].replaceAll('"', '');
// 尝试转换数字
if (int.tryParse(value) != null) {
map[key] = int.parse(value);
} else if (double.tryParse(value) != null) {
map[key] = double.parse(value);
} else {
map[key] = value;
}
}
}
_cachedPoetryList.add(map);
} catch (e) {
_debugLog('解析缓存数据失败: $e');
}
}
}
PoetryData? _convertToPoetryData(Map<String, dynamic> data) {
try {
final id = data['id'] ?? 0;
final name = data['name'] ?? '';
final alias = data['alias'] ?? '';
final keywords = data['keywords'] ?? '';
final introduce = data['introduce'] ?? '';
final drtime = data['drtime'] ?? '';
final like = data['like'] ?? 0;
final url = data['url'] ?? '';
final tui = data['tui'] ?? 0;
final star = data['star'] ?? 5;
final hitsTotal = data['hitsTotal'] ?? 0;
final hitsMonth = data['hitsMonth'] ?? 0;
final hitsDay = data['hitsDay'] ?? 0;
final date = data['date'] ?? '';
final datem = data['datem'] ?? '';
final time = data['time'] ?? '';
final createTime = data['createTime'] ?? '';
final updateTime = data['updateTime'] ?? '';
return PoetryData(
id: id,
name: name,
alias: alias,
keywords: keywords,
introduce: introduce,
drtime: drtime,
like: like,
url: url,
tui: tui,
star: star,
hitsTotal: hitsTotal,
hitsMonth: hitsMonth,
hitsDay: hitsDay,
date: date,
datem: datem,
time: time,
createTime: createTime,
updateTime: updateTime,
);
} catch (e) {
_debugLog('转换为PoetryData失败: $e');
return null;
}
}
void _debugLog(String message) {
if (kDebugMode) {
print('OfflineDataManager: $message');
}
}
}

View File

@@ -0,0 +1,279 @@
/// 时间: 2025-03-22
/// 功能: 诗词页面通用组件和工具函数
/// 介绍: 从 home_page.dart 和 home_part.dart 中提取的公共组件,用于代码复用和简化
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../constants/app_constants.dart';
import '../../utils/http/poetry_api.dart';
import 'home-load.dart';
/// 加载状态组件
class LoadingWidget extends StatelessWidget {
const LoadingWidget({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
AppConstants.primaryColor,
),
),
SizedBox(height: 16),
Text(
'加载中...',
style: TextStyle(fontSize: 16, color: const Color(0xFF757575)),
),
],
),
);
}
}
/// 错误状态组件
class CustomErrorWidget extends StatelessWidget {
final String errorMessage;
final VoidCallback onRetry;
const CustomErrorWidget({
super.key,
required this.errorMessage,
required this.onRetry,
});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
errorMessage,
style: const TextStyle(fontSize: 16, color: 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 {
const EmptyWidget({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text('暂无诗词内容', style: TextStyle(fontSize: 16, color: 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<String> 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<double> createFadeAnimation(AnimationController controller) {
return CurvedAnimation(parent: controller, curve: Curves.easeIn);
}
static Animation<Offset> createSlideAnimation(
AnimationController controller,
) {
return Tween<Offset>(
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,
}) {
try {
Clipboard.setData(ClipboardData(text: content));
PoetryStateManager.showSnackBar(context, '已复制$contentType');
DebugInfoManager().showCopySuccess();
onSuccess?.call();
} catch (e) {
PoetryStateManager.showSnackBar(
context,
'复制失败',
backgroundColor: AppConstants.errorColor,
);
DebugInfoManager().showCopyFailed();
}
}
static void showCopyDialog(
BuildContext context,
String content,
String contentType,
) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('复制$contentType'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'受隐私权限管理,写入剪切板需告知用户',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
const SizedBox(height: 12),
Text(
'预览内容:',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
const SizedBox(height: 4),
Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.grey[300]!),
),
child: Text(
content.length > 50
? '${content.substring(0, 50)}...'
: content,
style: const TextStyle(fontSize: 12, color: Colors.black87),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('返回'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
copyToClipboard(context, content, contentType);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppConstants.primaryColor,
foregroundColor: Colors.white,
),
child: Text('复制$contentType'),
),
],
),
);
}
}

View File

@@ -0,0 +1,806 @@
/// 时间: 2025-03-22
/// 功能: 诗词主页(参考微信小程序布局)
/// 介绍: 展示诗词内容支持点赞、收藏、分享等功能参考wxpm小程序的index页面布局
/// 最新变化: 2025-03-22 重构代码结构,使用组件化架构简化代码
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../constants/app_constants.dart';
import '../../../controllers/history_controller.dart';
import '../../../utils/http/poetry_api.dart';
import '../../../services/network_listener_service.dart';
import 'home_part.dart';
import 'home_components.dart';
import 'home-load.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with TickerProviderStateMixin, NetworkListenerMixin {
PoetryData? _poetryData;
List<String> _keywordList = [];
bool _loading = false;
bool _isLiked = false;
bool _isLoadingLike = false;
String _errorMessage = '';
String _starDisplay = '';
final String _historyKey = 'poetry_history';
List<Map<String, dynamic>> _historyList = [];
int _currentHistoryIndex = -1;
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
// 动态加载状态
bool _isLoadingNext = false;
bool _isLoadingPrevious = false;
Map<String, bool> _sectionLoadingStates = {
'title': false,
'content': false,
'name': false,
'keywords': false,
'introduction': false,
};
@override
void initState() {
super.initState();
_initAnimations();
_loadHistory();
_initAutoRefresh();
_initDebugInfo();
_initOfflineDataManager();
// 延迟加载诗词,确保页面先显示
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadPoetry();
});
}
Future<void> _initOfflineDataManager() async {
final offlineDataManager = OfflineDataManager();
await offlineDataManager.init();
}
Future<void> _initAutoRefresh() async {
final autoRefreshManager = AutoRefreshManager();
await autoRefreshManager.init();
autoRefreshManager.setOnRefresh(() {
if (mounted) {
_loadNextPoetry();
}
});
if (autoRefreshManager.isEnabled) {
autoRefreshManager.setEnabled(true);
}
}
Future<void> _initDebugInfo() async {
final debugInfoManager = DebugInfoManager();
await debugInfoManager.init();
}
void _initAnimations() {
_fadeController = AnimationUtils.createFadeController(this);
_slideController = AnimationUtils.createSlideController(this);
_fadeAnimation = AnimationUtils.createFadeAnimation(_fadeController);
_slideAnimation = AnimationUtils.createSlideAnimation(_slideController);
}
@override
void dispose() {
_fadeController.dispose();
_slideController.dispose();
// 只停止定时器,不释放单例资源
AutoRefreshManager().stopTimer();
// 不调用DebugInfoManager的dispose因为它是单例其他页面可能还在使用
super.dispose();
}
Future<void> _loadPoetry() async {
if (_loading) return;
setState(() => _loading = true);
PoetryStateManager.triggerHapticFeedback();
try {
final offlineDataManager = OfflineDataManager();
final isOnline = await offlineDataManager.isOnline();
final hasCachedData = await offlineDataManager.hasCachedData();
if (isOnline) {
// 在线状态:从网络加载
final response = await PoetryApi.getRandomPoetry();
if (mounted && response.data != null) {
setState(() {
_poetryData = response.data;
_keywordList = PoetryDataUtils.extractKeywords(response.data);
_starDisplay = PoetryDataUtils.getStarDisplay(response.data);
_isLiked = false;
_loading = false;
_errorMessage = '';
});
_fadeController.forward();
_slideController.forward();
_checkIfLiked();
await _saveToHistory(response.data!);
DebugInfoManager().showRefreshSuccess();
} else {
// 数据为空时,显示默认内容
if (mounted) {
setState(() {
_poetryData = _createDefaultPoetryData();
_keywordList = [];
_starDisplay = '⭐⭐⭐⭐⭐';
_isLiked = false;
_loading = false;
_errorMessage = '';
});
_fadeController.forward();
_slideController.forward();
}
DebugInfoManager().showRefreshFailed();
}
} else {
// 离线状态:从本地缓存加载
if (hasCachedData) {
final poetryData = await offlineDataManager.getNextPoetry();
if (mounted && poetryData != null) {
setState(() {
_poetryData = poetryData;
_keywordList = PoetryDataUtils.extractKeywords(poetryData);
_starDisplay = PoetryDataUtils.getStarDisplay(poetryData);
_isLiked = false;
_loading = false;
_errorMessage = '';
});
_fadeController.forward();
_slideController.forward();
_checkIfLiked();
DebugInfoManager().showRefreshSuccess();
} else {
// 缓存为空时,显示错误信息
if (mounted) {
setState(() {
_loading = false;
_errorMessage = '离线模式下无缓存数据,请先在线下载';
});
}
DebugInfoManager().showRefreshFailed();
}
} else {
// 离线且无缓存时,显示错误信息
if (mounted) {
setState(() {
_loading = false;
_errorMessage = '离线模式下无缓存数据,请先在线下载';
});
}
DebugInfoManager().showRefreshFailed();
}
}
} catch (e) {
debugPrint('加载诗词失败: $e');
if (mounted) {
setState(() {
_loading = false;
_errorMessage = '加载失败,请检查网络连接';
});
}
DebugInfoManager().showRefreshFailed();
}
}
// 创建默认诗词数据,确保页面始终有内容显示
PoetryData _createDefaultPoetryData() {
final now = DateTime.now();
final dateStr = now.toString().substring(0, 10);
final monthStr = dateStr.substring(0, 7);
final timeStr = now.toString().substring(11, 19);
return PoetryData(
id: 1,
name: '静夜思',
alias: '李白',
keywords: '思乡,月亮,静夜',
introduce: '床前明月光,疑是地上霜。举头望明月,低头思故乡。',
drtime: '唐·李白《静夜思》',
like: 0,
url: '李白-静夜思',
tui: 1,
star: 5,
hitsTotal: 10000,
hitsMonth: 1000,
hitsDay: 100,
date: dateStr,
datem: monthStr,
time: timeStr,
createTime: timeStr,
updateTime: timeStr,
);
}
Future<void> _loadPoetryById(int poetryId) async {
if (_loading) return;
setState(() => _loading = true);
try {
final response = await PoetryApi.getPoetryById(poetryId);
if (mounted && response.data != null) {
setState(() {
_poetryData = response.data;
_keywordList = PoetryDataUtils.extractKeywords(response.data);
_starDisplay = PoetryDataUtils.getStarDisplay(response.data);
_loading = false;
_errorMessage = '';
});
_fadeController.forward();
_slideController.forward();
_checkIfLiked();
_updateCurrentHistoryIndex();
} else {
// 数据为空时,显示默认内容
if (mounted) {
setState(() {
_poetryData = _createDefaultPoetryData();
_keywordList = [];
_starDisplay = '⭐⭐⭐⭐⭐';
_isLiked = false;
_loading = false;
_errorMessage = '';
});
_fadeController.forward();
_slideController.forward();
}
}
} catch (e) {
debugPrint('加载指定诗词失败: $e');
if (mounted) {
setState(() {
_poetryData = _createDefaultPoetryData();
_keywordList = [];
_starDisplay = '⭐⭐⭐⭐⭐';
_isLiked = false;
_loading = false;
_errorMessage = '';
});
_fadeController.forward();
_slideController.forward();
}
}
}
Future<void> _toggleLike() async {
if (_poetryData == null || _isLoadingLike) return;
// 立即切换按钮状态和显示加载
setState(() {
_isLoadingLike = true;
});
// 发送网络加载开始事件
startNetworkLoading('toggle_like');
try {
final response = await PoetryApi.toggleLike(_poetryData!.id);
if (mounted) {
setState(() {
// 根据API响应消息直接判断状态
_isLiked = response.message.contains('点赞成功');
// 更新诗词数据
if (response.data != null) {
_poetryData = PoetryData(
id: _poetryData!.id,
name: _poetryData!.name,
alias: _poetryData!.alias,
keywords: _poetryData!.keywords,
introduce: _poetryData!.introduce,
drtime: _poetryData!.drtime,
like: response.data!.like,
url: _poetryData!.url,
tui: _poetryData!.tui,
star: _poetryData!.star,
hitsTotal: _poetryData!.hitsTotal,
hitsMonth: _poetryData!.hitsMonth,
hitsDay: _poetryData!.hitsDay,
date: _poetryData!.date,
datem: _poetryData!.datem,
time: _poetryData!.time,
createTime: _poetryData!.createTime,
updateTime: _poetryData!.updateTime,
);
}
});
// 管理点赞存储
if (_isLiked) {
// 添加到点赞列表
await HistoryController.addToLiked(_poetryData!.toJson());
} else {
// 从点赞列表移除
await HistoryController.removeLikedPoetry(_poetryData!.id.toString());
}
// 发送点赞事件通知其他页面
sendLikeEvent(_poetryData!.id.toString(), _isLiked);
if (_isLiked) {
DebugInfoManager().showLiked();
} else {
DebugInfoManager().showUnliked();
}
PoetryStateManager.showSnackBar(
context,
response.message,
backgroundColor: AppConstants.successColor,
duration: const Duration(milliseconds: 200),
);
}
} catch (e) {
if (mounted) {
PoetryStateManager.showSnackBar(
context,
'操作失败,请重试',
backgroundColor: AppConstants.errorColor,
duration: const Duration(milliseconds: 200),
);
}
} finally {
if (mounted) {
setState(() => _isLoadingLike = false);
// 发送网络加载结束事件
endNetworkLoading('toggle_like');
}
}
}
Future<void> _loadHistory() async {
try {
final historyJson = await HistoryController.getHistory();
if (mounted) {
setState(() {
_historyList = historyJson;
// 重新计算当前诗词在历史记录中的索引
if (_poetryData != null && _historyList.isNotEmpty) {
_currentHistoryIndex = _historyList.indexWhere(
(item) => item['id'] == _poetryData!.id,
);
} else {
_currentHistoryIndex = -1;
}
});
}
} catch (e) {
print('加载历史记录失败: $e');
}
}
Future<void> _saveToHistory(PoetryData poetryData) async {
try {
final poetryMap = {
'id': poetryData.id,
'name': poetryData.name, // 诗句名称/标题
'alias': poetryData.alias, // 诗句朝代/作者
'introduce': poetryData.introduce, // 诗句译文/解释
'drtime': poetryData.drtime, // 诗句原文/内容
};
await HistoryController.addToHistory(poetryMap);
} catch (e) {
print('保存历史记录失败: $e');
}
}
void _updateCurrentHistoryIndex() {
if (_poetryData?.id != null) {
final index = _historyList.indexWhere(
(item) => item['id'] == _poetryData!.id,
);
_currentHistoryIndex = index >= 0 ? index : -1;
}
}
Future<void> _checkIfLiked() async {
// 这里可以实现检查收藏状态的逻辑
// 暂时使用简单的模拟逻辑
}
void _loadNextPoetry() async {
if (_isLoadingNext) return;
setState(() {
_isLoadingNext = true;
// 设置所有区域为加载状态
_sectionLoadingStates = {
'title': true,
'content': true,
'name': true,
'keywords': true,
'introduction': true,
};
});
try {
final offlineDataManager = OfflineDataManager();
final isOnline = await offlineDataManager.isOnline();
final hasCachedData = await offlineDataManager.hasCachedData();
PoetryData? newPoetryData;
if (isOnline) {
// 在线状态:从网络加载
// 确保历史记录已加载
if (_historyList.isEmpty) {
await _loadHistory();
}
if (_currentHistoryIndex < 0 ||
_currentHistoryIndex >= _historyList.length - 1) {
// 如果没有下一条了,加载新的诗词
final response = await PoetryApi.getRandomPoetry();
newPoetryData = response.data;
if (mounted && newPoetryData != null) {
await _saveToHistory(newPoetryData);
}
} else {
// 如果有下一条,加载下一条
final nextPoetry = _historyList[_currentHistoryIndex + 1];
final response = await PoetryApi.getPoetryById(nextPoetry['id']);
newPoetryData = response.data;
}
} else {
// 离线状态:从本地缓存加载
if (hasCachedData) {
newPoetryData = await offlineDataManager.getNextPoetry();
} else {
// 离线且无缓存时,显示错误信息
if (mounted) {
PoetryStateManager.showSnackBar(
context,
'离线模式下无缓存数据,请先在线下载',
backgroundColor: AppConstants.errorColor,
duration: const Duration(milliseconds: 200),
);
// 重置所有加载状态
setState(() {
_sectionLoadingStates.updateAll((key, value) => false);
});
}
DebugInfoManager().showNextFailed();
return;
}
}
if (mounted && newPoetryData != null) {
// 模拟分步加载
await _simulateSectionLoading(newPoetryData);
DebugInfoManager().showNextSuccess();
}
} catch (e) {
if (mounted) {
PoetryStateManager.showSnackBar(
context,
'加载下一条失败',
backgroundColor: AppConstants.errorColor,
duration: const Duration(milliseconds: 200),
);
// 重置所有加载状态
setState(() {
_sectionLoadingStates.updateAll((key, value) => false);
});
}
DebugInfoManager().showNextFailed();
} finally {
if (mounted) {
setState(() {
_isLoadingNext = false;
});
}
}
}
// 模拟分步加载过程
Future<void> _simulateSectionLoading(PoetryData newPoetryData) async {
// 1. 加载标题区域
setState(() {
_sectionLoadingStates['title'] = false;
_poetryData = newPoetryData;
});
await Future.delayed(const Duration(milliseconds: 200));
// 2. 加载诗句区域
setState(() {
_sectionLoadingStates['name'] = false;
});
await Future.delayed(const Duration(milliseconds: 200));
// 3. 加载原文区域
setState(() {
_sectionLoadingStates['content'] = false;
});
await Future.delayed(const Duration(milliseconds: 200));
// 4. 加载关键词区域
setState(() {
_sectionLoadingStates['keywords'] = false;
_keywordList = PoetryDataUtils.extractKeywords(newPoetryData);
});
await Future.delayed(const Duration(milliseconds: 200));
// 5. 加载译文区域
setState(() {
_sectionLoadingStates['introduction'] = false;
_starDisplay = PoetryDataUtils.getStarDisplay(newPoetryData);
_isLiked = false;
_errorMessage = '';
});
_checkIfLiked();
_updateCurrentHistoryIndex();
}
void _loadPreviousPoetry() async {
try {
final offlineDataManager = OfflineDataManager();
final isOnline = await offlineDataManager.isOnline();
final hasCachedData = await offlineDataManager.hasCachedData();
if (isOnline) {
// 在线状态:从历史记录加载
// 确保历史记录已加载
if (_historyList.isEmpty) {
await _loadHistory();
}
if (_currentHistoryIndex <= 0) {
// 如果当前索引无效或已经是第一条,则重新加载历史记录
await _loadHistory();
// 如果历史记录为空,显示提示
if (_historyList.isEmpty) {
if (mounted) {
PoetryStateManager.showSnackBar(
context,
'暂无历史记录',
backgroundColor: AppConstants.errorColor,
duration: const Duration(milliseconds: 200),
);
}
return;
}
// 如果当前索引无效,设置为最新的一条
if (_currentHistoryIndex < 0) {
_currentHistoryIndex = 0;
}
}
// 检查是否有上一条
if (_currentHistoryIndex > 0) {
final previousPoetry = _historyList[_currentHistoryIndex - 1];
await _loadPoetryById(previousPoetry['id']);
} else {
// 如果没有上一条了,显示提示
if (mounted) {
PoetryStateManager.showSnackBar(
context,
'已经是第一条了',
backgroundColor: AppConstants.infoColor,
duration: const Duration(milliseconds: 200),
);
}
}
} else {
// 离线状态:从本地缓存加载
if (hasCachedData) {
final poetryData = await offlineDataManager.getPreviousPoetry();
if (mounted && poetryData != null) {
setState(() {
_poetryData = poetryData;
_keywordList = PoetryDataUtils.extractKeywords(poetryData);
_starDisplay = PoetryDataUtils.getStarDisplay(poetryData);
_isLiked = false;
_errorMessage = '';
});
_fadeController.forward();
_slideController.forward();
_checkIfLiked();
DebugInfoManager().showPreviousSuccess();
} else {
// 缓存为空时,显示错误信息
if (mounted) {
PoetryStateManager.showSnackBar(
context,
'离线模式下无缓存数据,请先在线下载',
backgroundColor: AppConstants.errorColor,
duration: const Duration(milliseconds: 200),
);
}
DebugInfoManager().showPreviousFailed();
}
} else {
// 离线且无缓存时,显示错误信息
if (mounted) {
PoetryStateManager.showSnackBar(
context,
'离线模式下无缓存数据,请先在线下载',
backgroundColor: AppConstants.errorColor,
duration: const Duration(milliseconds: 200),
);
}
DebugInfoManager().showPreviousFailed();
}
}
} catch (e) {
if (mounted) {
PoetryStateManager.showSnackBar(
context,
'加载上一条失败',
backgroundColor: AppConstants.errorColor,
duration: const Duration(milliseconds: 200),
);
}
}
}
Future<void> _refreshPoetry() async {
if (_poetryData?.id != null) {
// 如果有当前诗词ID则重新加载当前诗词
await _loadPoetryById(_poetryData!.id);
} else {
// 如果没有当前诗词ID则加载新的诗词
await _loadPoetry();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
body: SafeArea(child: _buildBody()),
);
}
Widget _buildBody() {
if (_loading) {
return const LoadingWidget();
}
if (_errorMessage.isNotEmpty) {
return CustomErrorWidget(
errorMessage: _errorMessage,
onRetry: _loadPoetry,
);
}
if (!PoetryDataUtils.isValidPoetryData(_poetryData)) {
return const EmptyWidget();
}
return _buildContent();
}
Widget _buildContent() {
return FutureBuilder<bool>(
future: OfflineDataManager().isOnline(),
builder: (context, snapshot) {
final isOnline = snapshot.data ?? true;
return RefreshIndicator(
onRefresh: _refreshPoetry,
child: FadeTransition(
opacity: _fadeAnimation,
child: Stack(
children: [
SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), // 确保可以下拉刷新
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
children: [
PoetryCard(
poetryData: _poetryData!,
keywordList: _keywordList,
onTap: _loadNextPoetry,
sectionLoadingStates: _sectionLoadingStates,
),
const SizedBox(height: 160), // 为悬浮按钮留出更多空间
],
),
),
// 调试信息气泡
Positioned(
bottom: 66,
left: 0,
right: 0,
child: Center(child: _buildDebugInfoBubble()),
),
// 悬浮上一条按钮 - 左边上方
Positioned(
left: 16,
bottom: 100,
child: FloatingPreviousButton(
onPrevious: _loadPreviousPoetry,
),
),
// 悬浮下一条按钮 - 左边下方
Positioned(
left: 16,
bottom: 32,
child: FloatingNextButton(onNext: _loadNextPoetry),
),
// 悬浮点赞按钮 - 右边(仅在线状态显示)
if (isOnline)
Positioned(
right: 16,
bottom: 32,
child: FloatingLikeButton(
isLiked: _isLiked,
isLoadingLike: _isLoadingLike,
onToggleLike: _toggleLike,
),
),
],
),
),
);
},
);
}
Widget _buildDebugInfoBubble() {
return ValueListenableBuilder<String>(
valueListenable: DebugInfoManager().messageNotifier,
builder: (context, message, child) {
if (message.isEmpty) {
return const SizedBox.shrink();
}
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: AppConstants.primaryColor.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Text(
message,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
);
},
);
}
}

View File

@@ -0,0 +1,868 @@
/// 时间: 2025-03-22
/// 功能: 诗词页面组件部分
/// 介绍: 包含诗词卡片、操作按钮、统计信息等组件的独立文件
/// 最新变化: 重构代码结构,使用工具类简化代码
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../constants/app_constants.dart';
import '../../../utils/http/poetry_api.dart';
import 'home_components.dart';
/// 诗词卡片组件 - 优化版本,防止拉伸和处理文本溢出
class PoetryCard extends StatefulWidget {
final PoetryData poetryData;
final List<String> keywordList;
final VoidCallback? onTap;
final Map<String, bool>? sectionLoadingStates;
const PoetryCard({
super.key,
required this.poetryData,
required this.keywordList,
this.onTap,
this.sectionLoadingStates,
});
@override
State<PoetryCard> createState() => _PoetryCardState();
}
class _PoetryCardState extends State<PoetryCard> {
bool _showCopyTip = true;
bool _showRecommendation = false;
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 '推荐:婉约词';
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: widget.onTap,
child: Stack(
children: [
Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
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.withValues(alpha: 0.8),
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.withValues(alpha: 0.3),
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),
const Text(
'点击任意区域加载下一条,长按复制,下拉刷新',
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<Color>(
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.withValues(alpha: 0.1),
AppConstants.primaryColor.withValues(alpha: 0.05),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppConstants.primaryColor.withValues(alpha: 0.2),
width: 1,
),
boxShadow: [
BoxShadow(
color: AppConstants.primaryColor.withValues(alpha: 0.1),
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<Color>(
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<Color>(
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
.withValues(alpha: 0.1),
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.withValues(
alpha: 0.1,
),
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<Color>(
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.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppConstants.infoColor.withValues(alpha: 0.2),
),
),
child: isLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
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.withValues(alpha: 0.3),
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.withValues(alpha: 0.3),
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)
.withValues(alpha: 0.3),
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<Color>(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.withValues(alpha: 0.05),
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)),
],
),
);
}
}