import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; import '../../constants/app_constants.dart'; import 'collect_notes.dart'; import '../../controllers/history_controller.dart'; import '../../services/network_listener_service.dart'; /// 时间: 2026-03-26 /// 功能: 本地笔记列表组件 /// 介绍: 展示用户笔记列表,支持置顶、锁定、删除等功能 /// 最新变化: 从 favorites_page.dart 独立出来,支持实时更新 class LocalNotesList extends StatefulWidget { const LocalNotesList({super.key}); @override State createState() => _LocalNotesListState(); } class _LocalNotesListState extends State { List> _notes = []; bool _isLoadingNotes = false; StreamSubscription? _networkSubscription; @override void initState() { super.initState(); _loadNotes(); _listenToNoteUpdates(); } @override void dispose() { _networkSubscription?.cancel(); super.dispose(); } void _listenToNoteUpdates() { _networkSubscription = NetworkListenerService().eventStream.listen((event) { if (event.type == NetworkEventType.noteUpdate) { _loadNotes(); } }); } // 加载笔记列表 Future _loadNotes() async { if (_isLoadingNotes) return; setState(() { _isLoadingNotes = true; }); try { final notes = await HistoryController.getNotes(); if (mounted) { setState(() { _notes = notes; _isLoadingNotes = false; }); } } catch (e) { print('加载笔记失败: $e'); if (mounted) { setState(() { _isLoadingNotes = false; }); } } } @override Widget build(BuildContext context) { if (_isLoadingNotes && _notes.isEmpty) { return const Center(child: CircularProgressIndicator()); } if (_notes.isEmpty) { return _buildEmptyNotes(); } return RefreshIndicator( onRefresh: _loadNotes, child: ListView.builder( padding: const EdgeInsets.fromLTRB(16, 16, 16, 80), itemCount: _notes.length + 1, itemBuilder: (context, index) { if (index == _notes.length) { return _buildBottomIndicator(); } final note = _notes[index]; return _buildNoteCard(note); }, ), ); } 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 _buildEmptyNotes() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.note_add_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 _buildNoteCard(Map note) { final title = note['title'] as String? ?? ''; final content = note['content'] as String? ?? ''; final timeStr = note['time'] as String? ?? ''; final createTimeStr = note['createTime'] 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: 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.08), blurRadius: 12, offset: const Offset(0, 4), ), BoxShadow( color: Colors.black.withValues(alpha: 0.04), blurRadius: 6, offset: const Offset(0, 2), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Stack( children: [ // 原始内容 InkWell( onTap: () => _handleNoteTap(note, isLocked), borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 顶部:创建时间、保存时间和置顶/锁定按钮 Row( children: [ // 创建时间 if (createTimeStr.isNotEmpty) ...[ Icon( Icons.add_circle_outline, size: 12, color: Colors.grey[400], ), const SizedBox(width: 2), Text( _formatDate(createTimeStr), style: TextStyle( fontSize: 10, color: Colors.grey[400], ), ), const SizedBox(width: 8), ], // 保存时间 Icon( Icons.access_time, size: 12, color: Colors.grey[400], ), const SizedBox(width: 2), Text( _formatDateTime(timeStr), style: TextStyle( fontSize: 10, color: Colors.grey[400], ), ), const Spacer(), // 分类标签 if (hasCategory) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: AppConstants.primaryColor.withValues( alpha: 0.1, ), borderRadius: BorderRadius.circular(10), ), child: Text( category, style: TextStyle( fontSize: 10, color: AppConstants.primaryColor, ), ), ), if (hasCategory) const SizedBox(width: 8), // 锁定图标 if (isLocked) Container( padding: const EdgeInsets.all(4), child: Icon( Icons.lock, size: 16, color: AppConstants.primaryColor, ), ), // 置顶按钮 GestureDetector( onTap: () => _togglePin(note['id'] as String?), child: Container( padding: const EdgeInsets.all(4), child: Icon( isPinned ? Icons.push_pin : Icons.push_pin_outlined, size: 16, color: isPinned ? AppConstants.primaryColor : Colors.grey[400], ), ), ), ], ), const SizedBox(height: 12), // 标题或内容 Text( displayText, style: TextStyle( fontSize: hasTitle ? 16 : 14, fontWeight: hasTitle ? FontWeight.w600 : FontWeight.normal, color: Colors.black87, height: 1.5, ), maxLines: 3, overflow: TextOverflow.ellipsis, ), // 底部:字数和删除按钮 const SizedBox(height: 12), Row( children: [ Text( '${note['charCount'] ?? displayText.length} 字', style: TextStyle( fontSize: 12, color: Colors.grey[400], ), ), const Spacer(), // 删除按钮 GestureDetector( onTap: () => _showDeleteNoteDialog(note['id'] as String?), child: Container( padding: const EdgeInsets.all(4), child: Icon( Icons.delete_outline, size: 18, color: Colors.grey[400], ), ), ), ], ), ], ), ), ), // 锁定时的毛玻璃遮罩 if (isLocked) Positioned.fill( child: GestureDetector( onTap: () => _handleNoteTap(note, isLocked), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8), child: Container( color: Colors.white.withValues(alpha: 0.3), child: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ // 顶部时间信息 Row( children: [ Icon( Icons.access_time, size: 12, color: Colors.grey[600], ), const SizedBox(width: 4), Text( _formatDateTime(timeStr), style: TextStyle( fontSize: 10, color: Colors.grey[600], ), ), const Spacer(), // 删除按钮 GestureDetector( onTap: () => _showDeleteNoteDialog( note['id'] as String?, ), child: Container( padding: const EdgeInsets.all(4), child: Icon( Icons.delete_outline, size: 16, color: Colors.grey[600], ), ), ), ], ), const Spacer(), // 中间锁定图标(左右结构) Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.lock, size: 28, color: AppConstants.primaryColor, ), const SizedBox(width: 12), Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '已锁定', style: TextStyle( fontSize: 14, color: AppConstants.primaryColor, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 2), Text( '点击输入密码访问', style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), ], ), ], ), const Spacer(), // 底部字数 Row( children: [ Text( '${note['charCount'] ?? displayText.length} 字', style: TextStyle( fontSize: 10, color: Colors.grey[600], ), ), ], ), ], ), ), ), ), ), ), ), ], ), ), ); } // 处理笔记点击 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((_) { _loadNotes(); }); } } // 显示密码输入对话框 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('确定'), ), ], ), ); } // 切换置顶状态 Future _togglePin(String? noteId) async { if (noteId == null) return; try { await HistoryController.togglePinNote(noteId); NetworkListenerService().sendSuccessEvent( NetworkEventType.noteUpdate, data: noteId, ); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('已更新置顶状态'))); } } catch (e) { print('切换置顶失败: $e'); } } // 格式化创建日期 String _formatDate(String timeStr) { if (timeStr.isEmpty) return ''; try { final dateTime = DateTime.parse(timeStr); return '${dateTime.month}-${dateTime.day}'; } catch (e) { return ''; } } // 格式化日期时间 String _formatDateTime(String timeStr) { if (timeStr.isEmpty) return ''; try { final dateTime = DateTime.parse(timeStr); final now = DateTime.now(); final difference = now.difference(dateTime); if (difference.inMinutes < 1) { return '刚刚'; } else if (difference.inMinutes < 60) { return '${difference.inMinutes}分钟前'; } else if (difference.inHours < 24) { return '${difference.inHours}小时前'; } else if (difference.inDays < 7) { return '${difference.inDays}天前'; } else if (difference.inDays < 30) { return '${(difference.inDays / 7).floor()}周前'; } else { return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')}'; } } catch (e) { return timeStr; } } // 显示删除笔记对话框 void _showDeleteNoteDialog(String? noteId) { if (noteId == null) return; showDialog( context: context, builder: (context) => AlertDialog( title: const Text('确认删除'), content: const Text('确定要删除这条笔记吗?'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('取消'), ), TextButton( onPressed: () async { Navigator.of(context).pop(); await _deleteNote(noteId); }, child: const Text('删除', style: TextStyle(color: Colors.red)), ), ], ), ); } // 删除笔记 Future _deleteNote(String noteId) async { try { await HistoryController.deleteNote(noteId); NetworkListenerService().sendSuccessEvent( NetworkEventType.noteUpdate, data: noteId, ); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('删除成功'))); } } catch (e) { if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('删除失败: $e'))); } } } }