diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/models/data_models.dart b/lib/models/data_models.dart index 5fbf20a..2542b9d 100644 --- a/lib/models/data_models.dart +++ b/lib/models/data_models.dart @@ -89,8 +89,8 @@ class Movie { return File(posterPath!); } - /// 解析字符串列表 - static List _parseStringList(dynamic data) { + /// 解析字符串列表(公共静态方法,供Book使用) + static List parseStringList(dynamic data) { if (data == null) return []; if (data is List) { return data.map((e) => e.toString()).toList(); @@ -109,6 +109,9 @@ class Movie { } return []; } + + /// 解析字符串列表(私有别名,保持兼容性) + static List _parseStringList(dynamic data) => parseStringList(data); /// 复制并修改 Movie copyWith({ @@ -151,37 +154,54 @@ class Movie { /// 书籍条目模型 class Book { final String id; - final String title; - final String? author; - final String? cover; - final double? rating; - final String status; // 'read', 'reading', 'want_to_read' - final DateTime? readDate; - final String? note; + final String title; // 书籍名称 + final String? coverPath; // 本地封面路径 + final List authors; // 作者列表 + final List alternateTitles; // 别名 + final String? publisher; // 出版社 + final List genres; // 类型 + final String? summary; // 书籍简介 + final double? rating; // 评分 1-10 + final String status; // read/reading/want_to_read + final DateTime createdAt; + final DateTime updatedAt; + final bool isDeleted; Book({ required this.id, required this.title, - this.author, - this.cover, + this.coverPath, + this.authors = const [], + this.alternateTitles = const [], + this.publisher, + this.genres = const [], + this.summary, this.rating, required this.status, - this.readDate, - this.note, + required this.createdAt, + required this.updatedAt, + this.isDeleted = false, }); factory Book.fromJson(Map json) { return Book( id: json['id'] ?? '', title: json['title'] ?? '', - author: json['author'], - cover: json['cover'], + coverPath: json['cover_path'], + authors: Movie.parseStringList(json['authors']), + alternateTitles: Movie.parseStringList(json['alternate_titles']), + publisher: json['publisher'], + genres: Movie.parseStringList(json['genres']), + summary: json['summary'], rating: json['rating']?.toDouble(), status: json['status'] ?? 'want_to_read', - readDate: json['read_date'] != null - ? DateTime.parse(json['read_date']) - : null, - note: json['note'], + createdAt: json['created_at'] != null + ? DateTime.parse(json['created_at']) + : DateTime.now(), + updatedAt: json['updated_at'] != null + ? DateTime.parse(json['updated_at']) + : DateTime.now(), + isDeleted: json['is_deleted'] == 1 || json['is_deleted'] == true, ); } @@ -189,29 +209,73 @@ class Book { return { 'id': id, 'title': title, - 'author': author, - 'cover': cover, + 'cover_path': coverPath, + 'authors': jsonEncode(authors), + 'alternate_titles': jsonEncode(alternateTitles), + 'publisher': publisher, + 'genres': jsonEncode(genres), + 'summary': summary, 'rating': rating, 'status': status, - 'read_date': readDate?.toIso8601String(), - 'note': note, + 'created_at': createdAt.toIso8601String(), + 'updated_at': updatedAt.toIso8601String(), + 'is_deleted': isDeleted ? 1 : 0, }; } + + /// 获取封面文件 + File? get coverFile { + if (coverPath == null || coverPath!.isEmpty) return null; + return File(coverPath!); + } + + /// 复制并修改 + Book copyWith({ + String? id, + String? title, + String? coverPath, + List? authors, + List? alternateTitles, + String? publisher, + List? genres, + String? summary, + double? rating, + String? status, + DateTime? createdAt, + DateTime? updatedAt, + bool? isDeleted, + }) { + return Book( + id: id ?? this.id, + title: title ?? this.title, + coverPath: coverPath ?? this.coverPath, + authors: authors ?? this.authors, + alternateTitles: alternateTitles ?? this.alternateTitles, + publisher: publisher ?? this.publisher, + genres: genres ?? this.genres, + summary: summary ?? this.summary, + rating: rating ?? this.rating, + status: status ?? this.status, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + isDeleted: isDeleted ?? this.isDeleted, + ); + } } /// 笔记模型 class Note { final String id; - final String title; final String content; + final String contentType; // markdown / rich_text final List tags; final DateTime createdAt; final DateTime updatedAt; Note({ required this.id, - required this.title, required this.content, + this.contentType = 'markdown', this.tags = const [], required this.createdAt, required this.updatedAt, @@ -219,12 +283,10 @@ class Note { factory Note.fromJson(Map json) { return Note( - id: json['id'] ?? '', - title: json['title'] ?? '', + id: json['id']?.toString() ?? '', content: json['content'] ?? '', - tags: json['tags'] != null - ? List.from(json['tags']) - : [], + contentType: json['content_type'] ?? 'markdown', + tags: Movie.parseStringList(json['tags']), createdAt: json['created_at'] != null ? DateTime.parse(json['created_at']) : DateTime.now(), @@ -237,12 +299,37 @@ class Note { Map toJson() { return { 'id': id, - 'title': title, 'content': content, - 'tags': tags, + 'content_type': contentType, + 'tags': jsonEncode(tags), 'created_at': createdAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(), }; } + + /// 复制并修改 + Note copyWith({ + String? id, + String? content, + String? contentType, + List? tags, + DateTime? createdAt, + DateTime? updatedAt, + }) { + return Note( + id: id ?? this.id, + content: content ?? this.content, + contentType: contentType ?? this.contentType, + tags: tags ?? this.tags, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + } + + /// 获取内容摘要(前100字) + String get summary { + if (content.length <= 100) return content; + return '${content.substring(0, 100)}...'; + } } diff --git a/lib/models/data_models_extension.dart b/lib/models/data_models_extension.dart new file mode 100644 index 0000000..0b131a6 --- /dev/null +++ b/lib/models/data_models_extension.dart @@ -0,0 +1,78 @@ +/// 数据模型扩展 - 添加 copyWith 方法以便更新数据 +library; + +import 'data_models.dart'; + +/// Movie 扩展 - 添加 copyWith 方法 +extension MovieExtension on Movie { + /// 创建副本并允许修改部分属性 + Movie copyWith({ + String? id, + String? title, + String? poster, + double? rating, + int? year, + String? status, + DateTime? watchDate, + String? note, + }) { + return Movie( + id: id ?? this.id, + title: title ?? this.title, + poster: poster ?? this.poster, + rating: rating ?? this.rating, + year: year ?? this.year, + status: status ?? this.status, + watchDate: watchDate ?? this.watchDate, + note: note ?? this.note, + ); + } +} + +/// Book 扩展 - 添加 copyWith 方法 +extension BookExtension on Book { + /// 创建副本并允许修改部分属性 + Book copyWith({ + String? id, + String? title, + String? author, + String? cover, + double? rating, + String? status, + DateTime? readDate, + String? note, + }) { + return Book( + id: id ?? this.id, + title: title ?? this.title, + author: author ?? this.author, + cover: cover ?? this.cover, + rating: rating ?? this.rating, + status: status ?? this.status, + readDate: readDate ?? this.readDate, + note: note ?? this.note, + ); + } +} + +/// Note 扩展 - 添加 copyWith 方法 +extension NoteExtension on Note { + /// 创建副本并允许修改部分属性 + Note copyWith({ + String? id, + String? title, + String? content, + List? tags, + DateTime? createdAt, + DateTime? updatedAt, + }) { + return Note( + id: id ?? this.id, + title: title ?? this.title, + content: content ?? this.content, + tags: tags ?? this.tags, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + } +} diff --git a/lib/pages/book_detail_page.dart b/lib/pages/book_detail_page.dart index 3c21af4..cf5b7ab 100644 --- a/lib/pages/book_detail_page.dart +++ b/lib/pages/book_detail_page.dart @@ -1,272 +1,474 @@ +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; import '../models/data_models.dart'; -import '../utils/app_theme.dart'; -/// 书籍详情页 +/// 书籍详情页 - 极简主义设计 class BookDetailPage extends StatefulWidget { final Book book; - + const BookDetailPage({super.key, required this.book}); - + @override State createState() => _BookDetailPageState(); } class _BookDetailPageState extends State { - @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: Text(widget.book.title), - actions: [ - IconButton( - icon: const Icon(Icons.edit), - onPressed: () => _navigateToEdit(context), - ), - IconButton( - icon: const Icon(Icons.delete_outline), - onPressed: () => _showDeleteDialog(context), - ), - ], - ), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 封面区域 - _buildCoverSection(context), - - // 基本信息 - _buildInfoSection(context), - - // 笔记区域 - if (widget.book.note != null && widget.book.note!.isNotEmpty) - _buildNoteSection(context), - ], - ), - ), - ); - } - - /// 构建封面区域 - Widget _buildCoverSection(BuildContext context) { - return Container( - width: double.infinity, - height: 250, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Theme.of(context).colorScheme.primaryContainer, - Theme.of(context).colorScheme.surface, - ], - ), - ), - child: Stack( - children: [ - Center( + backgroundColor: Colors.white, + body: CustomScrollView( + slivers: [ + // 顶部封面区域 + _buildSliverAppBar(), + + // 内容区域 + SliverToBoxAdapter( child: Column( - mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - Icons.menu_book, - size: 80, - color: Theme.of(context).colorScheme.primary.withOpacity(0.6), - ), - const SizedBox(height: 16), - Text( - widget.book.title, - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurface, - ), - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), + // 基本信息 + _buildBasicInfo(), + + const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)), + + // 作者信息 + _buildAuthorsSection(), + + // 出版社 + if (widget.book.publisher != null && widget.book.publisher!.isNotEmpty) + _buildPublisherSection(), + + // 类型 + if (widget.book.genres.isNotEmpty) + _buildGenresSection(), + + const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)), + + // 简介 + if (widget.book.summary != null && widget.book.summary!.isNotEmpty) + _buildSummarySection(), + + // 别名 + if (widget.book.alternateTitles.isNotEmpty) + _buildAlternateTitlesSection(), + + const SizedBox(height: 48), ], ), ), - Positioned( - top: 16, - right: 16, - child: _buildStatusTag(context), - ), ], ), + + // 底部操作栏 + bottomNavigationBar: _buildBottomBar(), ); } - - /// 构建状态标签 - Widget _buildStatusTag(BuildContext context) { - Color statusColor; - String statusText; - - switch (widget.book.status) { - case 'read': - statusColor = AppTheme.readColor; - statusText = '读完'; - break; - case 'reading': - statusColor = AppTheme.readingColor; - statusText = '在读'; - break; - case 'want_to_read': - statusColor = AppTheme.wantToReadColor; - statusText = '准备读'; - break; - default: - statusColor = Colors.grey; - statusText = '未知'; - } - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: statusColor.withOpacity(0.9), - borderRadius: BorderRadius.circular(20), + + /// 构建顶部 AppBar + Widget _buildSliverAppBar() { + return SliverAppBar( + expandedHeight: 280, + pinned: true, + backgroundColor: Colors.white, + flexibleSpace: FlexibleSpaceBar( + background: _buildCoverSection(), ), - child: Text( - statusText, - style: const TextStyle( - fontSize: 14, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ); - } - - /// 构建信息区域 - Widget _buildInfoSection(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 作者 - if (widget.book.author != null && widget.book.author!.isNotEmpty) ...[ - _buildInfoItem( - context, - icon: Icons.person, - label: widget.book.author!, - ), - const SizedBox(height: 12), - ], - - // 评分 - if (widget.book.rating != null) ...[ - _buildInfoItem( - context, - icon: Icons.star, - label: widget.book.rating.toString(), - iconColor: Colors.amber[700], - textColor: Colors.amber[700], - ), - const SizedBox(height: 12), - ], - - // 阅读日期 - if (widget.book.readDate != null) - _buildInfoItem( - context, - icon: Icons.event, - label: '阅读日期:${_formatDate(widget.book.readDate!)}', - ), - ], - ), - ); - } - - /// 构建信息项 - Widget _buildInfoItem( - BuildContext context, { - required IconData icon, - required String label, - Color? iconColor, - Color? textColor, - }) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 18, color: iconColor), - const SizedBox(width: 4), - Text( - label, - style: TextStyle( - fontSize: 14, - color: textColor ?? Theme.of(context).colorScheme.onSurfaceVariant, - ), + actions: [ + IconButton( + icon: const Icon(Icons.edit_outlined), + onPressed: () => _navigateToEdit(context), ), + const SizedBox(width: 8), ], ); } - - /// 构建笔记区域 - Widget _buildNoteSection(BuildContext context) { + + /// 构建封面区域 + Widget _buildCoverSection() { return Container( - margin: const EdgeInsets.all(16), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), - ), + width: double.infinity, + color: const Color(0xFFF5F5F5), + child: widget.book.coverPath != null && widget.book.coverPath!.isNotEmpty + ? Image.file( + File(widget.book.coverPath!), + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => _buildCoverPlaceholder(), + ) + : _buildCoverPlaceholder(), + ); + } + + Widget _buildCoverPlaceholder() { + return const Center( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ - Row( - children: [ - Icon( - Icons.edit_note, - size: 20, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 8), - Text( - '笔记', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ], + Icon( + Icons.menu_book, + size: 64, + color: Color(0xFFCCCCCC), ), - const SizedBox(height: 12), + SizedBox(height: 16), Text( - widget.book.note!, - style: Theme.of(context).textTheme.bodyMedium, + '暂无封面', + style: TextStyle( + fontSize: 14, + color: Color(0xFF999999), + ), ), ], ), ); } - + + /// 构建基本信息 + Widget _buildBasicInfo() { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 书名 + Text( + widget.book.title, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w600, + color: Color(0xFF1A1A1A), + height: 1.3, + ), + ), + + const SizedBox(height: 16), + + // 评分和状态 + Row( + children: [ + if (widget.book.rating != null) ...[ + const Icon( + Icons.star, + size: 20, + color: Color(0xFF1A1A1A), + ), + const SizedBox(width: 4), + Text( + widget.book.rating!.toStringAsFixed(1), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Color(0xFF1A1A1A), + ), + ), + const SizedBox(width: 16), + ], + _buildStatusTag(), + ], + ), + + const SizedBox(height: 8), + + // 时间信息 + Text( + '添加于 ${_formatDate(widget.book.createdAt)}', + style: const TextStyle( + fontSize: 12, + color: Color(0xFF999999), + ), + ), + ], + ), + ); + } + + /// 构建状态标签 + Widget _buildStatusTag() { + String label; + Color color; + switch (widget.book.status) { + case 'read': + label = '已读'; + color = const Color(0xFF1A1A1A); + break; + case 'reading': + label = '在读'; + color = const Color(0xFF666666); + break; + case 'want_to_read': + label = '想读'; + color = const Color(0xFF999999); + break; + default: + label = '未知'; + color = const Color(0xFFCCCCCC); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: color, + ), + child: Text( + label, + style: const TextStyle( + fontSize: 12, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ); + } + + /// 构建作者区域 + Widget _buildAuthorsSection() { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '作者', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF999999), + letterSpacing: 1, + ), + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: widget.book.authors.map((author) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + border: Border.all(color: const Color(0xFFE5E5E5)), + ), + child: Text( + author, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF1A1A1A), + ), + ), + ); + }).toList(), + ), + ], + ), + ); + } + + /// 构建出版社区域 + Widget _buildPublisherSection() { + return Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '出版社', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF999999), + letterSpacing: 1, + ), + ), + const SizedBox(height: 8), + Text( + widget.book.publisher!, + style: const TextStyle( + fontSize: 15, + color: Color(0xFF1A1A1A), + ), + ), + ], + ), + ); + } + + /// 构建类型区域 + Widget _buildGenresSection() { + return Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '类型', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF999999), + letterSpacing: 1, + ), + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: widget.book.genres.map((genre) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + border: Border.all(color: const Color(0xFFE5E5E5)), + ), + child: Text( + genre, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF666666), + ), + ), + ); + }).toList(), + ), + ], + ), + ); + } + + /// 构建简介区域 + Widget _buildSummarySection() { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '简介', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF999999), + letterSpacing: 1, + ), + ), + const SizedBox(height: 12), + Text( + widget.book.summary!, + style: const TextStyle( + fontSize: 15, + color: Color(0xFF1A1A1A), + height: 1.6, + ), + ), + ], + ), + ); + } + + /// 构建别名区域 + Widget _buildAlternateTitlesSection() { + return Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '别名', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF999999), + letterSpacing: 1, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: widget.book.alternateTitles.map((title) { + return Text( + title, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF666666), + ), + ); + }).toList(), + ), + ], + ), + ); + } + + /// 构建底部操作栏 + Widget _buildBottomBar() { + return Container( + decoration: const BoxDecoration( + border: Border( + top: BorderSide(color: Color(0xFFE5E5E5), width: 0.5), + ), + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => _navigateToEdit(context), + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFF1A1A1A), + side: const BorderSide(color: Color(0xFF1A1A1A)), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + child: const Text('编辑'), + ), + ), + const SizedBox(width: 16), + Expanded( + child: OutlinedButton( + onPressed: () => _showDeleteDialog(context), + style: OutlinedButton.styleFrom( + foregroundColor: Colors.red, + side: const BorderSide(color: Colors.red), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + child: const Text('删除'), + ), + ), + ], + ), + ), + ), + ); + } + /// 格式化日期 String _formatDate(DateTime date) { return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; } - + /// 跳转到编辑页面 void _navigateToEdit(BuildContext context) { Navigator.pushNamed(context, '/book-form', arguments: widget.book).then((_) { context.read().loadBooks(); }); } - + /// 显示删除对话框 void _showDeleteDialog(BuildContext context) { showDialog( context: context, builder: (context) => AlertDialog( + backgroundColor: Colors.white, + elevation: 0, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), title: const Text('确认删除'), - content: Text('确定要删除"${widget.book.title}"吗?此操作不可恢复。'), + content: Text('确定要删除"${widget.book.title}"吗?'), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('取消'), + child: const Text('取消', style: TextStyle(color: Color(0xFF666666))), ), TextButton( onPressed: () async { @@ -275,16 +477,10 @@ class _BookDetailPageState extends State { Navigator.pop(context); Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('已删除'), - behavior: SnackBarBehavior.floating, - ), + const SnackBar(content: Text('已删除')), ); }, - child: const Text( - '删除', - style: TextStyle(color: Colors.red), - ), + child: const Text('删除', style: TextStyle(color: Colors.red)), ), ], ), diff --git a/lib/pages/book_form_page.dart b/lib/pages/book_form_page.dart index 4169605..22370eb 100644 --- a/lib/pages/book_form_page.dart +++ b/lib/pages/book_form_page.dart @@ -1,287 +1,554 @@ +import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as path; import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; import '../models/data_models.dart'; -/// 添加/编辑书籍记录页面 +/// 添加/编辑书籍页面 - 极简主义设计 class BookFormPage extends StatefulWidget { - final Book? book; // 如果为 null,则是添加模式;否则是编辑模式 - + final Book? book; + const BookFormPage({super.key, this.book}); - + @override State createState() => _BookFormPageState(); } class _BookFormPageState extends State { final _formKey = GlobalKey(); + final ImagePicker _picker = ImagePicker(); + late TextEditingController _titleController; - late TextEditingController _authorController; + late TextEditingController _publisherController; + late TextEditingController _summaryController; late TextEditingController _ratingController; - late TextEditingController _noteController; - late String _status; - DateTime? _readDate; - + + List _authors = []; + List _alternateTitles = []; + List _genres = []; + String? _coverPath; + String _status = 'want_to_read'; + @override void initState() { super.initState(); - _titleController = TextEditingController(text: widget.book?.title ?? ''); - _authorController = TextEditingController(text: widget.book?.author ?? ''); - _ratingController = TextEditingController(text: widget.book?.rating?.toString() ?? ''); - _noteController = TextEditingController(text: widget.book?.note ?? ''); - _status = widget.book?.status ?? 'want_to_read'; - _readDate = widget.book?.readDate; + final book = widget.book; + _titleController = TextEditingController(text: book?.title ?? ''); + _publisherController = TextEditingController(text: book?.publisher ?? ''); + _summaryController = TextEditingController(text: book?.summary ?? ''); + _ratingController = TextEditingController(text: book?.rating?.toString() ?? ''); + + if (book != null) { + _authors = List.from(book.authors); + _alternateTitles = List.from(book.alternateTitles); + _genres = List.from(book.genres); + _coverPath = book.coverPath; + _status = book.status; + } } - + @override void dispose() { _titleController.dispose(); - _authorController.dispose(); + _publisherController.dispose(); + _summaryController.dispose(); _ratingController.dispose(); - _noteController.dispose(); super.dispose(); } - + @override Widget build(BuildContext context) { final isEdit = widget.book != null; - + return Scaffold( + backgroundColor: Colors.white, appBar: AppBar( title: Text(isEdit ? '编辑书籍' : '添加书籍'), actions: [ - IconButton( - icon: const Icon(Icons.save), + TextButton( onPressed: _saveBook, + child: const Text( + '保存', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), ), + const SizedBox(width: 8), ], ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 标题 - TextFormField( - controller: _titleController, - decoration: const InputDecoration( - labelText: '书名 *', - hintText: '请输入书名', - prefixIcon: Icon(Icons.menu_book), - border: OutlineInputBorder(), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return '请输入书名'; - } - return null; - }, - ), - - const SizedBox(height: 16), - - // 作者 - TextFormField( - controller: _authorController, - decoration: const InputDecoration( - labelText: '作者', - hintText: '请输入作者', - prefixIcon: Icon(Icons.person), - border: OutlineInputBorder(), - ), - ), - - const SizedBox(height: 16), - - // 年份和评分 - Row( - children: [ - Expanded( - child: TextFormField( - controller: _ratingController, - decoration: const InputDecoration( - labelText: '评分', - hintText: '0-10', - prefixIcon: Icon(Icons.star), - border: OutlineInputBorder(), - ), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - validator: (value) { - if (value != null && value.isNotEmpty) { - final rating = double.tryParse(value); - if (rating == null || rating < 0 || rating > 10) { - return '评分必须在 0-10 之间'; - } + body: Form( + key: _formKey, + child: ListView( + padding: const EdgeInsets.all(24), + children: [ + // 封面选择 + _buildCoverPicker(), + + const SizedBox(height: 32), + + // 基本信息 + _buildSectionTitle('基本信息'), + const SizedBox(height: 16), + + // 书名 + _buildTextField( + controller: _titleController, + label: '书名 *', + hint: '请输入书名', + validator: (value) { + if (value == null || value.trim().isEmpty) { + return '请输入书名'; + } + return null; + }, + ), + + const SizedBox(height: 16), + + // 别名 + _buildTagInput( + label: '别名', + hint: '输入别名,按回车添加', + tags: _alternateTitles, + onAdd: (tag) => setState(() => _alternateTitles.add(tag)), + onRemove: (index) => setState(() => _alternateTitles.removeAt(index)), + ), + + const SizedBox(height: 24), + + // 作者 + _buildSectionTitle('作者'), + const SizedBox(height: 16), + + _buildTagInput( + label: '作者', + hint: '输入作者,按回车添加', + tags: _authors, + onAdd: (tag) => setState(() => _authors.add(tag)), + onRemove: (index) => setState(() => _authors.removeAt(index)), + ), + + const SizedBox(height: 24), + + // 出版社 + _buildTextField( + controller: _publisherController, + label: '出版社', + hint: '请输入出版社', + ), + + const SizedBox(height: 24), + + // 类型 + _buildSectionTitle('类型'), + const SizedBox(height: 16), + + _buildTagInput( + label: '类型', + hint: '输入类型,按回车添加', + tags: _genres, + onAdd: (tag) => setState(() => _genres.add(tag)), + onRemove: (index) => setState(() => _genres.removeAt(index)), + ), + + const SizedBox(height: 24), + + // 书籍简介 + _buildSectionTitle('书籍简介'), + const SizedBox(height: 16), + + _buildTextField( + controller: _summaryController, + label: '', + hint: '写下书籍简介...', + maxLines: 5, + ), + + const SizedBox(height: 24), + + // 评分和状态 + _buildSectionTitle('评分与状态'), + const SizedBox(height: 16), + + Row( + children: [ + Expanded( + flex: 2, + child: _buildTextField( + controller: _ratingController, + label: '评分', + hint: '1-10', + keyboardType: const TextInputType.numberWithOptions(decimal: true), + validator: (value) { + if (value != null && value.isNotEmpty) { + final rating = double.tryParse(value); + if (rating == null || rating < 1 || rating > 10) { + return '评分必须在 1-10 之间'; } - return null; - }, - ), + } + return null; + }, ), - ], - ), - - const SizedBox(height: 16), - - // 状态选择 - DropdownButtonFormField( - value: _status, - decoration: const InputDecoration( - labelText: '状态', - prefixIcon: Icon(Icons.check_circle_outline), - border: OutlineInputBorder(), ), - items: const [ - DropdownMenuItem(value: 'read', child: Text('读完')), - DropdownMenuItem(value: 'reading', child: Text('在读')), - DropdownMenuItem(value: 'want_to_read', child: Text('准备读')), - ], - onChanged: (value) { - setState(() { - _status = value!; - }); + const SizedBox(width: 16), + Expanded( + flex: 3, + child: _buildStatusSelector(), + ), + ], + ), + + const SizedBox(height: 48), + ], + ), + ), + ); + } + + /// 构建区块标题 + Widget _buildSectionTitle(String title) { + return Text( + title.toUpperCase(), + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF999999), + letterSpacing: 1, + ), + ); + } + + /// 构建封面选择器 + Widget _buildCoverPicker() { + return GestureDetector( + onTap: _pickCover, + child: Container( + width: 120, + height: 160, + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5), + ), + child: _coverPath != null && _coverPath!.isNotEmpty + ? Image.file( + File(_coverPath!), + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _buildCoverPlaceholder(), + ) + : _buildCoverPlaceholder(), + ), + ); + } + + Widget _buildCoverPlaceholder() { + return const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.add_photo_alternate_outlined, + size: 32, + color: Color(0xFF999999), + ), + SizedBox(height: 8), + Text( + '添加封面', + style: TextStyle( + fontSize: 13, + color: Color(0xFF999999), + ), + ), + ], + ); + } + + /// 构建文本输入框 + Widget _buildTextField({ + required TextEditingController controller, + required String label, + String? hint, + int maxLines = 1, + TextInputType? keyboardType, + String? Function(String?)? validator, + }) { + return TextFormField( + controller: controller, + maxLines: maxLines, + keyboardType: keyboardType, + validator: validator, + style: const TextStyle( + fontSize: 15, + color: Color(0xFF1A1A1A), + ), + decoration: InputDecoration( + labelText: label, + hintText: hint, + labelStyle: const TextStyle( + fontSize: 14, + color: Color(0xFF666666), + ), + hintStyle: const TextStyle( + fontSize: 14, + color: Color(0xFFCCCCCC), + ), + border: const UnderlineInputBorder( + borderSide: BorderSide(color: Color(0xFFE5E5E5)), + ), + enabledBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Color(0xFFE5E5E5)), + ), + focusedBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Color(0xFF1A1A1A)), + ), + contentPadding: const EdgeInsets.symmetric(vertical: 12), + ), + ); + } + + /// 构建标签输入 + Widget _buildTagInput({ + required String label, + required String hint, + required List tags, + required Function(String) onAdd, + required Function(int) onRemove, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (label.isNotEmpty) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + label, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF666666), + ), + ), + ), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + ...tags.asMap().entries.map((entry) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + border: Border.all(color: const Color(0xFFE5E5E5)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + entry.value, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF1A1A1A), + ), + ), + const SizedBox(width: 4), + GestureDetector( + onTap: () => onRemove(entry.key), + child: const Icon( + Icons.close, + size: 16, + color: Color(0xFF999999), + ), + ), + ], + ), + ); + }), + Container( + width: 120, + child: TextField( + decoration: InputDecoration( + hintText: hint, + hintStyle: const TextStyle( + fontSize: 13, + color: Color(0xFFCCCCCC), + ), + border: const UnderlineInputBorder( + borderSide: BorderSide(color: Color(0xFFE5E5E5)), + ), + contentPadding: const EdgeInsets.symmetric(vertical: 8), + ), + style: const TextStyle( + fontSize: 14, + color: Color(0xFF1A1A1A), + ), + onSubmitted: (value) { + if (value.trim().isNotEmpty && !tags.contains(value.trim())) { + onAdd(value.trim()); + } }, ), - - const SizedBox(height: 16), - - // 阅读日期选择 - InkWell( - onTap: _selectReadDate, - child: InputDecorator( - decoration: const InputDecoration( - labelText: '阅读日期', - prefixIcon: Icon(Icons.event), - border: OutlineInputBorder(), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - _readDate != null - ? '${_readDate!.year}-${_readDate!.month.toString().padLeft(2, '0')}-${_readDate!.day.toString().padLeft(2, '0')}' - : '选择日期', - style: TextStyle( - color: _readDate != null - ? Theme.of(context).colorScheme.onSurface - : Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - if (_readDate != null) - IconButton( - icon: const Icon(Icons.clear, size: 20), - onPressed: () { - setState(() { - _readDate = null; - }); - }, - ), - ], - ), - ), - ), - - const SizedBox(height: 16), - - // 笔记 - TextFormField( - controller: _noteController, - decoration: const InputDecoration( - labelText: '笔记', - hintText: '写下你的读后感...', - prefixIcon: Icon(Icons.edit_note), - border: OutlineInputBorder(), - alignLabelWithHint: true, - ), - maxLines: 5, - ), - - const SizedBox(height: 32), - - // 保存按钮 - SizedBox( - width: double.infinity, - height: 48, - child: ElevatedButton.icon( - onPressed: _saveBook, - icon: const Icon(Icons.save), - label: Text(isEdit ? '保存修改' : '添加记录'), - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - ), - ], + ), + ], + ), + ], + ); + } + + /// 构建状态选择器 + Widget _buildStatusSelector() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '状态', + style: TextStyle( + fontSize: 14, + color: Color(0xFF666666), + ), + ), + const SizedBox(height: 8), + Row( + children: [ + _buildStatusOption('已读', 'read'), + const SizedBox(width: 12), + _buildStatusOption('在读', 'reading'), + const SizedBox(width: 12), + _buildStatusOption('想读', 'want_to_read'), + ], + ), + ], + ); + } + + Widget _buildStatusOption(String label, String value) { + final isSelected = _status == value; + Color color; + switch (value) { + case 'read': + color = const Color(0xFF1A1A1A); + break; + case 'reading': + color = const Color(0xFF666666); + break; + case 'want_to_read': + color = const Color(0xFF999999); + break; + default: + color = const Color(0xFFCCCCCC); + } + + return GestureDetector( + onTap: () => setState(() => _status = value), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: isSelected ? color : Colors.transparent, + border: Border.all(color: color), + ), + child: Text( + label, + style: TextStyle( + fontSize: 13, + color: isSelected ? Colors.white : color, + fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal, ), ), ), ); } - - /// 选择阅读日期 - Future _selectReadDate() async { - final picked = await showDatePicker( - context: context, - initialDate: _readDate ?? DateTime.now(), - firstDate: DateTime(1900), - lastDate: DateTime.now(), - ); - - if (picked != null) { - setState(() { - _readDate = picked; - }); + + /// 选择封面 + Future _pickCover() async { + try { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource.gallery, + maxWidth: 800, + maxHeight: 1200, + imageQuality: 85, + ); + + if (pickedFile != null) { + final appDir = await getApplicationDocumentsDirectory(); + final fileName = 'book_cover_${DateTime.now().millisecondsSinceEpoch}.jpg'; + final savedPath = path.join(appDir.path, 'book_covers', fileName); + + final coverDir = Directory(path.join(appDir.path, 'book_covers')); + if (!await coverDir.exists()) { + await coverDir.create(recursive: true); + } + + await File(pickedFile.path).copy(savedPath); + + setState(() => _coverPath = savedPath); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('选择封面失败: $e')), + ); + } } } - - /// 保存书籍记录 + + /// 保存书籍 Future _saveBook() async { if (!_formKey.currentState!.validate()) { return; } - - final rating = _ratingController.text.isNotEmpty ? double.tryParse(_ratingController.text) : null; - + + final rating = _ratingController.text.isNotEmpty + ? double.tryParse(_ratingController.text) + : null; + + final now = DateTime.now(); + if (widget.book == null) { // 添加新模式 final newBook = Book( - id: DateTime.now().millisecondsSinceEpoch.toString(), + id: now.millisecondsSinceEpoch.toString(), title: _titleController.text.trim(), - author: _authorController.text.trim(), + coverPath: _coverPath, + authors: _authors, + alternateTitles: _alternateTitles, + publisher: _publisherController.text.trim(), + genres: _genres, + summary: _summaryController.text.trim(), rating: rating, status: _status, - readDate: _readDate, - note: _noteController.text.trim(), + createdAt: now, + updatedAt: now, ); - + await context.read().addBook(newBook); } else { // 编辑现有模式 - final updatedBook = Book( - id: widget.book!.id, + final updatedBook = widget.book!.copyWith( title: _titleController.text.trim(), - author: _authorController.text.trim(), + coverPath: _coverPath, + authors: _authors, + alternateTitles: _alternateTitles, + publisher: _publisherController.text.trim(), + genres: _genres, + summary: _summaryController.text.trim(), rating: rating, status: _status, - readDate: _readDate, - note: _noteController.text.trim(), + updatedAt: now, ); - + await context.read().updateBook(updatedBook); } - + if (!mounted) return; - + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(widget.book == null ? '添加成功' : '更新成功'), behavior: SnackBarBehavior.floating, ), ); - + Navigator.pop(context); } } diff --git a/lib/pages/book_tab_page.dart b/lib/pages/book_tab_page.dart index c160b11..067d2e0 100644 --- a/lib/pages/book_tab_page.dart +++ b/lib/pages/book_tab_page.dart @@ -66,7 +66,7 @@ class BookTabPage extends StatelessWidget { Icon( Icons.menu_book_outlined, size: 80, - color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.3), + color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.3), ), const SizedBox(height: 16), Text( @@ -92,48 +92,62 @@ class BookTabPage extends StatelessWidget { List _getSampleBooks(int statusIndex) { final statusMap = ['read', 'reading', 'want_to_read']; final currentStatus = statusMap[statusIndex]; + final now = DateTime.now(); // 示例数据(实际应从数据库获取) final allBooks = [ Book( id: '1', title: '活着', - author: '余华', + authors: ['余华'], rating: 9.2, status: 'read', - readDate: DateTime(2024, 1, 20), - note: '非常感人的故事,让人思考生命的意义', + genres: ['小说', '文学'], + summary: '非常感人的故事,让人思考生命的意义', + createdAt: now, + updatedAt: now, ), Book( id: '2', title: '百年孤独', - author: '加西亚·马尔克斯', + authors: ['加西亚·马尔克斯'], rating: 9.3, status: 'read', - readDate: DateTime(2024, 2, 15), + genres: ['小说', '魔幻现实主义'], + publisher: '南海出版公司', + createdAt: now, + updatedAt: now, ), Book( id: '3', title: '人类简史', - author: '尤瓦尔·赫拉利', + authors: ['尤瓦尔·赫拉利'], rating: 9.0, status: 'reading', + genres: ['历史', '科普'], + createdAt: now, + updatedAt: now, ), Book( id: '4', title: '三体', - author: '刘慈欣', + authors: ['刘慈欣'], rating: 9.5, status: 'want_to_read', + genres: ['科幻', '小说'], + createdAt: now, + updatedAt: now, ), Book( id: '5', title: '追风筝的人', - author: '卡勒德·胡赛尼', + authors: ['卡勒德·胡赛尼'], rating: 8.9, status: 'read', - readDate: DateTime(2024, 3, 5), - note: '关于救赎与成长的故事', + genres: ['小说', '文学'], + summary: '关于救赎与成长的故事', + createdAt: now, + updatedAt: now, ), ]; diff --git a/lib/pages/main_content_page.dart b/lib/pages/main_content_page.dart index a9fb918..84e7a42 100644 --- a/lib/pages/main_content_page.dart +++ b/lib/pages/main_content_page.dart @@ -34,10 +34,6 @@ class MainContentPage extends StatelessWidget { return AppBar( title: Text(_getAppBarTitle(provider)), actions: [ - IconButton( - icon: const Icon(Icons.add), - onPressed: () => _showAddDialog(context, provider), - ), IconButton( icon: const Icon(Icons.search), onPressed: () { diff --git a/lib/pages/movie_detail_page.dart b/lib/pages/movie_detail_page.dart index 8ac01af..9ea531b 100644 --- a/lib/pages/movie_detail_page.dart +++ b/lib/pages/movie_detail_page.dart @@ -7,328 +7,527 @@ import '../models/data_models.dart'; /// 影视详情页 - 极简主义设计 class MovieDetailPage extends StatefulWidget { final Movie movie; - + const MovieDetailPage({super.key, required this.movie}); - + @override State createState() => _MovieDetailPageState(); } class _MovieDetailPageState extends State { - late Movie _movie; - - @override - void initState() { - super.initState(); - _movie = widget.movie; - } - - void _refreshMovie() { - final provider = context.read(); - final updated = provider.movies.firstWhere( - (m) => m.id == _movie.id, - orElse: () => _movie, - ); - setState(() => _movie = updated); - } - @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('详情'), - actions: [ - TextButton( - onPressed: () => _navigateToEdit(context), - child: const Text('编辑'), + backgroundColor: Colors.white, + body: CustomScrollView( + slivers: [ + // 顶部海报区域 + _buildSliverAppBar(), + + // 内容区域 + SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 基本信息 + _buildBasicInfo(), + + const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)), + + // 导演 + if (widget.movie.directors.isNotEmpty) + _buildDirectorsSection(), + + // 编剧 + if (widget.movie.writers.isNotEmpty) + _buildWritersSection(), + + // 主演 + if (widget.movie.actors.isNotEmpty) + _buildActorsSection(), + + // 类型 + if (widget.movie.genres.isNotEmpty) + _buildGenresSection(), + + const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)), + + // 简介 + if (widget.movie.summary != null && widget.movie.summary!.isNotEmpty) + _buildSummarySection(), + + // 别名 + if (widget.movie.alternateTitles.isNotEmpty) + _buildAlternateTitlesSection(), + + const SizedBox(height: 48), + ], + ), ), - const SizedBox(width: 8), ], ), - body: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 海报 - _buildPoster(), - - const SizedBox(height: 40), - - // 标题 - Text( - _movie.title, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A), - height: 1.3, - ), - ), - - const SizedBox(height: 12), - - // 状态标签 - _buildStatusTag(), - - const SizedBox(height: 32), - - // 基本信息 - _buildInfoRow(), - - // 别名 - if (_movie.alternateTitles.isNotEmpty) ...[ - const SizedBox(height: 32), - _buildSectionTitle('别名'), - const SizedBox(height: 12), - _buildTextList(_movie.alternateTitles), - ], - - // 导演 - if (_movie.directors.isNotEmpty) ...[ - const SizedBox(height: 32), - _buildSectionTitle('导演'), - const SizedBox(height: 12), - _buildTextList(_movie.directors), - ], - - // 编剧 - if (_movie.writers.isNotEmpty) ...[ - const SizedBox(height: 32), - _buildSectionTitle('编剧'), - const SizedBox(height: 12), - _buildTextList(_movie.writers), - ], - - // 主演 - if (_movie.actors.isNotEmpty) ...[ - const SizedBox(height: 32), - _buildSectionTitle('主演'), - const SizedBox(height: 12), - _buildTextList(_movie.actors), - ], - - // 类型 - if (_movie.genres.isNotEmpty) ...[ - const SizedBox(height: 32), - _buildSectionTitle('类型'), - const SizedBox(height: 12), - Wrap( - spacing: 8, - runSpacing: 8, - children: _movie.genres.map((genre) => Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - border: Border.all( - color: const Color(0xFFE5E5E5), - width: 0.5, - ), - ), - child: Text( - genre, - style: const TextStyle( - fontSize: 13, - color: Color(0xFF666666), - ), - ), - )).toList(), - ), - ], - - // 剧情简介 - if (_movie.summary != null && _movie.summary!.isNotEmpty) ...[ - const SizedBox(height: 32), - _buildSectionTitle('简介'), - const SizedBox(height: 12), - Text( - _movie.summary!, - style: const TextStyle( - fontSize: 15, - color: Color(0xFF666666), - height: 1.7, - ), - ), - ], - - const SizedBox(height: 48), - - // 删除按钮 - Center( - child: TextButton( - onPressed: () => _showDeleteDialog(context), - style: TextButton.styleFrom( - foregroundColor: const Color(0xFFDC2626), - ), - child: const Text('删除此影片'), - ), - ), - - const SizedBox(height: 24), - ], - ), - ), + + // 底部操作栏 + bottomNavigationBar: _buildBottomBar(), ); } - - /// 海报 - Widget _buildPoster() { - return Center( - child: Container( - width: 140, - height: 200, - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - border: Border.all( - color: const Color(0xFFE5E5E5), - width: 0.5, - ), - ), - child: _movie.posterPath != null && _movie.posterPath!.isNotEmpty - ? Image.file( - File(_movie.posterPath!), - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => _buildPlaceholder(), - ) - : _buildPlaceholder(), + + /// 构建顶部 AppBar + Widget _buildSliverAppBar() { + return SliverAppBar( + expandedHeight: 280, + pinned: true, + backgroundColor: Colors.white, + flexibleSpace: FlexibleSpaceBar( + background: _buildPosterSection(), ), + actions: [ + IconButton( + icon: const Icon(Icons.edit_outlined), + onPressed: () => _navigateToEdit(context), + ), + const SizedBox(width: 8), + ], ); } - - Widget _buildPlaceholder() { - return const Center( - child: Text( - '无海报', - style: TextStyle( - fontSize: 13, - color: Color(0xFF999999), - ), - ), - ); - } - - /// 状态标签 - Widget _buildStatusTag() { - String label; - Color bgColor; - Color textColor; - - switch (_movie.status) { - case 'watched': - label = '已看'; - bgColor = const Color(0xFF1A1A1A); - textColor = Colors.white; - break; - case 'watching': - label = '在看'; - bgColor = const Color(0xFF666666); - textColor = Colors.white; - break; - case 'want_to_watch': - label = '想看'; - bgColor = const Color(0xFFF5F5F5); - textColor = const Color(0xFF666666); - break; - default: - label = '未知'; - bgColor = const Color(0xFFF5F5F5); - textColor = const Color(0xFF999999); - } - + + /// 构建海报区域 + Widget _buildPosterSection() { return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - decoration: BoxDecoration( - color: bgColor, - borderRadius: BorderRadius.zero, - ), - child: Text( - label, - style: TextStyle( - fontSize: 12, - color: textColor, - fontWeight: FontWeight.w500, - ), + width: double.infinity, + color: const Color(0xFFF5F5F5), + child: widget.movie.posterPath != null && widget.movie.posterPath!.isNotEmpty + ? Image.file( + File(widget.movie.posterPath!), + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => _buildPosterPlaceholder(), + ) + : _buildPosterPlaceholder(), + ); + } + + Widget _buildPosterPlaceholder() { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.movie_outlined, + size: 64, + color: Color(0xFFCCCCCC), + ), + SizedBox(height: 16), + Text( + '暂无海报', + style: TextStyle( + fontSize: 14, + color: Color(0xFF999999), + ), + ), + ], ), ); } - - /// 基本信息行 - Widget _buildInfoRow() { - final items = []; - - if (_movie.releaseDate != null) { - items.add('${_movie.releaseDate!.year}'); - } - if (_movie.rating != null) { - items.add('${_movie.rating!.toStringAsFixed(1)} 分'); - } - - if (items.isEmpty) return const SizedBox.shrink(); - - return Row( - children: items.asMap().entries.map((entry) { - return Row( - children: [ + + /// 构建基本信息 + Widget _buildBasicInfo() { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 影视名称 + Text( + widget.movie.title, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w600, + color: Color(0xFF1A1A1A), + height: 1.3, + ), + ), + + const SizedBox(height: 16), + + // 评分和状态 + Row( + children: [ + if (widget.movie.rating != null) ...[ + const Icon( + Icons.star, + size: 20, + color: Color(0xFF1A1A1A), + ), + const SizedBox(width: 4), + Text( + widget.movie.rating!.toStringAsFixed(1), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Color(0xFF1A1A1A), + ), + ), + const SizedBox(width: 16), + ], + _buildStatusTag(), + ], + ), + + const SizedBox(height: 8), + + // 上映日期 + if (widget.movie.releaseDate != null) Text( - entry.value, + '${widget.movie.releaseDate!.year}年上映', style: const TextStyle( fontSize: 14, color: Color(0xFF999999), ), ), - if (entry.key < items.length - 1) - const Padding( - padding: EdgeInsets.symmetric(horizontal: 12), - child: Text( - '·', - style: TextStyle( - fontSize: 14, - color: Color(0xFFCCCCCC), - ), - ), - ), - ], - ); - }).toList(), - ); - } - - /// 区块标题 - Widget _buildSectionTitle(String title) { - return Text( - title.toUpperCase(), - style: const TextStyle( - fontSize: 11, - fontWeight: FontWeight.w500, - color: Color(0xFF999999), - letterSpacing: 1, + + const SizedBox(height: 8), + + // 时间信息 + Text( + '添加于 ${_formatDate(widget.movie.createdAt)}', + style: const TextStyle( + fontSize: 12, + color: Color(0xFF999999), + ), + ), + ], ), ); } - - /// 文本列表 - Widget _buildTextList(List items) { - return Wrap( - spacing: 8, - runSpacing: 8, - children: items.map((item) => Text( - item, + + /// 构建状态标签 + Widget _buildStatusTag() { + String label; + Color color; + switch (widget.movie.status) { + case 'watched': + label = '已看'; + color = const Color(0xFF1A1A1A); + break; + case 'watching': + label = '在看'; + color = const Color(0xFF666666); + break; + case 'want_to_watch': + label = '想看'; + color = const Color(0xFF999999); + break; + default: + label = '未知'; + color = const Color(0xFFCCCCCC); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: color, + ), + child: Text( + label, style: const TextStyle( - fontSize: 15, - color: Color(0xFF333333), + fontSize: 12, + color: Colors.white, + fontWeight: FontWeight.w500, ), - )).toList(), + ), ); } - - /// 跳转到编辑 + + /// 构建导演区域 + Widget _buildDirectorsSection() { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '导演', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF999999), + letterSpacing: 1, + ), + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: widget.movie.directors.map((director) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + border: Border.all(color: const Color(0xFFE5E5E5)), + ), + child: Text( + director, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF1A1A1A), + ), + ), + ); + }).toList(), + ), + ], + ), + ); + } + + /// 构建编剧区域 + Widget _buildWritersSection() { + return Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '编剧', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF999999), + letterSpacing: 1, + ), + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: widget.movie.writers.map((writer) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + border: Border.all(color: const Color(0xFFE5E5E5)), + ), + child: Text( + writer, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF1A1A1A), + ), + ), + ); + }).toList(), + ), + ], + ), + ); + } + + /// 构建主演区域 + Widget _buildActorsSection() { + return Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '主演', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF999999), + letterSpacing: 1, + ), + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: widget.movie.actors.map((actor) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + border: Border.all(color: const Color(0xFFE5E5E5)), + ), + child: Text( + actor, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF1A1A1A), + ), + ), + ); + }).toList(), + ), + ], + ), + ); + } + + /// 构建类型区域 + Widget _buildGenresSection() { + return Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '类型', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF999999), + letterSpacing: 1, + ), + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: widget.movie.genres.map((genre) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + border: Border.all(color: const Color(0xFFE5E5E5)), + ), + child: Text( + genre, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF666666), + ), + ), + ); + }).toList(), + ), + ], + ), + ); + } + + /// 构建简介区域 + Widget _buildSummarySection() { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '简介', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF999999), + letterSpacing: 1, + ), + ), + const SizedBox(height: 12), + Text( + widget.movie.summary!, + style: const TextStyle( + fontSize: 15, + color: Color(0xFF1A1A1A), + height: 1.6, + ), + ), + ], + ), + ); + } + + /// 构建别名区域 + Widget _buildAlternateTitlesSection() { + return Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '别名', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF999999), + letterSpacing: 1, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: widget.movie.alternateTitles.map((title) { + return Text( + title, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF666666), + ), + ); + }).toList(), + ), + ], + ), + ); + } + + /// 构建底部操作栏 + Widget _buildBottomBar() { + return Container( + decoration: const BoxDecoration( + border: Border( + top: BorderSide(color: Color(0xFFE5E5E5), width: 0.5), + ), + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => _navigateToEdit(context), + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFF1A1A1A), + side: const BorderSide(color: Color(0xFF1A1A1A)), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + child: const Text('编辑'), + ), + ), + const SizedBox(width: 16), + Expanded( + child: OutlinedButton( + onPressed: () => _showDeleteDialog(context), + style: OutlinedButton.styleFrom( + foregroundColor: Colors.red, + side: const BorderSide(color: Colors.red), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + child: const Text('删除'), + ), + ), + ], + ), + ), + ), + ); + } + + /// 格式化日期 + String _formatDate(DateTime date) { + return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; + } + + /// 跳转到编辑页面 void _navigateToEdit(BuildContext context) { - Navigator.pushNamed(context, '/movie-form', arguments: _movie).then((_) { - _refreshMovie(); + Navigator.pushNamed(context, '/movie-form', arguments: widget.movie).then((_) { context.read().loadMovies(); }); } - - /// 删除对话框 + + /// 显示删除对话框 void _showDeleteDialog(BuildContext context) { showDialog( context: context, @@ -336,40 +535,24 @@ class _MovieDetailPageState extends State { backgroundColor: Colors.white, elevation: 0, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), - title: const Text( - '确认删除', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A1A), - ), - ), - content: Text( - '确定要删除"${_movie.title}"吗?', - style: const TextStyle( - fontSize: 15, - color: Color(0xFF666666), - ), - ), + title: const Text('确认删除'), + content: Text('确定要删除"${widget.movie.title}"吗?'), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text( - '取消', - style: TextStyle(color: Color(0xFF666666)), - ), + child: const Text('取消', style: TextStyle(color: Color(0xFF666666))), ), TextButton( onPressed: () async { - await context.read().removeMovie(_movie.id); - if (!context.mounted) return; + await context.read().removeMovie(widget.movie.id); + if (!mounted) return; Navigator.pop(context); Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('已删除')), + ); }, - child: const Text( - '删除', - style: TextStyle(color: Color(0xFFDC2626)), - ), + child: const Text('删除', style: TextStyle(color: Colors.red)), ), ], ), diff --git a/lib/pages/movie_form_page.dart b/lib/pages/movie_form_page.dart index eb6bd49..e46ca55 100644 --- a/lib/pages/movie_form_page.dart +++ b/lib/pages/movie_form_page.dart @@ -7,12 +7,12 @@ import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; import '../models/data_models.dart'; -/// 添加/编辑影视记录 - 极简主义设计 +/// 添加/编辑影视页面 - 极简主义设计 class MovieFormPage extends StatefulWidget { final Movie? movie; - + const MovieFormPage({super.key, this.movie}); - + @override State createState() => _MovieFormPageState(); } @@ -22,293 +22,321 @@ class _MovieFormPageState extends State { final ImagePicker _picker = ImagePicker(); late TextEditingController _titleController; - late TextEditingController _ratingController; late TextEditingController _summaryController; + late TextEditingController _ratingController; - final List _directorControllers = []; - final List _writerControllers = []; - final List _actorControllers = []; - final List _genreControllers = []; - final List _alternateTitleControllers = []; - - late String _status; - DateTime? _releaseDate; + List _directors = []; + List _writers = []; + List _actors = []; + List _genres = []; + List _alternateTitles = []; String? _posterPath; - bool _isLoading = false; - + String _status = 'want_to_watch'; + DateTime? _releaseDate; + @override void initState() { super.initState(); final movie = widget.movie; - _titleController = TextEditingController(text: movie?.title ?? ''); - _ratingController = TextEditingController(text: movie?.rating?.toString() ?? ''); _summaryController = TextEditingController(text: movie?.summary ?? ''); + _ratingController = TextEditingController(text: movie?.rating?.toString() ?? ''); - _status = movie?.status ?? 'want_to_watch'; - _releaseDate = movie?.releaseDate; - _posterPath = movie?.posterPath; - - _initListControllers(movie?.directors ?? [], _directorControllers); - _initListControllers(movie?.writers ?? [], _writerControllers); - _initListControllers(movie?.actors ?? [], _actorControllers); - _initListControllers(movie?.genres ?? [], _genreControllers); - _initListControllers(movie?.alternateTitles ?? [], _alternateTitleControllers); - } - - void _initListControllers(List items, List controllers) { - if (items.isEmpty) { - controllers.add(TextEditingController()); - } else { - for (final item in items) { - controllers.add(TextEditingController(text: item)); - } + if (movie != null) { + _directors = List.from(movie.directors); + _writers = List.from(movie.writers); + _actors = List.from(movie.actors); + _genres = List.from(movie.genres); + _alternateTitles = List.from(movie.alternateTitles); + _posterPath = movie.posterPath; + _status = movie.status; + _releaseDate = movie.releaseDate; } } - + @override void dispose() { _titleController.dispose(); - _ratingController.dispose(); _summaryController.dispose(); - - for (final c in _directorControllers) c.dispose(); - for (final c in _writerControllers) c.dispose(); - for (final c in _actorControllers) c.dispose(); - for (final c in _genreControllers) c.dispose(); - for (final c in _alternateTitleControllers) c.dispose(); - + _ratingController.dispose(); super.dispose(); } - + @override Widget build(BuildContext context) { final isEdit = widget.movie != null; - final theme = Theme.of(context); return Scaffold( + backgroundColor: Colors.white, appBar: AppBar( - title: Text(isEdit ? '编辑' : '添加'), + title: Text(isEdit ? '编辑影视' : '添加影视'), actions: [ TextButton( - onPressed: _isLoading ? null : _saveMovie, - child: _isLoading - ? SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator(strokeWidth: 2, color: theme.colorScheme.primary) - ) - : Text('保存'), + onPressed: _saveMovie, + child: const Text( + '保存', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), ), const SizedBox(width: 8), ], ), body: Form( key: _formKey, - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 海报 - _buildPosterSection(), - - const SizedBox(height: 40), - - // 基本信息 - _buildSectionTitle('基本信息'), - const SizedBox(height: 24), - - // 影视名称 - _buildTextField( - controller: _titleController, - label: '名称', - validator: (value) { - if (value == null || value.trim().isEmpty) { - return '请输入影视名称'; - } - return null; - }, - ), - - const SizedBox(height: 24), - - // 上映日期 - _buildDatePicker(), - - const SizedBox(height: 24), - - // 评分 - _buildTextField( - controller: _ratingController, - label: '评分', - hint: '1-10', - keyboardType: const TextInputType.numberWithOptions(decimal: true), - validator: (value) { - if (value != null && value.isNotEmpty) { - final rating = double.tryParse(value); - if (rating == null || rating < 1 || rating > 10) { - return '评分范围 1-10'; - } - } - return null; - }, - ), - - const SizedBox(height: 32), - - // 状态 - _buildSectionTitle('状态'), - const SizedBox(height: 16), - _buildStatusSelector(), - - const SizedBox(height: 40), - - // 别名 - _buildSectionTitle('别名'), - const SizedBox(height: 16), - _buildTagList(_alternateTitleControllers), - - const SizedBox(height: 40), - - // 导演 - _buildSectionTitle('导演'), - const SizedBox(height: 16), - _buildTagList(_directorControllers), - - const SizedBox(height: 40), - - // 编剧 - _buildSectionTitle('编剧'), - const SizedBox(height: 16), - _buildTagList(_writerControllers), - - const SizedBox(height: 40), - - // 主演 - _buildSectionTitle('主演'), - const SizedBox(height: 16), - _buildTagList(_actorControllers), - - const SizedBox(height: 40), - - // 类型 - _buildSectionTitle('类型'), - const SizedBox(height: 16), - _buildTagList(_genreControllers), - - const SizedBox(height: 40), - - // 剧情简介 - _buildSectionTitle('简介'), - const SizedBox(height: 16), - _buildTextField( - controller: _summaryController, - label: '', - hint: '剧情简介...', - maxLines: 6, - ), - - const SizedBox(height: 48), - ], - ), - ), - ), - ); - } - - /// 海报区域 - 极简 - Widget _buildPosterSection() { - return Center( - child: GestureDetector( - onTap: _pickImage, - child: Container( - width: 120, - height: 170, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - border: Border.all( - color: const Color(0xFFE5E5E5), - width: 0.5, + child: ListView( + padding: const EdgeInsets.all(24), + children: [ + // 封面选择 + _buildCoverPicker(), + + const SizedBox(height: 32), + + // 基本信息 + _buildSectionTitle('基本信息'), + const SizedBox(height: 16), + + // 影视名称 + _buildTextField( + controller: _titleController, + label: '影视名称 *', + hint: '请输入影视名称', + validator: (value) { + if (value == null || value.trim().isEmpty) { + return '请输入影视名称'; + } + return null; + }, ), - ), - child: _posterPath != null && _posterPath!.isNotEmpty - ? Image.file( - File(_posterPath!), - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => _buildPlaceholder(), - ) - : _buildPlaceholder(), + + const SizedBox(height: 16), + + // 别名 + _buildTagInput( + label: '别名', + hint: '输入别名,按回车添加', + tags: _alternateTitles, + onAdd: (tag) => setState(() => _alternateTitles.add(tag)), + onRemove: (index) => setState(() => _alternateTitles.removeAt(index)), + ), + + const SizedBox(height: 16), + + // 上映日期 + _buildDatePicker(), + + const SizedBox(height: 24), + + // 导演 + _buildSectionTitle('导演'), + const SizedBox(height: 16), + + _buildTagInput( + label: '导演', + hint: '输入导演,按回车添加', + tags: _directors, + onAdd: (tag) => setState(() => _directors.add(tag)), + onRemove: (index) => setState(() => _directors.removeAt(index)), + ), + + const SizedBox(height: 24), + + // 编剧 + _buildSectionTitle('编剧'), + const SizedBox(height: 16), + + _buildTagInput( + label: '编剧', + hint: '输入编剧,按回车添加', + tags: _writers, + onAdd: (tag) => setState(() => _writers.add(tag)), + onRemove: (index) => setState(() => _writers.removeAt(index)), + ), + + const SizedBox(height: 24), + + // 主演 + _buildSectionTitle('主演'), + const SizedBox(height: 16), + + _buildTagInput( + label: '主演', + hint: '输入主演,按回车添加', + tags: _actors, + onAdd: (tag) => setState(() => _actors.add(tag)), + onRemove: (index) => setState(() => _actors.removeAt(index)), + ), + + const SizedBox(height: 24), + + // 类型 + _buildSectionTitle('类型'), + const SizedBox(height: 16), + + _buildTagInput( + label: '类型', + hint: '输入类型,按回车添加', + tags: _genres, + onAdd: (tag) => setState(() => _genres.add(tag)), + onRemove: (index) => setState(() => _genres.removeAt(index)), + ), + + const SizedBox(height: 24), + + // 剧情简介 + _buildSectionTitle('剧情简介'), + const SizedBox(height: 16), + + _buildTextField( + controller: _summaryController, + label: '', + hint: '写下剧情简介...', + maxLines: 5, + ), + + const SizedBox(height: 24), + + // 评分和状态 + _buildSectionTitle('评分与状态'), + const SizedBox(height: 16), + + Row( + children: [ + Expanded( + flex: 2, + child: _buildTextField( + controller: _ratingController, + label: '评分', + hint: '1-10', + keyboardType: const TextInputType.numberWithOptions(decimal: true), + validator: (value) { + if (value != null && value.isNotEmpty) { + final rating = double.tryParse(value); + if (rating == null || rating < 1 || rating > 10) { + return '评分必须在 1-10 之间'; + } + } + return null; + }, + ), + ), + const SizedBox(width: 16), + Expanded( + flex: 3, + child: _buildStatusSelector(), + ), + ], + ), + + const SizedBox(height: 48), + ], ), ), ); } - - Widget _buildPlaceholder() { - return const Center( - child: Text( - '添加海报', - style: TextStyle( - fontSize: 13, - color: Color(0xFF999999), - ), - ), - ); - } - - /// 区块标题 - 大写字母,小字号 + + /// 构建区块标题 Widget _buildSectionTitle(String title) { return Text( title.toUpperCase(), style: const TextStyle( fontSize: 11, - fontWeight: FontWeight.w500, + fontWeight: FontWeight.w600, color: Color(0xFF999999), letterSpacing: 1, ), ); } - - /// 文本输入框 - 极简无边框 + + /// 构建封面选择器 + Widget _buildCoverPicker() { + return GestureDetector( + onTap: _pickCover, + child: Container( + width: 120, + height: 160, + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5), + ), + child: _posterPath != null && _posterPath!.isNotEmpty + ? Image.file( + File(_posterPath!), + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _buildCoverPlaceholder(), + ) + : _buildCoverPlaceholder(), + ), + ); + } + + Widget _buildCoverPlaceholder() { + return const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.add_photo_alternate_outlined, + size: 32, + color: Color(0xFF999999), + ), + SizedBox(height: 8), + Text( + '添加海报', + style: TextStyle( + fontSize: 13, + color: Color(0xFF999999), + ), + ), + ], + ); + } + + /// 构建文本输入框 Widget _buildTextField({ required TextEditingController controller, required String label, String? hint, + int maxLines = 1, TextInputType? keyboardType, String? Function(String?)? validator, - int maxLines = 1, }) { return TextFormField( controller: controller, - keyboardType: keyboardType, maxLines: maxLines, + keyboardType: keyboardType, validator: validator, style: const TextStyle( - fontSize: 16, + fontSize: 15, color: Color(0xFF1A1A1A), ), decoration: InputDecoration( - labelText: label.isEmpty ? null : label, + labelText: label, hintText: hint, + labelStyle: const TextStyle( + fontSize: 14, + color: Color(0xFF666666), + ), hintStyle: const TextStyle( - fontSize: 15, + fontSize: 14, color: Color(0xFFCCCCCC), ), border: const UnderlineInputBorder( - borderSide: BorderSide(color: Color(0xFFE5E5E5), width: 0.5), + borderSide: BorderSide(color: Color(0xFFE5E5E5)), ), enabledBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Color(0xFFE5E5E5), width: 0.5), + borderSide: BorderSide(color: Color(0xFFE5E5E5)), ), focusedBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Color(0xFF1A1A1A), width: 1), + borderSide: BorderSide(color: Color(0xFF1A1A1A)), ), contentPadding: const EdgeInsets.symmetric(vertical: 12), ), ); } - - /// 日期选择器 + + /// 构建日期选择器 Widget _buildDatePicker() { return GestureDetector( onTap: _selectReleaseDate, @@ -316,7 +344,7 @@ class _MovieFormPageState extends State { padding: const EdgeInsets.symmetric(vertical: 12), decoration: const BoxDecoration( border: Border( - bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5), + bottom: BorderSide(color: Color(0xFFE5E5E5)), ), ), child: Row( @@ -326,7 +354,7 @@ class _MovieFormPageState extends State { ? '${_releaseDate!.year}.${_releaseDate!.month.toString().padLeft(2, '0')}.${_releaseDate!.day.toString().padLeft(2, '0')}' : '上映日期', style: TextStyle( - fontSize: 16, + fontSize: 15, color: _releaseDate != null ? const Color(0xFF1A1A1A) : const Color(0xFFCCCCCC), @@ -347,128 +375,159 @@ class _MovieFormPageState extends State { ), ); } - - /// 状态选择器 - 极简分段 - Widget _buildStatusSelector() { - final statuses = [ - {'value': 'watching', 'label': '在看'}, - {'value': 'watched', 'label': '已看'}, - {'value': 'want_to_watch', 'label': '想看'}, - ]; - - return Container( - decoration: BoxDecoration( - border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5), - ), - child: Row( - children: statuses.asMap().entries.map((entry) { - final isSelected = _status == entry.value['value']; - final isLast = entry.key == statuses.length - 1; - - return Expanded( - child: GestureDetector( - onTap: () => setState(() => _status = entry.value['value'] as String), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: isSelected ? const Color(0xFF1A1A1A) : Colors.transparent, - border: !isLast - ? const Border( - right: BorderSide(color: Color(0xFFE5E5E5), width: 0.5), - ) - : null, - ), - child: Text( - entry.value['label'] as String, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - color: isSelected ? Colors.white : const Color(0xFF666666), - ), - ), - ), - ), - ); - }).toList(), - ), - ); - } - - /// 标签列表 - 极简输入 - Widget _buildTagList(List controllers) { + + /// 构建标签输入 + Widget _buildTagInput({ + required String label, + required String hint, + required List tags, + required Function(String) onAdd, + required Function(int) onRemove, + }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ...controllers.asMap().entries.map((entry) { - final index = entry.key; - final controller = entry.value; - return Container( - padding: const EdgeInsets.only(bottom: 12), - child: Row( - children: [ - Expanded( - child: TextField( - controller: controller, - style: const TextStyle( - fontSize: 15, - color: Color(0xFF1A1A1A), - ), - decoration: const InputDecoration( - isDense: true, - border: InputBorder.none, - contentPadding: EdgeInsets.symmetric(vertical: 8), - hintText: '输入...', - hintStyle: TextStyle( - fontSize: 15, - color: Color(0xFFCCCCCC), - ), - ), - ), - ), - if (controllers.length > 1) - GestureDetector( - onTap: () { - setState(() { - controller.dispose(); - controllers.removeAt(index); - }); - }, - child: const Padding( - padding: EdgeInsets.only(left: 12), - child: Text( - '删除', - style: TextStyle( - fontSize: 13, - color: Color(0xFF999999), - ), - ), - ), - ), - ], - ), - ); - }), - - // 添加按钮 - GestureDetector( - onTap: () => setState(() => controllers.add(TextEditingController())), - child: const Padding( - padding: EdgeInsets.only(top: 4), + if (label.isNotEmpty) + Padding( + padding: const EdgeInsets.only(bottom: 8), child: Text( - '+ 添加', - style: TextStyle( + label, + style: const TextStyle( fontSize: 14, color: Color(0xFF666666), ), ), ), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + ...tags.asMap().entries.map((entry) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + border: Border.all(color: const Color(0xFFE5E5E5)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + entry.value, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF1A1A1A), + ), + ), + const SizedBox(width: 4), + GestureDetector( + onTap: () => onRemove(entry.key), + child: const Icon( + Icons.close, + size: 16, + color: Color(0xFF999999), + ), + ), + ], + ), + ); + }), + SizedBox( + width: 120, + child: TextField( + decoration: InputDecoration( + hintText: hint, + hintStyle: const TextStyle( + fontSize: 13, + color: Color(0xFFCCCCCC), + ), + border: const UnderlineInputBorder( + borderSide: BorderSide(color: Color(0xFFE5E5E5)), + ), + contentPadding: const EdgeInsets.symmetric(vertical: 8), + ), + style: const TextStyle( + fontSize: 14, + color: Color(0xFF1A1A1A), + ), + onSubmitted: (value) { + if (value.trim().isNotEmpty && !tags.contains(value.trim())) { + onAdd(value.trim()); + } + }, + ), + ), + ], ), ], ); } - - /// 选择图片 - Future _pickImage() async { + + /// 构建状态选择器 + Widget _buildStatusSelector() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '状态', + style: TextStyle( + fontSize: 14, + color: Color(0xFF666666), + ), + ), + const SizedBox(height: 8), + Row( + children: [ + _buildStatusOption('已看', 'watched'), + const SizedBox(width: 12), + _buildStatusOption('在看', 'watching'), + const SizedBox(width: 12), + _buildStatusOption('想看', 'want_to_watch'), + ], + ), + ], + ); + } + + Widget _buildStatusOption(String label, String value) { + final isSelected = _status == value; + Color color; + switch (value) { + case 'watched': + color = const Color(0xFF1A1A1A); + break; + case 'watching': + color = const Color(0xFF666666); + break; + case 'want_to_watch': + color = const Color(0xFF999999); + break; + default: + color = const Color(0xFFCCCCCC); + } + + return GestureDetector( + onTap: () => setState(() => _status = value), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: isSelected ? color : Colors.transparent, + border: Border.all(color: color), + ), + child: Text( + label, + style: TextStyle( + fontSize: 13, + color: isSelected ? Colors.white : color, + fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal, + ), + ), + ), + ); + } + + /// 选择封面 + Future _pickCover() async { try { final XFile? pickedFile = await _picker.pickImage( source: ImageSource.gallery, @@ -479,10 +538,10 @@ class _MovieFormPageState extends State { if (pickedFile != null) { final appDir = await getApplicationDocumentsDirectory(); - final fileName = 'poster_${DateTime.now().millisecondsSinceEpoch}.jpg'; - final savedPath = path.join(appDir.path, 'posters', fileName); + final fileName = 'movie_poster_${DateTime.now().millisecondsSinceEpoch}.jpg'; + final savedPath = path.join(appDir.path, 'movie_posters', fileName); - final posterDir = Directory(path.join(appDir.path, 'posters')); + final posterDir = Directory(path.join(appDir.path, 'movie_posters')); if (!await posterDir.exists()) { await posterDir.create(recursive: true); } @@ -494,12 +553,12 @@ class _MovieFormPageState extends State { } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('选择图片失败: $e')), + SnackBar(content: Text('选择海报失败: $e')), ); } } } - + /// 选择日期 Future _selectReleaseDate() async { final picked = await showDatePicker( @@ -523,85 +582,68 @@ class _MovieFormPageState extends State { setState(() => _releaseDate = picked); } } - - /// 保存 + + /// 保存影视 Future _saveMovie() async { - if (!_formKey.currentState!.validate()) return; - - setState(() => _isLoading = true); - - try { - final rating = _ratingController.text.isNotEmpty - ? double.tryParse(_ratingController.text) - : null; - - final directors = _collectNonEmptyTexts(_directorControllers); - final writers = _collectNonEmptyTexts(_writerControllers); - final actors = _collectNonEmptyTexts(_actorControllers); - final genres = _collectNonEmptyTexts(_genreControllers); - final alternateTitles = _collectNonEmptyTexts(_alternateTitleControllers); - - final now = DateTime.now(); - - if (widget.movie == null) { - final newMovie = Movie( - id: now.millisecondsSinceEpoch.toString(), - title: _titleController.text.trim(), - posterPath: _posterPath, - releaseDate: _releaseDate, - directors: directors, - writers: writers, - actors: actors, - genres: genres, - alternateTitles: alternateTitles, - summary: _summaryController.text.trim().isEmpty - ? null - : _summaryController.text.trim(), - rating: rating, - status: _status, - createdAt: now, - updatedAt: now, - ); - - await context.read().addMovie(newMovie); - } else { - final updatedMovie = widget.movie!.copyWith( - title: _titleController.text.trim(), - posterPath: _posterPath, - releaseDate: _releaseDate, - directors: directors, - writers: writers, - actors: actors, - genres: genres, - alternateTitles: alternateTitles, - summary: _summaryController.text.trim().isEmpty - ? null - : _summaryController.text.trim(), - rating: rating, - status: _status, - updatedAt: now, - ); - - await context.read().updateMovie(updatedMovie); - } - - if (!mounted) return; - Navigator.pop(context); - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('保存失败: $e')), - ); - } - } finally { - if (mounted) setState(() => _isLoading = false); + if (!_formKey.currentState!.validate()) { + return; } - } - - List _collectNonEmptyTexts(List controllers) { - return controllers - .map((c) => c.text.trim()) - .where((text) => text.isNotEmpty) - .toList(); + + final rating = _ratingController.text.isNotEmpty + ? double.tryParse(_ratingController.text) + : null; + + final now = DateTime.now(); + + if (widget.movie == null) { + // 添加新模式 + final newMovie = Movie( + id: now.millisecondsSinceEpoch.toString(), + title: _titleController.text.trim(), + posterPath: _posterPath, + releaseDate: _releaseDate, + directors: _directors, + writers: _writers, + actors: _actors, + genres: _genres, + alternateTitles: _alternateTitles, + summary: _summaryController.text.trim(), + rating: rating, + status: _status, + createdAt: now, + updatedAt: now, + ); + + await context.read().addMovie(newMovie); + } else { + // 编辑现有模式 + final updatedMovie = widget.movie!.copyWith( + title: _titleController.text.trim(), + posterPath: _posterPath, + releaseDate: _releaseDate, + directors: _directors, + writers: _writers, + actors: _actors, + genres: _genres, + alternateTitles: _alternateTitles, + summary: _summaryController.text.trim(), + rating: rating, + status: _status, + updatedAt: now, + ); + + await context.read().updateMovie(updatedMovie); + } + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(widget.movie == null ? '添加成功' : '更新成功'), + behavior: SnackBarBehavior.floating, + ), + ); + + Navigator.pop(context); } } diff --git a/lib/pages/note_detail_page.dart b/lib/pages/note_detail_page.dart index a2fe02b..be3f1e0 100644 --- a/lib/pages/note_detail_page.dart +++ b/lib/pages/note_detail_page.dart @@ -3,7 +3,7 @@ import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; import '../models/data_models.dart'; -/// 笔记详情页 +/// 笔记详情页 - 极简主义设计 class NoteDetailPage extends StatefulWidget { final Note note; @@ -17,133 +17,112 @@ class _NoteDetailPageState extends State { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Colors.white, appBar: AppBar( - title: Text(widget.note.title), + title: Text(_formatDateTime(widget.note.createdAt)), actions: [ IconButton( - icon: const Icon(Icons.edit), + icon: const Icon(Icons.edit_outlined), onPressed: () => _navigateToEdit(context), ), - IconButton( - icon: const Icon(Icons.delete_outline), - onPressed: () => _showDeleteDialog(context), - ), + const SizedBox(width: 8), ], ), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 标签区域 - if (widget.note.tags.isNotEmpty) _buildTagsSection(context), - - // 内容区域 - _buildContentSection(context), - - // 时间信息 - _buildTimeSection(context), - ], - ), - ), - ); - } - - /// 构建标签区域 - Widget _buildTagsSection(BuildContext context) { - return Container( - padding: const EdgeInsets.all(16), - child: Wrap( - spacing: 8, - runSpacing: 8, - children: widget.note.tags.map((tag) { - return Chip( - label: Text(tag), - backgroundColor: Theme.of(context).colorScheme.primaryContainer, - labelStyle: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), - ); - }).toList(), - ), - ); - } - - /// 构建内容区域 - Widget _buildContentSection(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + body: Column( children: [ - Text( - widget.note.content, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( + // 标签区域 + if (widget.note.tags.isNotEmpty) + Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5), + ), + ), + child: Row( + children: [ + Wrap( + spacing: 8, + runSpacing: 8, + children: widget.note.tags.map((tag) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + border: Border.all(color: const Color(0xFFE5E5E5)), + ), + child: Text( + tag, + style: const TextStyle( + fontSize: 12, + color: Color(0xFF666666), + ), + ), + ); + }).toList(), + ), + ], + ), + ), + + // 内容区域 + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Text( + widget.note.content, + style: const TextStyle( + fontSize: 16, + color: Color(0xFF1A1A1A), height: 1.8, ), + ), + ), ), - ], - ), - ); - } - /// 构建时间信息区域 - Widget _buildTimeSection(BuildContext context) { - return Container( - margin: const EdgeInsets.all(16), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.access_time, - size: 18, - color: Theme.of(context).colorScheme.onSurfaceVariant, + // 底部操作栏 + Container( + decoration: const BoxDecoration( + border: Border( + top: BorderSide(color: Color(0xFFE5E5E5), width: 0.5), ), - const SizedBox(width: 8), - Text( - '创建时间', - style: Theme.of(context).textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.bold, + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '更新于 ${_formatDateTime(widget.note.updatedAt)}', + style: const TextStyle( + fontSize: 12, + color: Color(0xFF999999), + ), ), - ), - ], - ), - const SizedBox(height: 8), - Text( - _formatDateTime(widget.note.createdAt), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 16), - Row( - children: [ - Icon( - Icons.edit, - size: 18, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 8), - Text( - '更新时间', - style: Theme.of(context).textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.bold, + Row( + children: [ + IconButton( + icon: const Icon(Icons.edit_outlined, size: 20), + color: const Color(0xFF666666), + onPressed: () => _navigateToEdit(context), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + const SizedBox(width: 16), + IconButton( + icon: const Icon(Icons.delete_outline, size: 20), + color: Colors.red, + onPressed: () => _showDeleteDialog(context), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], ), - ), - ], - ), - const SizedBox(height: 8), - Text( - _formatDateTime(widget.note.updatedAt), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, + ], ), + ), + ), ), ], ), @@ -167,12 +146,15 @@ class _NoteDetailPageState extends State { showDialog( context: context, builder: (context) => AlertDialog( + backgroundColor: Colors.white, + elevation: 0, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), title: const Text('确认删除'), - content: Text('确定要删除"${widget.note.title}"吗?此操作不可恢复。'), + content: const Text('确定要删除这条笔记吗?'), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('取消'), + child: const Text('取消', style: TextStyle(color: Color(0xFF666666))), ), TextButton( onPressed: () async { @@ -181,16 +163,10 @@ class _NoteDetailPageState extends State { Navigator.pop(context); Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('已删除'), - behavior: SnackBarBehavior.floating, - ), + const SnackBar(content: Text('已删除')), ); }, - child: const Text( - '删除', - style: TextStyle(color: Colors.red), - ), + child: const Text('删除', style: TextStyle(color: Colors.red)), ), ], ), diff --git a/lib/pages/note_form_page.dart b/lib/pages/note_form_page.dart index c3cac78..2448070 100644 --- a/lib/pages/note_form_page.dart +++ b/lib/pages/note_form_page.dart @@ -3,9 +3,9 @@ import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; import '../models/data_models.dart'; -/// 添加/编辑笔记页面 +/// 添加/编辑笔记页面 - 极简书写界面 class NoteFormPage extends StatefulWidget { - final Note? note; // 如果为 null,则是添加模式;否则是编辑模式 + final Note? note; const NoteFormPage({super.key, this.note}); @@ -14,170 +14,270 @@ class NoteFormPage extends StatefulWidget { } class _NoteFormPageState extends State { - final _formKey = GlobalKey(); - late TextEditingController _titleController; late TextEditingController _contentController; - late TextEditingController _tagsController; + late DateTime _createdAt; + List _tags = []; + bool _isEditing = false; @override void initState() { super.initState(); - _titleController = TextEditingController(text: widget.note?.title ?? ''); - _contentController = TextEditingController(text: widget.note?.content ?? ''); - _tagsController = TextEditingController( - text: widget.note?.tags.join(', ') ?? '', - ); + final note = widget.note; + _contentController = TextEditingController(text: note?.content ?? ''); + _createdAt = note?.createdAt ?? DateTime.now(); + _tags = note != null ? List.from(note.tags) : []; + _isEditing = note != null; } @override void dispose() { - _titleController.dispose(); _contentController.dispose(); - _tagsController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - final isEdit = widget.note != null; - return Scaffold( + backgroundColor: Colors.white, appBar: AppBar( - title: Text(isEdit ? '编辑笔记' : '添加笔记'), + title: Text(_isEditing ? '编辑笔记' : '新建笔记'), actions: [ - IconButton( - icon: const Icon(Icons.save), + TextButton( onPressed: _saveNote, + child: const Text( + '保存', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), ), + const SizedBox(width: 8), ], ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 标题 - TextFormField( - controller: _titleController, - decoration: const InputDecoration( - labelText: '标题 *', - hintText: '请输入笔记标题', - prefixIcon: Icon(Icons.title), - border: OutlineInputBorder(), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return '请输入标题'; - } - return null; - }, + body: Column( + children: [ + // 顶部信息栏:创建时间 + 标签 + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5), ), - - const SizedBox(height: 16), - - // 标签 - TextFormField( - controller: _tagsController, - decoration: const InputDecoration( - labelText: '标签', - hintText: '多个标签用逗号分隔', - prefixIcon: Icon(Icons.local_offer), - border: OutlineInputBorder(), - ), - ), - - const SizedBox(height: 16), - - // 内容 - TextFormField( - controller: _contentController, - decoration: const InputDecoration( - labelText: '内容 *', - hintText: '请输入笔记内容...', - prefixIcon: Icon(Icons.edit_note), - border: OutlineInputBorder(), - alignLabelWithHint: true, - ), - maxLines: 15, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return '请输入内容'; - } - return null; - }, - ), - - const SizedBox(height: 32), - - // 保存按钮 - SizedBox( - width: double.infinity, - height: 48, - child: ElevatedButton.icon( - onPressed: _saveNote, - icon: const Icon(Icons.save), - label: Text(isEdit ? '保存修改' : '添加笔记'), - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), + ), + child: Row( + children: [ + // 创建时间 + Text( + _formatDateTime(_createdAt), + style: const TextStyle( + fontSize: 12, + color: Color(0xFF999999), ), ), - ), - ], + const SizedBox(width: 16), + // 标签 + Expanded( + child: _buildTagSelector(), + ), + ], + ), ), - ), + + // 书写区域 + Expanded( + child: TextField( + controller: _contentController, + maxLines: null, + expands: true, + textAlignVertical: TextAlignVertical.top, + style: const TextStyle( + fontSize: 16, + color: Color(0xFF1A1A1A), + height: 1.6, + ), + decoration: const InputDecoration( + hintText: '开始书写...', + hintStyle: TextStyle( + fontSize: 16, + color: Color(0xFFCCCCCC), + ), + border: InputBorder.none, + contentPadding: EdgeInsets.all(16), + ), + ), + ), + ], ), ); } + /// 构建标签选择器 + Widget _buildTagSelector() { + return Wrap( + spacing: 8, + runSpacing: 4, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ..._tags.asMap().entries.map((entry) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + border: Border.all(color: const Color(0xFFE5E5E5)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + entry.value, + style: const TextStyle( + fontSize: 12, + color: Color(0xFF666666), + ), + ), + const SizedBox(width: 4), + GestureDetector( + onTap: () => setState(() => _tags.removeAt(entry.key)), + child: const Icon( + Icons.close, + size: 12, + color: Color(0xFF999999), + ), + ), + ], + ), + ); + }), + // 添加标签按钮 + GestureDetector( + onTap: () => _showAddTagDialog(), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + border: Border.all(color: const Color(0xFFE5E5E5)), + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.add, + size: 12, + color: Color(0xFF999999), + ), + SizedBox(width: 2), + Text( + '标签', + style: TextStyle( + fontSize: 12, + color: Color(0xFF999999), + ), + ), + ], + ), + ), + ), + ], + ); + } + + /// 显示添加标签对话框 + void _showAddTagDialog() { + final controller = TextEditingController(); + + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: Colors.white, + elevation: 0, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), + title: const Text( + '添加标签', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + content: TextField( + controller: controller, + autofocus: true, + decoration: const InputDecoration( + hintText: '输入标签名称', + border: UnderlineInputBorder(), + ), + onSubmitted: (value) { + _addTag(value); + Navigator.pop(context); + }, + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消', style: TextStyle(color: Color(0xFF666666))), + ), + TextButton( + onPressed: () { + _addTag(controller.text); + Navigator.pop(context); + }, + child: const Text('添加'), + ), + ], + ), + ); + } + + /// 添加标签 + void _addTag(String tag) { + final trimmed = tag.trim(); + if (trimmed.isNotEmpty && !_tags.contains(trimmed)) { + setState(() => _tags.add(trimmed)); + } + } + + /// 格式化日期时间 + String _formatDateTime(DateTime date) { + return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}'; + } + /// 保存笔记 Future _saveNote() async { - if (!_formKey.currentState!.validate()) { + final content = _contentController.text.trim(); + + if (content.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('笔记内容不能为空')), + ); return; } - // 解析标签 - final tags = _tagsController.text - .split(',') - .map((tag) => tag.trim()) - .where((tag) => tag.isNotEmpty) - .toList(); + final now = DateTime.now(); - if (widget.note == null) { - // 添加新模式 - final now = DateTime.now(); - final newNote = Note( - id: now.millisecondsSinceEpoch.toString(), - title: _titleController.text.trim(), - content: _contentController.text.trim(), - tags: tags, - createdAt: now, + if (_isEditing) { + // 更新现有笔记 + final updatedNote = widget.note!.copyWith( + content: content, + tags: _tags, updatedAt: now, ); - - await context.read().addNote(newNote); - } else { - // 编辑现有模式 - final updatedNote = Note( - id: widget.note!.id, - title: _titleController.text.trim(), - content: _contentController.text.trim(), - tags: tags, - createdAt: widget.note!.createdAt, - updatedAt: DateTime.now(), - ); - await context.read().updateNote(updatedNote); + } else { + // 添加新笔记 + final newNote = Note( + id: now.millisecondsSinceEpoch.toString(), + content: content, + tags: _tags, + createdAt: _createdAt, + updatedAt: now, + ); + await context.read().addNote(newNote); } if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(widget.note == null ? '添加成功' : '更新成功'), + content: Text(_isEditing ? '保存成功' : '添加成功'), behavior: SnackBarBehavior.floating, ), ); diff --git a/lib/pages/note_tab_page.dart b/lib/pages/note_tab_page.dart index c8bb3a9..b4bbe6a 100644 --- a/lib/pages/note_tab_page.dart +++ b/lib/pages/note_tab_page.dart @@ -77,47 +77,43 @@ class NoteTabPage extends StatelessWidget { /// 获取示例笔记数据 List _getSampleNotes() { + final now = DateTime.now(); // 示例数据(实际应从数据库获取) return [ Note( id: '1', - title: 'Flutter 学习心得', content: '今天开始学习 Flutter 框架,感觉和 Vue 有很多相似之处,都是声明式 UI,组件化开发。Widget 的概念很有趣,一切皆 Widget。', tags: ['学习', 'Flutter', '编程'], - createdAt: DateTime(2024, 3, 1, 10, 30), - updatedAt: DateTime(2024, 3, 1, 10, 30), + createdAt: now.subtract(const Duration(days: 2)), + updatedAt: now.subtract(const Duration(days: 2)), ), Note( id: '2', - title: '《活着》读后感', content: '余华的《活着》真的是一部让人深思的作品。福贵的一生经历了太多的苦难,但他依然坚强地活着。生命的意义或许就在于活着本身。', tags: ['阅读', '感悟', '书籍'], - createdAt: DateTime(2024, 2, 20, 15, 20), - updatedAt: DateTime(2024, 2, 20, 16, 0), + createdAt: now.subtract(const Duration(days: 5)), + updatedAt: now.subtract(const Duration(days: 5)), ), Note( id: '3', - title: '电影《星际穿越》观后感', content: '诺兰的电影总是充满想象力。《星际穿越》将科幻与亲情完美结合,五维空间的呈现方式令人震撼。配乐也是一绝。', tags: ['观影', '科幻', '电影'], - createdAt: DateTime(2024, 2, 15, 20, 0), - updatedAt: DateTime(2024, 2, 15, 20, 30), + createdAt: now.subtract(const Duration(days: 10)), + updatedAt: now.subtract(const Duration(days: 10)), ), Note( id: '4', - title: 'Python 数据分析笔记', content: 'Pandas 库的 DataFrame 操作非常强大,可以方便地进行数据清洗和分析。需要多练习熟练掌握常用操作。', tags: ['Python', '数据分析', '技术'], - createdAt: DateTime(2024, 1, 10, 9, 0), - updatedAt: DateTime(2024, 1, 10, 9, 30), + createdAt: now.subtract(const Duration(days: 30)), + updatedAt: now.subtract(const Duration(days: 30)), ), Note( id: '5', - title: '生活随笔', content: '春天来了,天气渐暖。周末去公园散步,看到花开得很好。生活中的小确幸值得记录。', tags: ['生活', '随笔'], - createdAt: DateTime(2024, 3, 3, 18, 0), - updatedAt: DateTime(2024, 3, 3, 18, 0), + createdAt: now.subtract(const Duration(hours: 5)), + updatedAt: now.subtract(const Duration(hours: 5)), ), ]; } diff --git a/lib/utils/book_dao.dart b/lib/utils/book_dao.dart index ec6d3ae..be02818 100644 --- a/lib/utils/book_dao.dart +++ b/lib/utils/book_dao.dart @@ -6,25 +6,17 @@ import 'database_helper.dart'; class BookDao { final DatabaseHelper _dbHelper = DatabaseHelper.instance; - // 获取所有书籍记录 + // 获取所有未删除的书籍记录 Future> getAllBooks() async { final db = await _dbHelper.database; - final List> maps = await db.query('books'); + final List> maps = await db.query( + 'books', + where: 'is_deleted = ?', + whereArgs: [0], + orderBy: 'updated_at DESC', + ); - return List.generate(maps.length, (i) { - return Book( - id: maps[i]['id'].toString(), - title: maps[i]['title'], - author: maps[i]['author'], - cover: maps[i]['cover'], - rating: maps[i]['rating']?.toDouble(), - status: maps[i]['status'], - readDate: maps[i]['read_date'] != null - ? DateTime.parse(maps[i]['read_date']) - : null, - note: maps[i]['note'], - ); - }); + return List.generate(maps.length, (i) => Book.fromJson(maps[i])); } // 根据状态筛选书籍记录 @@ -32,24 +24,25 @@ class BookDao { final db = await _dbHelper.database; final List> maps = await db.query( 'books', - where: 'status = ?', - whereArgs: [status], + where: 'status = ? AND is_deleted = ?', + whereArgs: [status, 0], + orderBy: 'updated_at DESC', ); - return List.generate(maps.length, (i) { - return Book( - id: maps[i]['id'].toString(), - title: maps[i]['title'], - author: maps[i]['author'], - cover: maps[i]['cover'], - rating: maps[i]['rating']?.toDouble(), - status: maps[i]['status'], - readDate: maps[i]['read_date'] != null - ? DateTime.parse(maps[i]['read_date']) - : null, - note: maps[i]['note'], - ); - }); + return List.generate(maps.length, (i) => Book.fromJson(maps[i])); + } + + // 根据ID获取书籍 + Future getBookById(String id) async { + final db = await _dbHelper.database; + final List> maps = await db.query( + 'books', + where: 'id = ? AND is_deleted = ?', + whereArgs: [id, 0], + ); + + if (maps.isEmpty) return null; + return Book.fromJson(maps.first); } // 添加书籍记录 @@ -69,7 +62,21 @@ class BookDao { ); } - // 删除书籍记录 + // 软删除书籍记录 + Future softDeleteBook(String id) async { + final db = await _dbHelper.database; + return await db.update( + 'books', + { + 'is_deleted': 1, + 'updated_at': DateTime.now().toIso8601String(), + }, + where: 'id = ?', + whereArgs: [id], + ); + } + + // 彻底删除书籍记录 Future deleteBook(String id) async { final db = await _dbHelper.database; return await db.delete( @@ -78,4 +85,43 @@ class BookDao { whereArgs: [id], ); } + + // 搜索书籍(标题、别名) + Future> searchBooks(String query) async { + final db = await _dbHelper.database; + final List> maps = await db.query( + 'books', + where: '(title LIKE ? OR alternate_titles LIKE ?) AND is_deleted = ?', + whereArgs: ['%$query%', '%$query%', 0], + orderBy: 'updated_at DESC', + ); + + return List.generate(maps.length, (i) => Book.fromJson(maps[i])); + } + + // 根据作者筛选 + Future> getBooksByAuthor(String author) async { + final db = await _dbHelper.database; + final List> maps = await db.query( + 'books', + where: 'authors LIKE ? AND is_deleted = ?', + whereArgs: ['%$author%', 0], + orderBy: 'updated_at DESC', + ); + + return List.generate(maps.length, (i) => Book.fromJson(maps[i])); + } + + // 根据类型筛选 + Future> getBooksByGenre(String genre) async { + final db = await _dbHelper.database; + final List> maps = await db.query( + 'books', + where: 'genres LIKE ? AND is_deleted = ?', + whereArgs: ['%$genre%', 0], + orderBy: 'updated_at DESC', + ); + + return List.generate(maps.length, (i) => Book.fromJson(maps[i])); + } } diff --git a/lib/utils/database_helper.dart b/lib/utils/database_helper.dart index d4f8ae9..a7e1b41 100644 --- a/lib/utils/database_helper.dart +++ b/lib/utils/database_helper.dart @@ -20,7 +20,7 @@ class DatabaseHelper { return await openDatabase( path, - version: 2, + version: 4, onCreate: _createDB, onUpgrade: _onUpgrade, ); @@ -32,6 +32,110 @@ class DatabaseHelper { // 升级movies表结构 await _upgradeMoviesTableV2(db); } + if (oldVersion < 3) { + // 升级books表结构 + await _upgradeBooksTableV3(db); + } + if (oldVersion < 4) { + // 升级notes表结构 + await _upgradeNotesTableV4(db); + } + } + + /// 升级notes表到V4 + Future _upgradeNotesTableV4(Database db) async { + // 备份旧数据 + final oldData = await db.query('notes'); + + // 删除旧表 + await db.execute('DROP TABLE IF EXISTS notes'); + + // 创建新表 + await db.execute(''' + CREATE TABLE notes ( + id TEXT PRIMARY KEY, + content TEXT NOT NULL, + content_type TEXT DEFAULT 'markdown', + tags TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ) + '''); + + // 迁移旧数据(将title合并到content中) + for (final row in oldData) { + try { + final now = DateTime.now().toIso8601String(); + final title = row['title']?.toString() ?? ''; + final content = row['content']?.toString() ?? ''; + final combinedContent = title.isNotEmpty + ? '# $title\n\n$content' + : content; + + await db.insert('notes', { + 'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(), + 'content': combinedContent, + 'content_type': 'markdown', + 'tags': row['tags'] ?? '', + 'created_at': row['created_at']?.toString() ?? now, + 'updated_at': row['updated_at']?.toString() ?? now, + }); + } catch (e) { + // 忽略迁移失败的记录 + } + } + } + + /// 升级books表到V3 + Future _upgradeBooksTableV3(Database db) async { + // 备份旧数据 + final oldData = await db.query('books'); + + // 删除旧表 + await db.execute('DROP TABLE IF EXISTS books'); + + // 创建新表 + await db.execute(''' + CREATE TABLE books ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + cover_path TEXT, + authors TEXT, + alternate_titles TEXT, + publisher TEXT, + genres TEXT, + summary TEXT, + rating REAL, + status TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + is_deleted INTEGER DEFAULT 0 + ) + '''); + + // 迁移旧数据 + for (final row in oldData) { + try { + final now = DateTime.now().toIso8601String(); + await db.insert('books', { + 'id': row['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(), + 'title': row['title']?.toString() ?? '', + 'cover_path': row['cover'], + 'authors': row['author'] != null ? '["${row['author']}"]' : '[]', + 'alternate_titles': '[]', + 'publisher': null, + 'genres': '[]', + 'summary': row['note'], + 'rating': row['rating'], + 'status': row['status'] ?? 'want_to_read', + 'created_at': now, + 'updated_at': now, + 'is_deleted': 0, + }); + } catch (e) { + // 忽略迁移失败的记录 + } + } } /// 升级movies表到V2 @@ -120,26 +224,31 @@ class DatabaseHelper { // 书籍表 await db.execute(''' CREATE TABLE books ( - id $idType, - title $textType, - author TEXT, - cover TEXT, + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + cover_path TEXT, + authors TEXT, + alternate_titles TEXT, + publisher TEXT, + genres TEXT, + summary TEXT, rating REAL, - status $textType, - read_date TEXT, - note TEXT + status TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + is_deleted INTEGER DEFAULT 0 ) '''); // 笔记表 await db.execute(''' CREATE TABLE notes ( - id $idType, - title $textType, - content $textType, + id TEXT PRIMARY KEY, + content TEXT NOT NULL, + content_type TEXT DEFAULT 'markdown', tags TEXT, - created_at $textType, - updated_at $textType + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL ) '''); } diff --git a/lib/utils/note_dao.dart b/lib/utils/note_dao.dart index 41a8466..d5480ecd 100644 --- a/lib/utils/note_dao.dart +++ b/lib/utils/note_dao.dart @@ -14,31 +14,26 @@ class NoteDao { orderBy: 'updated_at DESC', ); - return List.generate(maps.length, (i) { - return Note( - id: maps[i]['id'].toString(), - title: maps[i]['title'], - content: maps[i]['content'], - tags: maps[i]['tags'] != null - ? List.from(maps[i]['tags'].split(',')) - : [], - createdAt: DateTime.parse(maps[i]['created_at']), - updatedAt: DateTime.parse(maps[i]['updated_at']), - ); - }); + return List.generate(maps.length, (i) => Note.fromJson(maps[i])); + } + + // 根据ID获取笔记 + Future getNoteById(String id) async { + final db = await _dbHelper.database; + final List> maps = await db.query( + 'notes', + where: 'id = ?', + whereArgs: [id], + ); + + if (maps.isEmpty) return null; + return Note.fromJson(maps.first); } // 添加笔记 Future insertNote(Note note) async { final db = await _dbHelper.database; - return await db.insert('notes', { - 'id': note.id, - 'title': note.title, - 'content': note.content, - 'tags': note.tags.join(','), - 'created_at': note.createdAt.toIso8601String(), - 'updated_at': note.updatedAt.toIso8601String(), - }); + return await db.insert('notes', note.toJson()); } // 更新笔记 @@ -46,12 +41,7 @@ class NoteDao { final db = await _dbHelper.database; return await db.update( 'notes', - { - 'title': note.title, - 'content': note.content, - 'tags': note.tags.join(','), - 'updated_at': note.updatedAt.toIso8601String(), - }, + note.toJson(), where: 'id = ?', whereArgs: [note.id], ); @@ -66,4 +56,30 @@ class NoteDao { whereArgs: [id], ); } + + // 搜索笔记 + Future> searchNotes(String query) async { + final db = await _dbHelper.database; + final List> maps = await db.query( + 'notes', + where: 'content LIKE ? OR tags LIKE ?', + whereArgs: ['%$query%', '%$query%'], + orderBy: 'updated_at DESC', + ); + + return List.generate(maps.length, (i) => Note.fromJson(maps[i])); + } + + // 根据标签筛选 + Future> getNotesByTag(String tag) async { + final db = await _dbHelper.database; + final List> maps = await db.query( + 'notes', + where: 'tags LIKE ?', + whereArgs: ['%$tag%'], + orderBy: 'updated_at DESC', + ); + + return List.generate(maps.length, (i) => Note.fromJson(maps[i])); + } } diff --git a/lib/widgets/book_list_item.dart b/lib/widgets/book_list_item.dart index df46021..741a8ba 100644 --- a/lib/widgets/book_list_item.dart +++ b/lib/widgets/book_list_item.dart @@ -1,217 +1,220 @@ +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; import '../models/data_models.dart'; -import '../utils/app_theme.dart'; -/// 书籍列表项组件 +/// 书籍列表项组件 - 极简主义设计 class BookListItem extends StatelessWidget { final Book book; - + const BookListItem({super.key, required this.book}); - + @override Widget build(BuildContext context) { - return Card( - margin: const EdgeInsets.only(bottom: 12), - child: InkWell( - onTap: () { - // 跳转到详情页 - Navigator.pushNamed(context, '/book-detail', arguments: book); - }, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 封面占位图 - _buildCover(), - - const SizedBox(width: 12), - - // 书籍信息 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 标题 + return InkWell( + onTap: () { + Navigator.pushNamed(context, '/book-detail', arguments: book); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 封面 + _buildCover(), + + const SizedBox(width: 16), + + // 信息 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 书名 + Text( + book.title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFF1A1A1A), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + + const SizedBox(height: 4), + + // 作者 + if (book.authors.isNotEmpty) Text( - book.title, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, + book.authors.join(' / '), + style: const TextStyle( + fontSize: 13, + color: Color(0xFF666666), ), maxLines: 1, overflow: TextOverflow.ellipsis, ), - - if (book.author != null) ...[ - const SizedBox(height: 4), - Text( - book.author!, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, + + const SizedBox(height: 8), + + // 评分和状态 + Row( + children: [ + if (book.rating != null) ...[ + const Icon( + Icons.star, + size: 14, + color: Color(0xFF1A1A1A), ), - ), - ], - - const SizedBox(height: 8), - - // 评分和状态 - Row( - children: [ - if (book.rating != null) ...[ - Icon( - Icons.star, - size: 16, - color: Colors.amber[700], + const SizedBox(width: 2), + Text( + book.rating!.toStringAsFixed(1), + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Color(0xFF1A1A1A), ), - const SizedBox(width: 4), - Text( - book.rating.toString(), - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.amber[700], - ), - ), - const SizedBox(width: 12), - ], - - // 状态标签 - _buildStatusTag(context), + ), + const SizedBox(width: 12), ], + _buildStatusTag(), + ], + ), + + // 类型 + if (book.genres.isNotEmpty) ...[ + const SizedBox(height: 8), + Text( + book.genres.take(3).join(' · '), + style: const TextStyle( + fontSize: 12, + color: Color(0xFF999999), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - - if (book.readDate != null) ...[ - const SizedBox(height: 6), - Text( - '阅读日期:${_formatDate(book.readDate!)}', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - - if (book.note != null && book.note!.isNotEmpty) ...[ - const SizedBox(height: 6), - Text( - book.note!, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], ], - ), - ), - - // 右侧操作按钮 - Column( - children: [ - IconButton( - icon: const Icon(Icons.edit, size: 20), - onPressed: () { - Navigator.pushNamed(context, '/book-form', arguments: book); - }, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - IconButton( - icon: const Icon(Icons.delete_outline, size: 20), - color: Colors.red, - onPressed: () => _showDeleteDialog(context, book), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), ], ), - ], - ), + ), + + // 操作按钮 + Column( + children: [ + IconButton( + icon: const Icon(Icons.edit_outlined, size: 20), + color: const Color(0xFF666666), + onPressed: () { + Navigator.pushNamed(context, '/book-form', arguments: book); + }, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + const SizedBox(height: 8), + IconButton( + icon: const Icon(Icons.delete_outline, size: 20), + color: Colors.red, + onPressed: () => _showDeleteDialog(context), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + ], ), ), ); } - - /// 构建封面占位图 + + /// 构建封面 Widget _buildCover() { return Container( width: 60, - height: 90, + height: 80, decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(8), + color: const Color(0xFFF5F5F5), + border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5), ), + child: book.coverPath != null && book.coverPath!.isNotEmpty + ? Image.file( + File(book.coverPath!), + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _buildCoverPlaceholder(), + ) + : _buildCoverPlaceholder(), + ); + } + + Widget _buildCoverPlaceholder() { + return const Center( child: Icon( Icons.menu_book, - color: Colors.grey[500], - size: 32, + size: 24, + color: Color(0xFFCCCCCC), ), ); } - + /// 构建状态标签 - Widget _buildStatusTag(BuildContext context) { - Color statusColor; - String statusText; - + Widget _buildStatusTag() { + String label; + Color color; switch (book.status) { case 'read': - statusColor = AppTheme.readColor; - statusText = '读完'; + label = '已读'; + color = const Color(0xFF1A1A1A); break; case 'reading': - statusColor = AppTheme.readingColor; - statusText = '在读'; + label = '在读'; + color = const Color(0xFF666666); break; case 'want_to_read': - statusColor = AppTheme.wantToReadColor; - statusText = '准备读'; + label = '想读'; + color = const Color(0xFF999999); break; default: - statusColor = Colors.grey; - statusText = '未知'; + label = '未知'; + color = const Color(0xFFCCCCCC); } return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( - color: statusColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - border: Border.all( - color: statusColor, - width: 1, - ), + color: color.withValues(alpha: 0.1), + border: Border.all(color: color), ), child: Text( - statusText, + label, style: TextStyle( fontSize: 11, - color: statusColor, - fontWeight: FontWeight.bold, + color: color, + fontWeight: FontWeight.w500, ), ), ); } - - /// 格式化日期 - String _formatDate(DateTime date) { - return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; - } - + /// 显示删除对话框 - void _showDeleteDialog(BuildContext context, Book book) { + void _showDeleteDialog(BuildContext context) { showDialog( context: context, builder: (context) => AlertDialog( + backgroundColor: Colors.white, + elevation: 0, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), title: const Text('确认删除'), - content: Text('确定要删除"${book.title}"吗?此操作不可恢复。'), + content: Text('确定要删除"${book.title}"吗?'), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('取消'), + child: const Text('取消', style: TextStyle(color: Color(0xFF666666))), ), TextButton( onPressed: () async { @@ -219,16 +222,10 @@ class BookListItem extends StatelessWidget { if (!context.mounted) return; Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('已删除'), - behavior: SnackBarBehavior.floating, - ), + const SnackBar(content: Text('已删除')), ); }, - child: const Text( - '删除', - style: TextStyle(color: Colors.red), - ), + child: const Text('删除', style: TextStyle(color: Colors.red)), ), ], ), diff --git a/lib/widgets/book_status_bar.dart b/lib/widgets/book_status_bar.dart index 260467e..d6feaa4 100644 --- a/lib/widgets/book_status_bar.dart +++ b/lib/widgets/book_status_bar.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; -import '../utils/app_theme.dart'; -/// 阅读状态选择栏 +/// 阅读状态选择栏 - 极简主义设计 class BookStatusBar extends StatelessWidget { const BookStatusBar({super.key}); @@ -12,44 +11,34 @@ class BookStatusBar extends StatelessWidget { return Consumer( builder: (context, provider, child) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + decoration: const BoxDecoration( + color: Colors.white, + border: Border( + bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5), + ), ), child: Row( children: [ _buildStatusItem( context, - '读完', - AppTheme.readColor, - Icons.check_circle, + '已读', 0, provider.bookStatusIndex, () => provider.setBookStatusIndex(0), ), - const SizedBox(width: 12), + const SizedBox(width: 16), _buildStatusItem( context, '在读', - AppTheme.readingColor, - Icons.auto_stories, 1, provider.bookStatusIndex, () => provider.setBookStatusIndex(1), ), - const SizedBox(width: 12), + const SizedBox(width: 16), _buildStatusItem( context, - '准备读', - AppTheme.wantToReadColor, - Icons.bookmark_border, + '想读', 2, provider.bookStatusIndex, () => provider.setBookStatusIndex(2), @@ -65,46 +54,44 @@ class BookStatusBar extends StatelessWidget { Widget _buildStatusItem( BuildContext context, String label, - Color color, - IconData icon, int index, int currentIndex, VoidCallback onTap, ) { final isSelected = index == currentIndex; + Color color; + switch (index) { + case 0: + color = const Color(0xFF1A1A1A); + break; + case 1: + color = const Color(0xFF666666); + break; + case 2: + color = const Color(0xFF999999); + break; + default: + color = const Color(0xFFCCCCCC); + } + return Expanded( child: InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( - color: isSelected ? color.withOpacity(0.1) : Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: isSelected ? color : Colors.transparent, - width: 2, - ), + color: isSelected ? color : Colors.transparent, + border: Border.all(color: color), ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - icon, - color: isSelected ? color : Theme.of(context).colorScheme.onSurfaceVariant, - size: 20, - ), - const SizedBox(height: 4), - Text( - label, - style: TextStyle( - fontSize: 13, - fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, - color: isSelected ? color : Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], + child: Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal, + color: isSelected ? Colors.white : color, + ), ), ), ), diff --git a/lib/widgets/movie_list_item.dart b/lib/widgets/movie_list_item.dart index 431e36d..e670d97 100644 --- a/lib/widgets/movie_list_item.dart +++ b/lib/widgets/movie_list_item.dart @@ -1,17 +1,21 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../providers/app_provider.dart'; import '../models/data_models.dart'; -/// 观影列表项 - 极简主义设计 +/// 观影列表项组件 - 极简主义设计 class MovieListItem extends StatelessWidget { final Movie movie; - + const MovieListItem({super.key, required this.movie}); - + @override Widget build(BuildContext context) { return InkWell( - onTap: () => Navigator.pushNamed(context, '/movie-detail', arguments: movie), + onTap: () { + Navigator.pushNamed(context, '/movie-detail', arguments: movie); + }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), decoration: const BoxDecoration( @@ -32,7 +36,7 @@ class MovieListItem extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 标题 + // 影视名称 Text( movie.title, style: const TextStyle( @@ -44,12 +48,7 @@ class MovieListItem extends StatelessWidget { overflow: TextOverflow.ellipsis, ), - const SizedBox(height: 6), - - // 年份和评分 - _buildMetaInfo(), - - const SizedBox(height: 8), + const SizedBox(height: 4), // 导演 if (movie.directors.isNotEmpty) @@ -57,7 +56,7 @@ class MovieListItem extends StatelessWidget { movie.directors.take(2).join(' / '), style: const TextStyle( fontSize: 13, - color: Color(0xFF999999), + color: Color(0xFF666666), ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -65,102 +64,108 @@ class MovieListItem extends StatelessWidget { const SizedBox(height: 8), - // 状态 - _buildStatusTag(), + // 评分和状态 + Row( + children: [ + if (movie.rating != null) ...[ + const Icon( + Icons.star, + size: 14, + color: Color(0xFF1A1A1A), + ), + const SizedBox(width: 2), + Text( + movie.rating!.toStringAsFixed(1), + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Color(0xFF1A1A1A), + ), + ), + const SizedBox(width: 12), + ], + _buildStatusTag(), + ], + ), + + // 类型 + if (movie.genres.isNotEmpty) ...[ + const SizedBox(height: 8), + Text( + movie.genres.take(3).join(' · '), + style: const TextStyle( + fontSize: 12, + color: Color(0xFF999999), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], ], ), ), - // 箭头 - const Icon( - Icons.chevron_right, - size: 20, - color: Color(0xFFCCCCCC), + // 操作按钮 + Column( + children: [ + IconButton( + icon: const Icon(Icons.edit_outlined, size: 20), + color: const Color(0xFF666666), + onPressed: () { + Navigator.pushNamed(context, '/movie-form', arguments: movie); + }, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + const SizedBox(height: 8), + IconButton( + icon: const Icon(Icons.delete_outline, size: 20), + color: Colors.red, + onPressed: () => _showDeleteDialog(context), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], ), ], ), ), ); } - - /// 海报 + + /// 构建海报 Widget _buildPoster() { return Container( - width: 56, + width: 60, height: 80, decoration: BoxDecoration( color: const Color(0xFFF5F5F5), - border: Border.all( - color: const Color(0xFFE5E5E5), - width: 0.5, - ), + border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5), ), child: movie.posterPath != null && movie.posterPath!.isNotEmpty ? Image.file( File(movie.posterPath!), fit: BoxFit.cover, - errorBuilder: (_, __, ___) => _buildPlaceholder(), + errorBuilder: (_, __, ___) => _buildPosterPlaceholder(), ) - : _buildPlaceholder(), + : _buildPosterPlaceholder(), ); } - - Widget _buildPlaceholder() { + + Widget _buildPosterPlaceholder() { return const Center( child: Icon( Icons.movie_outlined, - size: 20, + size: 24, color: Color(0xFFCCCCCC), ), ); } - - /// 元信息 - Widget _buildMetaInfo() { - final items = []; - - if (movie.releaseDate != null) { - items.add('${movie.releaseDate!.year}'); - } - if (movie.rating != null) { - items.add(movie.rating!.toStringAsFixed(1)); - } - - if (items.isEmpty) return const SizedBox.shrink(); - - return Row( - children: items.asMap().entries.map((entry) { - return Row( - children: [ - Text( - entry.value, - style: const TextStyle( - fontSize: 13, - color: Color(0xFF999999), - ), - ), - if (entry.key < items.length - 1) - const Padding( - padding: EdgeInsets.symmetric(horizontal: 8), - child: Text( - '·', - style: TextStyle( - fontSize: 13, - color: Color(0xFFCCCCCC), - ), - ), - ), - ], - ); - }).toList(), - ); - } - - /// 状态标签 + + /// 构建状态标签 Widget _buildStatusTag() { String label; Color color; - switch (movie.status) { case 'watched': label = '已看'; @@ -179,12 +184,50 @@ class MovieListItem extends StatelessWidget { color = const Color(0xFFCCCCCC); } - return Text( - label, - style: TextStyle( - fontSize: 12, - color: color, - fontWeight: FontWeight.w500, + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + border: Border.all(color: color), + ), + child: Text( + label, + style: TextStyle( + fontSize: 11, + color: color, + fontWeight: FontWeight.w500, + ), + ), + ); + } + + /// 显示删除对话框 + void _showDeleteDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: Colors.white, + elevation: 0, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), + title: const Text('确认删除'), + content: Text('确定要删除"${movie.title}"吗?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消', style: TextStyle(color: Color(0xFF666666))), + ), + TextButton( + onPressed: () async { + await context.read().removeMovie(movie.id); + if (!context.mounted) return; + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('已删除')), + ); + }, + child: const Text('删除', style: TextStyle(color: Colors.red)), + ), + ], ), ); } diff --git a/lib/widgets/movie_status_bar.dart b/lib/widgets/movie_status_bar.dart index 40d4f72..31ebbfc 100644 --- a/lib/widgets/movie_status_bar.dart +++ b/lib/widgets/movie_status_bar.dart @@ -12,25 +12,33 @@ class MovieStatusBar extends StatelessWidget { builder: (context, provider, child) { return Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - color: Colors.white, + decoration: const BoxDecoration( + color: Colors.white, + border: Border( + bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5), + ), + ), child: Row( children: [ _buildStatusItem( + context, '已看', 0, provider.movieStatusIndex, () => provider.setMovieStatusIndex(0), ), - const SizedBox(width: 24), + const SizedBox(width: 16), _buildStatusItem( - '想看', + context, + '在看', 1, provider.movieStatusIndex, () => provider.setMovieStatusIndex(1), ), - const SizedBox(width: 24), + const SizedBox(width: 16), _buildStatusItem( - '在看', + context, + '想看', 2, provider.movieStatusIndex, () => provider.setMovieStatusIndex(2), @@ -44,6 +52,7 @@ class MovieStatusBar extends StatelessWidget { /// 构建状态项 Widget _buildStatusItem( + BuildContext context, String label, int index, int currentIndex, @@ -51,14 +60,39 @@ class MovieStatusBar extends StatelessWidget { ) { final isSelected = index == currentIndex; - return GestureDetector( - onTap: onTap, - child: Text( - label, - style: TextStyle( - fontSize: 15, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, - color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFF999999), + Color color; + switch (index) { + case 0: + color = const Color(0xFF1A1A1A); + break; + case 1: + color = const Color(0xFF666666); + break; + case 2: + color = const Color(0xFF999999); + break; + default: + color = const Color(0xFFCCCCCC); + } + + return Expanded( + child: InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + color: isSelected ? color : Colors.transparent, + border: Border.all(color: color), + ), + child: Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal, + color: isSelected ? Colors.white : color, + ), + ), ), ), ); diff --git a/lib/widgets/note_list_item.dart b/lib/widgets/note_list_item.dart index d054ab6..3a4520c 100644 --- a/lib/widgets/note_list_item.dart +++ b/lib/widgets/note_list_item.dart @@ -3,7 +3,7 @@ import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; import '../models/data_models.dart'; -/// 笔记列表项组件 +/// 笔记列表项组件 - 极简主义设计 class NoteListItem extends StatelessWidget { final Note note; @@ -11,104 +11,75 @@ class NoteListItem extends StatelessWidget { @override Widget build(BuildContext context) { - return Card( - margin: const EdgeInsets.only(bottom: 12), - child: InkWell( - onTap: () { - // 跳转到详情页 - Navigator.pushNamed(context, '/note-detail', arguments: note); - }, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 标题 - Text( - note.title, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - - const SizedBox(height: 8), - - // 内容摘要 - Text( - note.content, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - - const SizedBox(height: 12), - - // 标签 - if (note.tags.isNotEmpty) ...[ - Wrap( - spacing: 6, - runSpacing: 6, - children: note.tags.map((tag) => _buildTag(context, tag)).toList(), - ), - ], - - const SizedBox(height: 12), - - // 时间信息和操作按钮 - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - _formatDate(note.updatedAt), - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.edit, size: 20), - onPressed: () { - Navigator.pushNamed(context, '/note-form', arguments: note); - }, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - IconButton( - icon: const Icon(Icons.delete_outline, size: 20), - color: Colors.red, - onPressed: () => _showDeleteDialog(context, note), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - ], - ), - ], - ), - ], + return InkWell( + onTap: () { + Navigator.pushNamed(context, '/note-detail', arguments: note); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5), ), ), - ), - ); - } - - /// 构建标签 - Widget _buildTag(BuildContext context, String tag) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(12), - ), - child: Text( - '#$tag', - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.onPrimaryContainer, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 内容摘要 + Text( + note.summary, + style: const TextStyle( + fontSize: 15, + color: Color(0xFF1A1A1A), + height: 1.5, + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + + const SizedBox(height: 12), + + // 底部信息:标签 + 时间 + 操作 + Row( + children: [ + // 标签 + if (note.tags.isNotEmpty) ...[ + Expanded( + child: Wrap( + spacing: 8, + runSpacing: 4, + children: note.tags.take(3).map((tag) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + border: Border.all(color: const Color(0xFFE5E5E5)), + ), + child: Text( + tag, + style: const TextStyle( + fontSize: 11, + color: Color(0xFF666666), + ), + ), + ); + }).toList(), + ), + ), + ] else + const Spacer(), + + // 时间 + Text( + _formatDate(note.updatedAt), + style: const TextStyle( + fontSize: 12, + color: Color(0xFF999999), + ), + ), + ], + ), + ], ), ), ); @@ -133,38 +104,4 @@ class NoteListItem extends StatelessWidget { return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; } } - - /// 显示删除对话框 - void _showDeleteDialog(BuildContext context, Note note) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('确认删除'), - content: Text('确定要删除"${note.title}"吗?此操作不可恢复。'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('取消'), - ), - TextButton( - onPressed: () async { - await context.read().removeNote(note.id); - if (!context.mounted) return; - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('已删除'), - behavior: SnackBarBehavior.floating, - ), - ); - }, - child: const Text( - '删除', - style: TextStyle(color: Colors.red), - ), - ), - ], - ), - ); - } }