import 'dart:async'; import 'package:flutter/material.dart'; import '../../constants/app_constants.dart'; import '../../controllers/history_controller.dart'; import '../../services/network_listener_service.dart'; /// 时间: 2026-03-26 /// 功能: 笔记编辑页面 /// 介绍: 支持创建和编辑笔记,实时保存到本地存储 /// 最新变化: 实时保存,显示保存时间和字数,支持置顶、密码锁定和分类 class CollectNotesPage extends StatefulWidget { final String? noteId; const CollectNotesPage({super.key, this.noteId}); @override State createState() => _CollectNotesPageState(); } class _CollectNotesPageState extends State { final TextEditingController _titleController = TextEditingController(); final TextEditingController _contentController = TextEditingController(); final FocusNode _titleFocusNode = FocusNode(); final FocusNode _contentFocusNode = FocusNode(); String? _currentNoteId; DateTime? _lastSavedTime; DateTime? _createTime; int _charCount = 0; bool _isLoading = true; bool _isPinned = false; bool _isLocked = false; String? _password; String? _category; Timer? _debounceTimer; // 分类选项 final List _categoryOptions = ['未分类', '工作', '学习', '生活', '灵感', '待办']; // 自定义分类标记 static const String _customCategoryKey = '__custom__'; @override void initState() { super.initState(); _currentNoteId = widget.noteId; _loadNote(); _titleController.addListener(_onContentChanged); _contentController.addListener(_onContentChanged); // 监听焦点变化以更新边框颜色 _titleFocusNode.addListener(_onFocusChange); _contentFocusNode.addListener(_onFocusChange); } void _onFocusChange() { setState(() {}); } @override void dispose() { _debounceTimer?.cancel(); _titleController.removeListener(_onContentChanged); _contentController.removeListener(_onContentChanged); _titleFocusNode.removeListener(_onFocusChange); _contentFocusNode.removeListener(_onFocusChange); _titleController.dispose(); _contentController.dispose(); _titleFocusNode.dispose(); _contentFocusNode.dispose(); super.dispose(); } // 加载笔记数据 Future _loadNote() async { if (_currentNoteId == null) { setState(() { _isLoading = false; _category = _categoryOptions[0]; }); return; } try { final note = await HistoryController.getNote(_currentNoteId!); if (note != null && note.isNotEmpty) { _titleController.text = note['title'] ?? ''; _contentController.text = note['content'] ?? ''; _lastSavedTime = DateTime.tryParse(note['time'] ?? ''); _createTime = DateTime.tryParse(note['createTime'] ?? ''); _charCount = note['charCount'] ?? (_titleController.text.length + _contentController.text.length); _isPinned = note['isPinned'] ?? false; _isLocked = note['isLocked'] ?? false; _password = note['password']; _category = note['category'] ?? _categoryOptions[0]; } } catch (e) { // 加载失败 } setState(() { _isLoading = false; }); } // 内容变化时触发防抖保存 void _onContentChanged() { final newCharCount = _titleController.text.length + _contentController.text.length; if (newCharCount != _charCount) { setState(() { _charCount = newCharCount; }); } _debounceTimer?.cancel(); _debounceTimer = Timer(const Duration(milliseconds: 500), _saveNote); } // 保存笔记 Future _saveNote() async { final title = _titleController.text.trim(); final content = _contentController.text.trim(); if (title.isEmpty && content.isEmpty) { return; } try { final noteId = await HistoryController.saveNote( noteId: _currentNoteId, title: title, content: content, isPinned: _isPinned, isLocked: _isLocked, password: _password, category: _category, createTime: _createTime?.toIso8601String(), ); if (noteId != null) { _currentNoteId = noteId; _lastSavedTime = DateTime.now(); if (_createTime == null) { _createTime = _lastSavedTime; } setState(() {}); NetworkListenerService().sendSuccessEvent( NetworkEventType.noteUpdate, data: noteId, ); } } catch (e) { // 保存失败 } } // 切换置顶状态 Future _togglePin() async { if (_currentNoteId == null) { final title = _titleController.text.trim(); final content = _contentController.text.trim(); if (title.isEmpty && content.isEmpty) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('请先输入内容'))); return; } await _saveNote(); } setState(() { _isPinned = !_isPinned; }); await _saveNote(); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(_isPinned ? '已置顶' : '已取消置顶'))); } } // 验证密码格式(只能数字和字母) bool _isValidPassword(String password) { final regex = RegExp(r'^[a-zA-Z0-9]+$'); return regex.hasMatch(password); } // 显示密码设置对话框 Future _showPasswordDialog() async { if (_password != null && _password!.isNotEmpty) { final action = await showDialog( context: context, builder: (context) => AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), title: Row( children: [ Icon(Icons.lock, color: AppConstants.primaryColor, size: 24), const SizedBox(width: 8), const Text('锁定设置'), ], ), content: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.vpn_key, size: 16, color: Colors.grey[600]), const SizedBox(width: 8), Text( '当前密码: $_password', style: TextStyle( color: Colors.grey[700], fontSize: 13, fontWeight: FontWeight.w500, ), ), ], ), ), const SizedBox(height: 20), // 取消锁定选项 InkWell( onTap: () => Navigator.of(context).pop('remove'), borderRadius: BorderRadius.circular(12), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric( vertical: 14, horizontal: 16, ), decoration: BoxDecoration( color: Colors.red[50], borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.red[200]!), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.lock_open, color: Colors.red[400], size: 22), const SizedBox(width: 10), Text( '取消锁定', style: TextStyle( color: Colors.red[400], fontSize: 15, fontWeight: FontWeight.w600, ), ), ], ), ), ), const SizedBox(height: 12), // 修改密码选项 InkWell( onTap: () => Navigator.of(context).pop('modify'), borderRadius: BorderRadius.circular(12), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric( vertical: 14, horizontal: 16, ), decoration: BoxDecoration( color: AppConstants.primaryColor.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all( color: AppConstants.primaryColor.withOpacity(0.3), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.edit, color: AppConstants.primaryColor, size: 22, ), const SizedBox(width: 10), Text( '修改密码', style: TextStyle( color: AppConstants.primaryColor, fontSize: 15, fontWeight: FontWeight.w600, ), ), ], ), ), ), ], ), ), ); if (action == null) return; if (action == 'remove') { await _removePassword(); return; } if (action == 'modify') { await _showModifyPasswordDialog(); return; } } else { await _showSetPasswordDialog(); } } // 显示修改密码对话框 Future _showModifyPasswordDialog() async { final TextEditingController passwordController = TextEditingController(); final TextEditingController confirmPasswordController = TextEditingController(); await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('修改密码'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: passwordController, obscureText: true, decoration: const InputDecoration( labelText: '新密码', hintText: '只能输入数字和字母', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), TextField( controller: confirmPasswordController, obscureText: true, decoration: const InputDecoration( labelText: '确认密码', hintText: '再次输入新密码', border: OutlineInputBorder(), ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('取消'), ), TextButton( onPressed: () async { if (passwordController.text.isEmpty) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('密码不能为空'))); return; } if (!_isValidPassword(passwordController.text)) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('密码只能包含数字和字母'))); return; } if (passwordController.text != confirmPasswordController.text) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('两次密码不一致'))); return; } Navigator.of(context).pop(); await _setPassword(passwordController.text); }, child: const Text('确定'), ), ], ), ); } // 显示设置密码对话框 Future _showSetPasswordDialog() async { final TextEditingController passwordController = TextEditingController(); final TextEditingController confirmPasswordController = TextEditingController(); await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('设置密码'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: passwordController, obscureText: true, decoration: const InputDecoration( labelText: '密码', hintText: '只能输入数字和字母', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), TextField( controller: confirmPasswordController, obscureText: true, decoration: const InputDecoration( labelText: '确认密码', hintText: '再次输入密码', border: OutlineInputBorder(), ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('取消'), ), TextButton( onPressed: () async { if (passwordController.text.isEmpty) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('密码不能为空'))); return; } if (!_isValidPassword(passwordController.text)) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('密码只能包含数字和字母'))); return; } if (passwordController.text != confirmPasswordController.text) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('两次密码不一致'))); return; } Navigator.of(context).pop(); await _setPassword(passwordController.text); }, child: const Text('确定'), ), ], ), ); } // 设置密码 Future _setPassword(String password) async { if (_currentNoteId == null) { final title = _titleController.text.trim(); final content = _contentController.text.trim(); if (title.isEmpty && content.isEmpty) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('请先输入内容'))); return; } await _saveNote(); } setState(() { _isLocked = true; _password = password; }); await _saveNote(); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('密码设置成功,笔记已锁定'))); } } // 移除密码 Future _removePassword() async { if (_currentNoteId == null) return; setState(() { _isLocked = false; _password = null; }); await _saveNote(); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('已取消锁定'))); } } // 显示删除确认对话框 Future _showDeleteDialog() async { if (_currentNoteId == null) return; final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('删除笔记'), content: const Text('确定要删除这条笔记吗?此操作不可撤销。'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('取消'), ), TextButton( onPressed: () => Navigator.of(context).pop(true), child: Text('删除', style: TextStyle(color: Colors.red[400])), ), ], ), ); if (confirmed == true) { await HistoryController.deleteNote(_currentNoteId!); NetworkListenerService().sendSuccessEvent( NetworkEventType.noteUpdate, data: _currentNoteId, ); if (mounted) { Navigator.of(context).pop(); ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('笔记已删除'))); } } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( backgroundColor: Colors.white, elevation: 0, leading: IconButton( icon: Icon(Icons.arrow_back, color: AppConstants.primaryColor), onPressed: () => Navigator.of(context).pop(), ), title: Text( _currentNoteId == null ? '新建笔记' : '编辑笔记', style: const TextStyle( color: Colors.black87, fontWeight: FontWeight.w600, ), ), actions: [ // 删除按钮(仅编辑时显示) if (_currentNoteId != null) IconButton( icon: Icon(Icons.delete_outline, color: Colors.red[400]), onPressed: _showDeleteDialog, tooltip: '删除笔记', ), // 锁定按钮 IconButton( icon: Icon( _isLocked ? Icons.lock : Icons.lock_outline, color: _isLocked ? AppConstants.primaryColor : Colors.grey[600], ), onPressed: _showPasswordDialog, tooltip: _isLocked ? '修改密码' : '设置密码', ), // 置顶按钮 IconButton( icon: Icon( _isPinned ? Icons.push_pin : Icons.push_pin_outlined, color: _isPinned ? AppConstants.primaryColor : Colors.grey[600], ), onPressed: _togglePin, tooltip: _isPinned ? '取消置顶' : '置顶', ), ], ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : GestureDetector( onTap: () { // 点击非输入框区域取消焦点 _titleFocusNode.unfocus(); _contentFocusNode.unfocus(); }, child: Column( children: [ _buildStatusBar(), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ _buildTitleInput(), const SizedBox(height: 16), _buildContentInput(), ], ), ), ), ], ), ), ); } Widget _buildStatusBar() { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), color: Colors.grey[50], child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ // 创建时间 Icon(Icons.add_circle_outline, size: 14, color: Colors.grey[500]), const SizedBox(width: 4), Text( _createTime != null ? '创建 ${_formatDate(_createTime!)}' : '新笔记', style: TextStyle(fontSize: 12, color: Colors.grey[500]), ), const SizedBox(width: 16), // 保存时间 Icon(Icons.access_time, size: 14, color: Colors.grey[500]), const SizedBox(width: 4), Text( _lastSavedTime != null ? '保存 ${_formatDateTime(_lastSavedTime!)}' : '未保存', style: TextStyle(fontSize: 12, color: Colors.grey[500]), ), const SizedBox(width: 16), // 字数 Icon(Icons.text_fields, size: 14, color: Colors.grey[500]), const SizedBox(width: 4), Text( '$_charCount 字', style: TextStyle(fontSize: 12, color: Colors.grey[500]), ), if (_isPinned) ...[ const SizedBox(width: 16), Icon(Icons.push_pin, size: 14, color: AppConstants.primaryColor), const SizedBox(width: 4), Text( '已置顶', style: TextStyle( fontSize: 12, color: AppConstants.primaryColor, ), ), ], if (_isLocked) ...[ const SizedBox(width: 16), Icon(Icons.lock, size: 14, color: AppConstants.primaryColor), const SizedBox(width: 4), Text( '已锁定', style: TextStyle( fontSize: 12, color: AppConstants.primaryColor, ), ), ], ], ), ), ); } // 显示自定义分类对话框 Future _showCustomCategoryDialog() async { final TextEditingController customCategoryController = TextEditingController(); // 如果当前分类不在预设选项中,显示当前分类 if (_category != null && !_categoryOptions.contains(_category) && _category!.isNotEmpty) { customCategoryController.text = _category!; } await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('自定义分类'), content: TextField( controller: customCategoryController, autofocus: true, decoration: const InputDecoration( labelText: '分类名称', hintText: '输入自定义分类名称', border: OutlineInputBorder(), ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('取消'), ), TextButton( onPressed: () { final customCategory = customCategoryController.text.trim(); if (customCategory.isEmpty) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('分类名称不能为空'))); return; } Navigator.of(context).pop(); setState(() { _category = customCategory; }); _saveNote(); }, child: const Text('确定'), ), ], ), ); } Widget _buildTitleInput() { return Row( children: [ Expanded( child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all( color: _titleFocusNode.hasFocus ? AppConstants.primaryColor.withValues(alpha: 0.5) : Colors.grey.withValues(alpha: 0.2), width: 1.5, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: TextField( controller: _titleController, focusNode: _titleFocusNode, maxLines: 1, decoration: InputDecoration( hintText: '标题(可选)', hintStyle: TextStyle(color: Colors.grey[400]), border: InputBorder.none, contentPadding: const EdgeInsets.all(16), ), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), ), ), ), const SizedBox(width: 12), // 分类选择 Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: PopupMenuButton( initialValue: (_category != null && _categoryOptions.contains(_category)) ? _category : null, onSelected: (value) async { if (value == _customCategoryKey) { // 弹出自定义分类输入框 await _showCustomCategoryDialog(); } else { setState(() { _category = value; }); _saveNote(); } }, itemBuilder: (context) { final items = _categoryOptions.map((category) { return PopupMenuItem( value: category, child: Row( children: [ if (category == _category) Icon( Icons.check, size: 16, color: AppConstants.primaryColor, ), if (category == _category) const SizedBox(width: 8), Text(category), ], ), ); }).toList(); // 添加自定义分类选项 items.add( PopupMenuItem( value: _customCategoryKey, child: Row( children: [ if (!_categoryOptions.contains(_category) && _category != null && _category!.isNotEmpty) Icon( Icons.check, size: 16, color: AppConstants.primaryColor, ), if (!_categoryOptions.contains(_category) && _category != null && _category!.isNotEmpty) const SizedBox(width: 8), Icon(Icons.edit, size: 16, color: Colors.grey[600]), const SizedBox(width: 8), Text('自定义', style: TextStyle(color: Colors.grey[600])), ], ), ), ); return items; }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.folder_outlined, size: 18, color: AppConstants.primaryColor, ), const SizedBox(width: 4), Text( _category ?? '分类', style: TextStyle( fontSize: 14, color: AppConstants.primaryColor, ), ), Icon( Icons.arrow_drop_down, size: 18, color: Colors.grey[600], ), ], ), ), ), ), ], ); } Widget _buildContentInput() { return Container( constraints: const BoxConstraints(minHeight: 300), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all( color: _contentFocusNode.hasFocus ? AppConstants.primaryColor.withValues(alpha: 0.5) : Colors.grey.withValues(alpha: 0.2), width: 1.5, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: TextField( controller: _contentController, focusNode: _contentFocusNode, maxLines: null, minLines: 12, decoration: InputDecoration( hintText: '开始写笔记...', hintStyle: TextStyle(color: Colors.grey[400]), border: InputBorder.none, contentPadding: const EdgeInsets.all(16), ), style: const TextStyle(fontSize: 16, height: 1.5), ), ); } String _formatDate(DateTime dateTime) { return '${dateTime.month}-${dateTime.day}'; } String _formatDateTime(DateTime dateTime) { final now = DateTime.now(); final difference = now.difference(dateTime); if (difference.inSeconds < 10) { return '刚刚'; } else if (difference.inMinutes < 1) { return '${difference.inSeconds}秒前'; } else if (difference.inMinutes < 60) { return '${difference.inMinutes}分钟前'; } else if (difference.inHours < 24) { return '${difference.inHours}小时前'; } else { return '${dateTime.month}-${dateTime.day} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}'; } } }