import 'dart:ui'; import 'dart:async'; import 'package:flutter/material.dart'; import '../../constants/app_constants.dart'; import '../../controllers/history_controller.dart'; import '../../services/network_listener_service.dart'; import '../../utils/http/poetry_api.dart'; import 'collect_notes.dart'; import 'liked_poetry_manager.dart'; /// 时间: 2026-03-26 /// 功能: 全部收藏列表组件 /// 介绍: 同时展示点赞诗词和笔记,支持多种卡片类型 /// 最新变化: 新建组件,统一展示所有收藏内容 /// 卡片类型枚举 enum CardType { like, note } /// 统一卡片数据模型 class UnifiedCard { final CardType type; final dynamic data; final DateTime time; UnifiedCard({required this.type, required this.data, required this.time}); } class AllListPage extends StatefulWidget { const AllListPage({super.key}); @override State createState() => _AllListPageState(); } class _AllListPageState extends State { List _cards = []; bool _isLoading = false; StreamSubscription? _networkSubscription; @override void initState() { super.initState(); _loadAllData(); _listenToNetworkEvents(); } @override void dispose() { _networkSubscription?.cancel(); super.dispose(); } // 监听网络事件,实时更新列表 void _listenToNetworkEvents() { _networkSubscription = NetworkListenerService().eventStream.listen((event) { if (event.type == NetworkEventType.like || event.type == NetworkEventType.unlike || event.type == NetworkEventType.noteUpdate) { debugPrint('AllListPage: 收到事件 ${event.type},刷新列表'); _loadAllData(); } }); } // 加载所有数据 Future _loadAllData() async { if (_isLoading) return; setState(() { _isLoading = true; }); try { final List allCards = []; // 并行加载点赞和笔记数据 final results = await Future.wait([ HistoryController.getLikedHistory(), HistoryController.getNotes(), ]); // 处理点赞数据 final likedList = results[0]; for (final item in likedList) { try { final poetry = PoetryData.fromJson(item); final timeStr = item['time'] as String? ?? ''; allCards.add( UnifiedCard( type: CardType.like, data: poetry, time: DateTime.tryParse(timeStr) ?? DateTime.now(), ), ); } catch (e) { debugPrint('解析点赞数据失败: $e'); } } // 处理笔记数据 final notesList = results[1]; for (final note in notesList) { final timeStr = note['time'] as String? ?? ''; allCards.add( UnifiedCard( type: CardType.note, data: note, time: DateTime.tryParse(timeStr) ?? DateTime.now(), ), ); } // 按时间排序(最新的在前) allCards.sort((a, b) => b.time.compareTo(a.time)); if (mounted) { setState(() { _cards = allCards; _isLoading = false; }); } } catch (e) { debugPrint('加载全部数据失败: $e'); if (mounted) { setState(() { _isLoading = false; }); } } } @override Widget build(BuildContext context) { if (_isLoading && _cards.isEmpty) { return const Center(child: CircularProgressIndicator()); } if (_cards.isEmpty) { return _buildEmptyState(); } return RefreshIndicator( onRefresh: _loadAllData, child: ListView.builder( padding: const EdgeInsets.fromLTRB(16, 16, 16, 80), itemCount: _cards.length + 1, itemBuilder: (context, index) { if (index == _cards.length) { return _buildBottomIndicator(); } return _buildCard(_cards[index]); }, ), ); } Widget _buildBottomIndicator() { return Container( padding: const EdgeInsets.symmetric(vertical: 24), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Container(width: 40, height: 1, color: Colors.grey[300]), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( '到底了', style: TextStyle(fontSize: 12, color: Colors.grey[400]), ), ), Container(width: 40, height: 1, color: Colors.grey[300]), ], ), ); } // 构建空状态 Widget _buildEmptyState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.inbox_outlined, size: 64, color: Colors.grey[400]), const SizedBox(height: 16), Text( '暂无收藏内容', style: TextStyle(fontSize: 16, color: Colors.grey[600]), ), const SizedBox(height: 8), Text( '点赞诗词或创建笔记后将显示在这里', style: TextStyle(fontSize: 14, color: Colors.grey[500]), ), ], ), ); } // 根据类型构建卡片 - 使用策略模式 Widget _buildCard(UnifiedCard card) { switch (card.type) { case CardType.like: return _buildLikeCard(card.data as PoetryData); case CardType.note: return _buildNoteCard(card.data as Map); } } // 构建点赞卡片 - 简洁紧凑样式 Widget _buildLikeCard(PoetryData poetry) { return Container( margin: const EdgeInsets.only(bottom: 10), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all( color: AppConstants.primaryColor.withValues(alpha: 0.2), width: 1, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.03), blurRadius: 6, offset: const Offset(0, 1), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 顶部行:出处(左) + 标签(右) Padding( padding: const EdgeInsets.fromLTRB(10, 4, 0, 4), child: Row( children: [ // 出处 - 左上角 if (poetry.url.isNotEmpty) Expanded( child: Text( poetry.url, style: TextStyle(fontSize: 11, color: Colors.grey[600]), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), if (poetry.url.isEmpty) const Spacer(), // 类型标识 - 右上角 Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: AppConstants.primaryColor.withValues(alpha: 0.1), borderRadius: const BorderRadius.only( topRight: Radius.circular(10), bottomLeft: Radius.circular(10), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.favorite, size: 12, color: AppConstants.primaryColor, ), const SizedBox(width: 3), Text( '点赞诗词', style: TextStyle( fontSize: 11, color: AppConstants.primaryColor, fontWeight: FontWeight.w500, ), ), ], ), ), ], ), ), // 诗句内容 Padding( padding: const EdgeInsets.fromLTRB(10, 4, 10, 4), child: Text( poetry.name, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: Colors.black87, height: 1.3, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), // 操作按钮 Padding( padding: const EdgeInsets.fromLTRB(8, 0, 8, 6), child: Row( children: [ TextButton.icon( onPressed: () => _showPoetryDetails(poetry), icon: Icon( Icons.visibility, size: 14, color: AppConstants.primaryColor, ), label: Text( '查看详情', style: TextStyle( fontSize: 12, color: AppConstants.primaryColor, ), ), ), const Spacer(), TextButton.icon( onPressed: () => _createNoteFromPoetry(poetry), icon: Icon( Icons.note_add, size: 14, color: Colors.orange[700], ), label: Text( '创建笔记', style: TextStyle(fontSize: 12, color: Colors.orange[700]), ), ), TextButton.icon( onPressed: () => _removeLikedPoetry(poetry.id.toString()), icon: Icon( Icons.favorite_border, size: 14, color: Colors.grey[600], ), label: Text( '取消', style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), ), ], ), ), ], ), ); } // 构建笔记卡片 - 紧凑阴影样式 Widget _buildNoteCard(Map note) { final title = note['title'] as String? ?? ''; final content = note['content'] as String? ?? ''; final category = note['category'] as String? ?? ''; final isPinned = note['isPinned'] == true; final isLocked = note['isLocked'] == true; String displayText; bool hasTitle = title.isNotEmpty; bool hasCategory = category.isNotEmpty && category != '未分类'; if (hasTitle) { displayText = title; } else if (hasCategory) { displayText = '[$category]'; } else { displayText = content; } return Container( margin: const EdgeInsets.only(bottom: 10), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.06), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Stack( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 笔记内容 InkWell( onTap: () => _handleNoteTap(note, isLocked), borderRadius: BorderRadius.circular(10), child: Padding( padding: const EdgeInsets.fromLTRB(10, 28, 10, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( displayText, style: TextStyle( fontSize: hasTitle ? 14 : 13, fontWeight: hasTitle ? FontWeight.w600 : FontWeight.normal, color: Colors.black87, height: 1.4, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Row( children: [ Text( '${note['charCount'] ?? displayText.length} 字', style: TextStyle( fontSize: 11, color: Colors.grey[500], ), ), if (hasCategory) ...[ const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 1, ), decoration: BoxDecoration( color: Colors.grey[200], borderRadius: BorderRadius.circular(3), ), child: Text( category, style: TextStyle( fontSize: 9, color: Colors.grey[600], ), ), ), ], ], ), ], ), ), ), ], ), // 类型标识 - 右上角 Positioned( top: 0, right: 0, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.orange.withValues(alpha: 0.1), borderRadius: const BorderRadius.only( topRight: Radius.circular(10), bottomLeft: Radius.circular(10), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.note_outlined, size: 12, color: Colors.orange[700], ), const SizedBox(width: 3), Text( '笔记', style: TextStyle( fontSize: 11, color: Colors.orange[700], fontWeight: FontWeight.w500, ), ), if (isPinned) ...[ const SizedBox(width: 6), Icon(Icons.push_pin, size: 10, color: Colors.orange[700]), ], if (isLocked) ...[ const SizedBox(width: 6), Icon(Icons.lock, size: 10, color: Colors.orange[700]), ], ], ), ), ), // 锁定遮罩 if (isLocked) Positioned.fill( child: GestureDetector( onTap: () => _handleNoteTap(note, isLocked), child: ClipRRect( borderRadius: BorderRadius.circular(10), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), child: Container( color: Colors.white.withValues(alpha: 0.4), child: Center( child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.lock, size: 16, color: Colors.orange[700], ), const SizedBox(width: 6), Text( '已锁定', style: TextStyle( fontSize: 13, color: Colors.orange[700], fontWeight: FontWeight.w500, ), ), ], ), ), ), ), ), ), ), ], ), ); } // 显示诗词详情 - 调用公共方法 Future _showPoetryDetails(PoetryData poetry) async { await LikedPoetryManager.showPoetryDetails(context, poetry); } // 移除点赞 Future _removeLikedPoetry(String poetryId) async { try { await HistoryController.removeLikedPoetry(poetryId); await _loadAllData(); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('已取消点赞'))); } } catch (e) { debugPrint('取消点赞失败: $e'); } } // 从诗词创建笔记 Future _createNoteFromPoetry(PoetryData poetry) async { try { final title = poetry.url.isNotEmpty ? poetry.url : '诗词笔记'; final category = poetry.alias.isNotEmpty ? poetry.alias : '诗词'; final content = poetry.name; final noteId = await HistoryController.saveNote( title: title, content: content, category: category, ); if (noteId != null) { NetworkListenerService().sendSuccessEvent( NetworkEventType.noteUpdate, data: noteId, ); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('已创建笔记'))); } } } catch (e) { debugPrint('创建笔记失败: $e'); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('创建笔记失败: $e'))); } } } // 处理笔记点击 Future _handleNoteTap(Map note, bool isLocked) async { final noteId = note['id'] as String?; if (noteId == null) return; if (isLocked) { final password = await _showPasswordInputDialog(noteId); if (password == null) return; final isValid = await HistoryController.verifyNotePassword( noteId, password, ); if (!isValid) { if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('密码错误'))); } return; } } if (mounted) { Navigator.of(context) .push( MaterialPageRoute( builder: (_) => CollectNotesPage(noteId: noteId), ), ) .then((_) => _loadAllData()); } } // 密码输入对话框 Future _showPasswordInputDialog(String noteId) async { final TextEditingController passwordController = TextEditingController(); return showDialog( context: context, builder: (context) => AlertDialog( title: const Row( children: [ Icon(Icons.lock, size: 20), SizedBox(width: 8), Text('输入密码'), ], ), content: TextField( controller: passwordController, obscureText: true, autofocus: true, decoration: const InputDecoration( hintText: '请输入访问密码', border: OutlineInputBorder(), ), onSubmitted: (value) => Navigator.of(context).pop(value), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(null), child: const Text('取消'), ), TextButton( onPressed: () => Navigator.of(context).pop(passwordController.text), child: const Text('确定'), ), ], ), ); } }