Initial commit: Flutter 无书应用项目
This commit is contained in:
337
lib/views/home/home-load.dart
Normal file
337
lib/views/home/home-load.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
279
lib/views/home/home_components.dart
Normal file
279
lib/views/home/home_components.dart
Normal 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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
806
lib/views/home/home_page.dart
Normal file
806
lib/views/home/home_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
868
lib/views/home/home_part.dart
Normal file
868
lib/views/home/home_part.dart
Normal 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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user