笔记功能初步完善

This commit is contained in:
DelLevin-Home
2026-03-06 13:49:13 +08:00
parent 3ebc71da7d
commit 17de976d4b
20 changed files with 3058 additions and 1951 deletions

3
devtools_options.yaml Normal file
View File

@@ -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:

View File

@@ -89,8 +89,8 @@ class Movie {
return File(posterPath!); return File(posterPath!);
} }
/// 解析字符串列表 /// 解析字符串列表公共静态方法供Book使用
static List<String> _parseStringList(dynamic data) { static List<String> parseStringList(dynamic data) {
if (data == null) return []; if (data == null) return [];
if (data is List) { if (data is List) {
return data.map((e) => e.toString()).toList(); return data.map((e) => e.toString()).toList();
@@ -109,6 +109,9 @@ class Movie {
} }
return []; return [];
} }
/// 解析字符串列表(私有别名,保持兼容性)
static List<String> _parseStringList(dynamic data) => parseStringList(data);
/// 复制并修改 /// 复制并修改
Movie copyWith({ Movie copyWith({
@@ -151,37 +154,54 @@ class Movie {
/// 书籍条目模型 /// 书籍条目模型
class Book { class Book {
final String id; final String id;
final String title; final String title; // 书籍名称
final String? author; final String? coverPath; // 本地封面路径
final String? cover; final List<String> authors; // 作者列表
final double? rating; final List<String> alternateTitles; // 别名
final String status; // 'read', 'reading', 'want_to_read' final String? publisher; // 出版社
final DateTime? readDate; final List<String> genres; // 类型
final String? note; 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({ Book({
required this.id, required this.id,
required this.title, required this.title,
this.author, this.coverPath,
this.cover, this.authors = const [],
this.alternateTitles = const [],
this.publisher,
this.genres = const [],
this.summary,
this.rating, this.rating,
required this.status, required this.status,
this.readDate, required this.createdAt,
this.note, required this.updatedAt,
this.isDeleted = false,
}); });
factory Book.fromJson(Map<String, dynamic> json) { factory Book.fromJson(Map<String, dynamic> json) {
return Book( return Book(
id: json['id'] ?? '', id: json['id'] ?? '',
title: json['title'] ?? '', title: json['title'] ?? '',
author: json['author'], coverPath: json['cover_path'],
cover: json['cover'], 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(), rating: json['rating']?.toDouble(),
status: json['status'] ?? 'want_to_read', status: json['status'] ?? 'want_to_read',
readDate: json['read_date'] != null createdAt: json['created_at'] != null
? DateTime.parse(json['read_date']) ? DateTime.parse(json['created_at'])
: null, : DateTime.now(),
note: json['note'], 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 { return {
'id': id, 'id': id,
'title': title, 'title': title,
'author': author, 'cover_path': coverPath,
'cover': cover, 'authors': jsonEncode(authors),
'alternate_titles': jsonEncode(alternateTitles),
'publisher': publisher,
'genres': jsonEncode(genres),
'summary': summary,
'rating': rating, 'rating': rating,
'status': status, 'status': status,
'read_date': readDate?.toIso8601String(), 'created_at': createdAt.toIso8601String(),
'note': note, '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<String>? authors,
List<String>? alternateTitles,
String? publisher,
List<String>? 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 { class Note {
final String id; final String id;
final String title;
final String content; final String content;
final String contentType; // markdown / rich_text
final List<String> tags; final List<String> tags;
final DateTime createdAt; final DateTime createdAt;
final DateTime updatedAt; final DateTime updatedAt;
Note({ Note({
required this.id, required this.id,
required this.title,
required this.content, required this.content,
this.contentType = 'markdown',
this.tags = const [], this.tags = const [],
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
@@ -219,12 +283,10 @@ class Note {
factory Note.fromJson(Map<String, dynamic> json) { factory Note.fromJson(Map<String, dynamic> json) {
return Note( return Note(
id: json['id'] ?? '', id: json['id']?.toString() ?? '',
title: json['title'] ?? '',
content: json['content'] ?? '', content: json['content'] ?? '',
tags: json['tags'] != null contentType: json['content_type'] ?? 'markdown',
? List<String>.from(json['tags']) tags: Movie.parseStringList(json['tags']),
: [],
createdAt: json['created_at'] != null createdAt: json['created_at'] != null
? DateTime.parse(json['created_at']) ? DateTime.parse(json['created_at'])
: DateTime.now(), : DateTime.now(),
@@ -237,12 +299,37 @@ class Note {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'id': id, 'id': id,
'title': title,
'content': content, 'content': content,
'tags': tags, 'content_type': contentType,
'tags': jsonEncode(tags),
'created_at': createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
}; };
} }
/// 复制并修改
Note copyWith({
String? id,
String? content,
String? contentType,
List<String>? 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)}...';
}
} }

View File

@@ -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<String>? 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,
);
}
}

View File

@@ -1,272 +1,474 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/app_provider.dart'; import '../providers/app_provider.dart';
import '../models/data_models.dart'; import '../models/data_models.dart';
import '../utils/app_theme.dart';
/// 书籍详情页 /// 书籍详情页 - 极简主义设计
class BookDetailPage extends StatefulWidget { class BookDetailPage extends StatefulWidget {
final Book book; final Book book;
const BookDetailPage({super.key, required this.book}); const BookDetailPage({super.key, required this.book});
@override @override
State<BookDetailPage> createState() => _BookDetailPageState(); State<BookDetailPage> createState() => _BookDetailPageState();
} }
class _BookDetailPageState extends State<BookDetailPage> { class _BookDetailPageState extends State<BookDetailPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( backgroundColor: Colors.white,
title: Text(widget.book.title), body: CustomScrollView(
actions: [ slivers: [
IconButton( // 顶部封面区域
icon: const Icon(Icons.edit), _buildSliverAppBar(),
onPressed: () => _navigateToEdit(context),
), // 内容区域
IconButton( SliverToBoxAdapter(
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(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Icon( // 基本信息
Icons.menu_book, _buildBasicInfo(),
size: 80,
color: Theme.of(context).colorScheme.primary.withOpacity(0.6), const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
),
const SizedBox(height: 16), // 作者信息
Text( _buildAuthorsSection(),
widget.book.title,
style: Theme.of(context).textTheme.headlineSmall?.copyWith( // 出版社
fontWeight: FontWeight.bold, if (widget.book.publisher != null && widget.book.publisher!.isNotEmpty)
color: Theme.of(context).colorScheme.onSurface, _buildPublisherSection(),
),
textAlign: TextAlign.center, // 类型
maxLines: 2, if (widget.book.genres.isNotEmpty)
overflow: TextOverflow.ellipsis, _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(),
); );
} }
/// 构建状态标签 /// 构建顶部 AppBar
Widget _buildStatusTag(BuildContext context) { Widget _buildSliverAppBar() {
Color statusColor; return SliverAppBar(
String statusText; expandedHeight: 280,
pinned: true,
switch (widget.book.status) { backgroundColor: Colors.white,
case 'read': flexibleSpace: FlexibleSpaceBar(
statusColor = AppTheme.readColor; background: _buildCoverSection(),
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),
), ),
child: Text( actions: [
statusText, IconButton(
style: const TextStyle( icon: const Icon(Icons.edit_outlined),
fontSize: 14, onPressed: () => _navigateToEdit(context),
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,
),
), ),
const SizedBox(width: 8),
], ],
); );
} }
/// 构建笔记区域 /// 构建封面区域
Widget _buildNoteSection(BuildContext context) { Widget _buildCoverSection() {
return Container( return Container(
margin: const EdgeInsets.all(16), width: double.infinity,
padding: const EdgeInsets.all(16), color: const Color(0xFFF5F5F5),
decoration: BoxDecoration( child: widget.book.coverPath != null && widget.book.coverPath!.isNotEmpty
color: Theme.of(context).colorScheme.surfaceContainerHighest, ? Image.file(
borderRadius: BorderRadius.circular(12), File(widget.book.coverPath!),
), fit: BoxFit.contain,
errorBuilder: (_, __, ___) => _buildCoverPlaceholder(),
)
: _buildCoverPlaceholder(),
);
}
Widget _buildCoverPlaceholder() {
return const Center(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Row( Icon(
children: [ Icons.menu_book,
Icon( size: 64,
Icons.edit_note, color: Color(0xFFCCCCCC),
size: 20,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
'笔记',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
), ),
const SizedBox(height: 12), SizedBox(height: 16),
Text( 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) { String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
} }
/// 跳转到编辑页面 /// 跳转到编辑页面
void _navigateToEdit(BuildContext context) { void _navigateToEdit(BuildContext context) {
Navigator.pushNamed(context, '/book-form', arguments: widget.book).then((_) { Navigator.pushNamed(context, '/book-form', arguments: widget.book).then((_) {
context.read<AppProvider>().loadBooks(); context.read<AppProvider>().loadBooks();
}); });
} }
/// 显示删除对话框 /// 显示删除对话框
void _showDeleteDialog(BuildContext context) { void _showDeleteDialog(BuildContext context) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('确认删除'), title: const Text('确认删除'),
content: Text('确定要删除"${widget.book.title}"吗?此操作不可恢复。'), content: Text('确定要删除"${widget.book.title}"吗?'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('取消'), child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
@@ -275,16 +477,10 @@ class _BookDetailPageState extends State<BookDetailPage> {
Navigator.pop(context); Navigator.pop(context);
Navigator.pop(context); Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(content: Text('已删除')),
content: Text('已删除'),
behavior: SnackBarBehavior.floating,
),
); );
}, },
child: const Text( child: const Text('删除', style: TextStyle(color: Colors.red)),
'删除',
style: TextStyle(color: Colors.red),
),
), ),
], ],
), ),

View File

@@ -1,287 +1,554 @@
import 'dart:io';
import 'package:flutter/material.dart'; 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 'package:provider/provider.dart';
import '../providers/app_provider.dart'; import '../providers/app_provider.dart';
import '../models/data_models.dart'; import '../models/data_models.dart';
/// 添加/编辑书籍记录页面 /// 添加/编辑书籍页面 - 极简主义设计
class BookFormPage extends StatefulWidget { class BookFormPage extends StatefulWidget {
final Book? book; // 如果为 null则是添加模式否则是编辑模式 final Book? book;
const BookFormPage({super.key, this.book}); const BookFormPage({super.key, this.book});
@override @override
State<BookFormPage> createState() => _BookFormPageState(); State<BookFormPage> createState() => _BookFormPageState();
} }
class _BookFormPageState extends State<BookFormPage> { class _BookFormPageState extends State<BookFormPage> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final ImagePicker _picker = ImagePicker();
late TextEditingController _titleController; late TextEditingController _titleController;
late TextEditingController _authorController; late TextEditingController _publisherController;
late TextEditingController _summaryController;
late TextEditingController _ratingController; late TextEditingController _ratingController;
late TextEditingController _noteController;
late String _status; List<String> _authors = [];
DateTime? _readDate; List<String> _alternateTitles = [];
List<String> _genres = [];
String? _coverPath;
String _status = 'want_to_read';
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_titleController = TextEditingController(text: widget.book?.title ?? ''); final book = widget.book;
_authorController = TextEditingController(text: widget.book?.author ?? ''); _titleController = TextEditingController(text: book?.title ?? '');
_ratingController = TextEditingController(text: widget.book?.rating?.toString() ?? ''); _publisherController = TextEditingController(text: book?.publisher ?? '');
_noteController = TextEditingController(text: widget.book?.note ?? ''); _summaryController = TextEditingController(text: book?.summary ?? '');
_status = widget.book?.status ?? 'want_to_read'; _ratingController = TextEditingController(text: book?.rating?.toString() ?? '');
_readDate = widget.book?.readDate;
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 @override
void dispose() { void dispose() {
_titleController.dispose(); _titleController.dispose();
_authorController.dispose(); _publisherController.dispose();
_summaryController.dispose();
_ratingController.dispose(); _ratingController.dispose();
_noteController.dispose();
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isEdit = widget.book != null; final isEdit = widget.book != null;
return Scaffold( return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar( appBar: AppBar(
title: Text(isEdit ? '编辑书籍' : '添加书籍'), title: Text(isEdit ? '编辑书籍' : '添加书籍'),
actions: [ actions: [
IconButton( TextButton(
icon: const Icon(Icons.save),
onPressed: _saveBook, onPressed: _saveBook,
child: const Text(
'保存',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
), ),
const SizedBox(width: 8),
], ],
), ),
body: SingleChildScrollView( body: Form(
padding: const EdgeInsets.all(16), key: _formKey,
child: Form( child: ListView(
key: _formKey, padding: const EdgeInsets.all(24),
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, // 封面选择
children: [ _buildCoverPicker(),
// 标题
TextFormField( const SizedBox(height: 32),
controller: _titleController,
decoration: const InputDecoration( // 基本信息
labelText: '书名 *', _buildSectionTitle('基本信息'),
hintText: '请输入书名', const SizedBox(height: 16),
prefixIcon: Icon(Icons.menu_book),
border: OutlineInputBorder(), // 书名
), _buildTextField(
validator: (value) { controller: _titleController,
if (value == null || value.trim().isEmpty) { label: '书名 *',
return '请输入书名'; hint: '请输入书名',
} validator: (value) {
return null; if (value == null || value.trim().isEmpty) {
}, return '请输入书名';
), }
return null;
const SizedBox(height: 16), },
),
// 作者
TextFormField( const SizedBox(height: 16),
controller: _authorController,
decoration: const InputDecoration( // 别名
labelText: '作者', _buildTagInput(
hintText: '请输入作者', label: '别名',
prefixIcon: Icon(Icons.person), hint: '输入别名,按回车添加',
border: OutlineInputBorder(), tags: _alternateTitles,
), onAdd: (tag) => setState(() => _alternateTitles.add(tag)),
), onRemove: (index) => setState(() => _alternateTitles.removeAt(index)),
),
const SizedBox(height: 16),
const SizedBox(height: 24),
// 年份和评分
Row( // 作者
children: [ _buildSectionTitle('作者'),
Expanded( const SizedBox(height: 16),
child: TextFormField(
controller: _ratingController, _buildTagInput(
decoration: const InputDecoration( label: '作者',
labelText: '评分', hint: '输入作者,按回车添加',
hintText: '0-10', tags: _authors,
prefixIcon: Icon(Icons.star), onAdd: (tag) => setState(() => _authors.add(tag)),
border: OutlineInputBorder(), onRemove: (index) => setState(() => _authors.removeAt(index)),
), ),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
validator: (value) { const SizedBox(height: 24),
if (value != null && value.isNotEmpty) {
final rating = double.tryParse(value); // 出版社
if (rating == null || rating < 0 || rating > 10) { _buildTextField(
return '评分必须在 0-10 之间'; 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<String>(
value: _status,
decoration: const InputDecoration(
labelText: '状态',
prefixIcon: Icon(Icons.check_circle_outline),
border: OutlineInputBorder(),
), ),
items: const [ const SizedBox(width: 16),
DropdownMenuItem(value: 'read', child: Text('读完')), Expanded(
DropdownMenuItem(value: 'reading', child: Text('在读')), flex: 3,
DropdownMenuItem(value: 'want_to_read', child: Text('准备读')), child: _buildStatusSelector(),
], ),
onChanged: (value) { ],
setState(() { ),
_status = value!;
}); 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<String> 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: '阅读日期', Widget _buildStatusSelector() {
prefixIcon: Icon(Icons.event), return Column(
border: OutlineInputBorder(), crossAxisAlignment: CrossAxisAlignment.start,
), children: [
child: Row( const Text(
mainAxisAlignment: MainAxisAlignment.spaceBetween, '状态',
children: [ style: TextStyle(
Text( fontSize: 14,
_readDate != null color: Color(0xFF666666),
? '${_readDate!.year}-${_readDate!.month.toString().padLeft(2, '0')}-${_readDate!.day.toString().padLeft(2, '0')}' ),
: '选择日期', ),
style: TextStyle( const SizedBox(height: 8),
color: _readDate != null Row(
? Theme.of(context).colorScheme.onSurface children: [
: Theme.of(context).colorScheme.onSurfaceVariant, _buildStatusOption('已读', 'read'),
), const SizedBox(width: 12),
), _buildStatusOption('在读', 'reading'),
if (_readDate != null) const SizedBox(width: 12),
IconButton( _buildStatusOption('想读', 'want_to_read'),
icon: const Icon(Icons.clear, size: 20), ],
onPressed: () { ),
setState(() { ],
_readDate = null; );
}); }
},
), Widget _buildStatusOption(String label, String value) {
], final isSelected = _status == value;
), Color color;
), switch (value) {
), case 'read':
color = const Color(0xFF1A1A1A);
const SizedBox(height: 16), break;
case 'reading':
// 笔记 color = const Color(0xFF666666);
TextFormField( break;
controller: _noteController, case 'want_to_read':
decoration: const InputDecoration( color = const Color(0xFF999999);
labelText: '笔记', break;
hintText: '写下你的读后感...', default:
prefixIcon: Icon(Icons.edit_note), color = const Color(0xFFCCCCCC);
border: OutlineInputBorder(), }
alignLabelWithHint: true,
), return GestureDetector(
maxLines: 5, onTap: () => setState(() => _status = value),
), child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
const SizedBox(height: 32), decoration: BoxDecoration(
color: isSelected ? color : Colors.transparent,
// 保存按钮 border: Border.all(color: color),
SizedBox( ),
width: double.infinity, child: Text(
height: 48, label,
child: ElevatedButton.icon( style: TextStyle(
onPressed: _saveBook, fontSize: 13,
icon: const Icon(Icons.save), color: isSelected ? Colors.white : color,
label: Text(isEdit ? '保存修改' : '添加记录'), fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal,
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
],
), ),
), ),
), ),
); );
} }
/// 选择阅读日期 /// 选择封面
Future<void> _selectReadDate() async { Future<void> _pickCover() async {
final picked = await showDatePicker( try {
context: context, final XFile? pickedFile = await _picker.pickImage(
initialDate: _readDate ?? DateTime.now(), source: ImageSource.gallery,
firstDate: DateTime(1900), maxWidth: 800,
lastDate: DateTime.now(), maxHeight: 1200,
); imageQuality: 85,
);
if (picked != null) {
setState(() { if (pickedFile != null) {
_readDate = picked; 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<void> _saveBook() async { Future<void> _saveBook() async {
if (!_formKey.currentState!.validate()) { if (!_formKey.currentState!.validate()) {
return; 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) { if (widget.book == null) {
// 添加新模式 // 添加新模式
final newBook = Book( final newBook = Book(
id: DateTime.now().millisecondsSinceEpoch.toString(), id: now.millisecondsSinceEpoch.toString(),
title: _titleController.text.trim(), 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, rating: rating,
status: _status, status: _status,
readDate: _readDate, createdAt: now,
note: _noteController.text.trim(), updatedAt: now,
); );
await context.read<AppProvider>().addBook(newBook); await context.read<AppProvider>().addBook(newBook);
} else { } else {
// 编辑现有模式 // 编辑现有模式
final updatedBook = Book( final updatedBook = widget.book!.copyWith(
id: widget.book!.id,
title: _titleController.text.trim(), 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, rating: rating,
status: _status, status: _status,
readDate: _readDate, updatedAt: now,
note: _noteController.text.trim(),
); );
await context.read<AppProvider>().updateBook(updatedBook); await context.read<AppProvider>().updateBook(updatedBook);
} }
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(widget.book == null ? '添加成功' : '更新成功'), content: Text(widget.book == null ? '添加成功' : '更新成功'),
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
), ),
); );
Navigator.pop(context); Navigator.pop(context);
} }
} }

View File

@@ -66,7 +66,7 @@ class BookTabPage extends StatelessWidget {
Icon( Icon(
Icons.menu_book_outlined, Icons.menu_book_outlined,
size: 80, 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), const SizedBox(height: 16),
Text( Text(
@@ -92,48 +92,62 @@ class BookTabPage extends StatelessWidget {
List<Book> _getSampleBooks(int statusIndex) { List<Book> _getSampleBooks(int statusIndex) {
final statusMap = ['read', 'reading', 'want_to_read']; final statusMap = ['read', 'reading', 'want_to_read'];
final currentStatus = statusMap[statusIndex]; final currentStatus = statusMap[statusIndex];
final now = DateTime.now();
// 示例数据(实际应从数据库获取) // 示例数据(实际应从数据库获取)
final allBooks = [ final allBooks = [
Book( Book(
id: '1', id: '1',
title: '活着', title: '活着',
author: '余华', authors: ['余华'],
rating: 9.2, rating: 9.2,
status: 'read', status: 'read',
readDate: DateTime(2024, 1, 20), genres: ['小说', '文学'],
note: '非常感人的故事,让人思考生命的意义', summary: '非常感人的故事,让人思考生命的意义',
createdAt: now,
updatedAt: now,
), ),
Book( Book(
id: '2', id: '2',
title: '百年孤独', title: '百年孤独',
author: '加西亚·马尔克斯', authors: ['加西亚·马尔克斯'],
rating: 9.3, rating: 9.3,
status: 'read', status: 'read',
readDate: DateTime(2024, 2, 15), genres: ['小说', '魔幻现实主义'],
publisher: '南海出版公司',
createdAt: now,
updatedAt: now,
), ),
Book( Book(
id: '3', id: '3',
title: '人类简史', title: '人类简史',
author: '尤瓦尔·赫拉利', authors: ['尤瓦尔·赫拉利'],
rating: 9.0, rating: 9.0,
status: 'reading', status: 'reading',
genres: ['历史', '科普'],
createdAt: now,
updatedAt: now,
), ),
Book( Book(
id: '4', id: '4',
title: '三体', title: '三体',
author: '刘慈欣', authors: ['刘慈欣'],
rating: 9.5, rating: 9.5,
status: 'want_to_read', status: 'want_to_read',
genres: ['科幻', '小说'],
createdAt: now,
updatedAt: now,
), ),
Book( Book(
id: '5', id: '5',
title: '追风筝的人', title: '追风筝的人',
author: '卡勒德·胡赛尼', authors: ['卡勒德·胡赛尼'],
rating: 8.9, rating: 8.9,
status: 'read', status: 'read',
readDate: DateTime(2024, 3, 5), genres: ['小说', '文学'],
note: '关于救赎与成长的故事', summary: '关于救赎与成长的故事',
createdAt: now,
updatedAt: now,
), ),
]; ];

View File

@@ -34,10 +34,6 @@ class MainContentPage extends StatelessWidget {
return AppBar( return AppBar(
title: Text(_getAppBarTitle(provider)), title: Text(_getAppBarTitle(provider)),
actions: [ actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () => _showAddDialog(context, provider),
),
IconButton( IconButton(
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
onPressed: () { onPressed: () {

View File

@@ -7,328 +7,527 @@ import '../models/data_models.dart';
/// 影视详情页 - 极简主义设计 /// 影视详情页 - 极简主义设计
class MovieDetailPage extends StatefulWidget { class MovieDetailPage extends StatefulWidget {
final Movie movie; final Movie movie;
const MovieDetailPage({super.key, required this.movie}); const MovieDetailPage({super.key, required this.movie});
@override @override
State<MovieDetailPage> createState() => _MovieDetailPageState(); State<MovieDetailPage> createState() => _MovieDetailPageState();
} }
class _MovieDetailPageState extends State<MovieDetailPage> { class _MovieDetailPageState extends State<MovieDetailPage> {
late Movie _movie;
@override
void initState() {
super.initState();
_movie = widget.movie;
}
void _refreshMovie() {
final provider = context.read<AppProvider>();
final updated = provider.movies.firstWhere(
(m) => m.id == _movie.id,
orElse: () => _movie,
);
setState(() => _movie = updated);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( backgroundColor: Colors.white,
title: const Text('详情'), body: CustomScrollView(
actions: [ slivers: [
TextButton( // 顶部海报区域
onPressed: () => _navigateToEdit(context), _buildSliverAppBar(),
child: const Text('编辑'),
// 内容区域
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( bottomNavigationBar: _buildBottomBar(),
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),
],
),
),
); );
} }
/// 海报 /// 构建顶部 AppBar
Widget _buildPoster() { Widget _buildSliverAppBar() {
return Center( return SliverAppBar(
child: Container( expandedHeight: 280,
width: 140, pinned: true,
height: 200, backgroundColor: Colors.white,
decoration: BoxDecoration( flexibleSpace: FlexibleSpaceBar(
color: const Color(0xFFF5F5F5), background: _buildPosterSection(),
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(),
), ),
actions: [
IconButton(
icon: const Icon(Icons.edit_outlined),
onPressed: () => _navigateToEdit(context),
),
const SizedBox(width: 8),
],
); );
} }
Widget _buildPlaceholder() { /// 构建海报区域
return const Center( Widget _buildPosterSection() {
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);
}
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), width: double.infinity,
decoration: BoxDecoration( color: const Color(0xFFF5F5F5),
color: bgColor, child: widget.movie.posterPath != null && widget.movie.posterPath!.isNotEmpty
borderRadius: BorderRadius.zero, ? Image.file(
), File(widget.movie.posterPath!),
child: Text( fit: BoxFit.contain,
label, errorBuilder: (_, __, ___) => _buildPosterPlaceholder(),
style: TextStyle( )
fontSize: 12, : _buildPosterPlaceholder(),
color: textColor, );
fontWeight: FontWeight.w500, }
),
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() { Widget _buildBasicInfo() {
final items = <String>[]; return Padding(
padding: const EdgeInsets.all(24),
if (_movie.releaseDate != null) { child: Column(
items.add('${_movie.releaseDate!.year}'); crossAxisAlignment: CrossAxisAlignment.start,
} children: [
if (_movie.rating != null) { // 影视名称
items.add('${_movie.rating!.toStringAsFixed(1)}'); Text(
} widget.movie.title,
style: const TextStyle(
if (items.isEmpty) return const SizedBox.shrink(); fontSize: 24,
fontWeight: FontWeight.w600,
return Row( color: Color(0xFF1A1A1A),
children: items.asMap().entries.map((entry) { height: 1.3,
return Row( ),
children: [ ),
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( Text(
entry.value, '${widget.movie.releaseDate!.year}年上映',
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
color: Color(0xFF999999), color: Color(0xFF999999),
), ),
), ),
if (entry.key < items.length - 1)
const Padding( const SizedBox(height: 8),
padding: EdgeInsets.symmetric(horizontal: 12),
child: Text( // 时间信息
'·', Text(
style: TextStyle( '添加于 ${_formatDate(widget.movie.createdAt)}',
fontSize: 14, style: const TextStyle(
color: Color(0xFFCCCCCC), fontSize: 12,
), color: Color(0xFF999999),
), ),
), ),
], ],
);
}).toList(),
);
}
/// 区块标题
Widget _buildSectionTitle(String title) {
return Text(
title.toUpperCase(),
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: Color(0xFF999999),
letterSpacing: 1,
), ),
); );
} }
/// 文本列表 /// 构建状态标签
Widget _buildTextList(List<String> items) { Widget _buildStatusTag() {
return Wrap( String label;
spacing: 8, Color color;
runSpacing: 8, switch (widget.movie.status) {
children: items.map((item) => Text( case 'watched':
item, 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( style: const TextStyle(
fontSize: 15, fontSize: 12,
color: Color(0xFF333333), 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) { void _navigateToEdit(BuildContext context) {
Navigator.pushNamed(context, '/movie-form', arguments: _movie).then((_) { Navigator.pushNamed(context, '/movie-form', arguments: widget.movie).then((_) {
_refreshMovie();
context.read<AppProvider>().loadMovies(); context.read<AppProvider>().loadMovies();
}); });
} }
/// 删除对话框 /// 显示删除对话框
void _showDeleteDialog(BuildContext context) { void _showDeleteDialog(BuildContext context) {
showDialog( showDialog(
context: context, context: context,
@@ -336,40 +535,24 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
backgroundColor: Colors.white, backgroundColor: Colors.white,
elevation: 0, elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text( title: const Text('确认删除'),
'确认删除', content: Text('确定要删除"${widget.movie.title}"吗?'),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
),
content: Text(
'确定要删除"${_movie.title}"吗?',
style: const TextStyle(
fontSize: 15,
color: Color(0xFF666666),
),
),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text( child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
'取消',
style: TextStyle(color: Color(0xFF666666)),
),
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
await context.read<AppProvider>().removeMovie(_movie.id); await context.read<AppProvider>().removeMovie(widget.movie.id);
if (!context.mounted) return; if (!mounted) return;
Navigator.pop(context); Navigator.pop(context);
Navigator.pop(context); Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已删除')),
);
}, },
child: const Text( child: const Text('删除', style: TextStyle(color: Colors.red)),
'删除',
style: TextStyle(color: Color(0xFFDC2626)),
),
), ),
], ],
), ),

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ import 'package:provider/provider.dart';
import '../providers/app_provider.dart'; import '../providers/app_provider.dart';
import '../models/data_models.dart'; import '../models/data_models.dart';
/// 笔记详情页 /// 笔记详情页 - 极简主义设计
class NoteDetailPage extends StatefulWidget { class NoteDetailPage extends StatefulWidget {
final Note note; final Note note;
@@ -17,133 +17,112 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar( appBar: AppBar(
title: Text(widget.note.title), title: Text(_formatDateTime(widget.note.createdAt)),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.edit), icon: const Icon(Icons.edit_outlined),
onPressed: () => _navigateToEdit(context), onPressed: () => _navigateToEdit(context),
), ),
IconButton( const SizedBox(width: 8),
icon: const Icon(Icons.delete_outline),
onPressed: () => _showDeleteDialog(context),
),
], ],
), ),
body: SingleChildScrollView( body: Column(
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,
children: [ children: [
Text( // 标签区域
widget.note.content, if (widget.note.tags.isNotEmpty)
style: Theme.of(context).textTheme.bodyLarge?.copyWith( 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, height: 1.8,
), ),
),
),
), ),
],
),
);
}
/// 构建时间信息区域 // 底部操作栏
Widget _buildTimeSection(BuildContext context) { Container(
return Container( decoration: const BoxDecoration(
margin: const EdgeInsets.all(16), border: Border(
padding: const EdgeInsets.all(16), top: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
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,
), ),
const SizedBox(width: 8), ),
Text( child: SafeArea(
'创建时间', child: Padding(
style: Theme.of(context).textTheme.titleSmall?.copyWith( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
fontWeight: FontWeight.bold, child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'更新于 ${_formatDateTime(widget.note.updatedAt)}',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
), ),
), Row(
], children: [
), IconButton(
const SizedBox(height: 8), icon: const Icon(Icons.edit_outlined, size: 20),
Text( color: const Color(0xFF666666),
_formatDateTime(widget.note.createdAt), onPressed: () => _navigateToEdit(context),
style: Theme.of(context).textTheme.bodyMedium?.copyWith( padding: EdgeInsets.zero,
color: Theme.of(context).colorScheme.onSurfaceVariant, constraints: const BoxConstraints(),
), ),
), const SizedBox(width: 16),
const SizedBox(height: 16), IconButton(
Row( icon: const Icon(Icons.delete_outline, size: 20),
children: [ color: Colors.red,
Icon( onPressed: () => _showDeleteDialog(context),
Icons.edit, padding: EdgeInsets.zero,
size: 18, constraints: const BoxConstraints(),
color: Theme.of(context).colorScheme.onSurfaceVariant, ),
), ],
const SizedBox(width: 8),
Text(
'更新时间',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
), ),
), ],
],
),
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<NoteDetailPage> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('确认删除'), title: const Text('确认删除'),
content: Text('确定要删除"${widget.note.title}"吗?此操作不可恢复。'), content: const Text('确定要删除这条笔记吗?'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('取消'), child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
@@ -181,16 +163,10 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
Navigator.pop(context); Navigator.pop(context);
Navigator.pop(context); Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(content: Text('已删除')),
content: Text('已删除'),
behavior: SnackBarBehavior.floating,
),
); );
}, },
child: const Text( child: const Text('删除', style: TextStyle(color: Colors.red)),
'删除',
style: TextStyle(color: Colors.red),
),
), ),
], ],
), ),

View File

@@ -3,9 +3,9 @@ import 'package:provider/provider.dart';
import '../providers/app_provider.dart'; import '../providers/app_provider.dart';
import '../models/data_models.dart'; import '../models/data_models.dart';
/// 添加/编辑笔记页面 /// 添加/编辑笔记页面 - 极简书写界面
class NoteFormPage extends StatefulWidget { class NoteFormPage extends StatefulWidget {
final Note? note; // 如果为 null则是添加模式否则是编辑模式 final Note? note;
const NoteFormPage({super.key, this.note}); const NoteFormPage({super.key, this.note});
@@ -14,170 +14,270 @@ class NoteFormPage extends StatefulWidget {
} }
class _NoteFormPageState extends State<NoteFormPage> { class _NoteFormPageState extends State<NoteFormPage> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _titleController;
late TextEditingController _contentController; late TextEditingController _contentController;
late TextEditingController _tagsController; late DateTime _createdAt;
List<String> _tags = [];
bool _isEditing = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_titleController = TextEditingController(text: widget.note?.title ?? ''); final note = widget.note;
_contentController = TextEditingController(text: widget.note?.content ?? ''); _contentController = TextEditingController(text: note?.content ?? '');
_tagsController = TextEditingController( _createdAt = note?.createdAt ?? DateTime.now();
text: widget.note?.tags.join(', ') ?? '', _tags = note != null ? List.from(note.tags) : [];
); _isEditing = note != null;
} }
@override @override
void dispose() { void dispose() {
_titleController.dispose();
_contentController.dispose(); _contentController.dispose();
_tagsController.dispose();
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isEdit = widget.note != null;
return Scaffold( return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar( appBar: AppBar(
title: Text(isEdit ? '编辑笔记' : '添加笔记'), title: Text(_isEditing ? '编辑笔记' : '新建笔记'),
actions: [ actions: [
IconButton( TextButton(
icon: const Icon(Icons.save),
onPressed: _saveNote, onPressed: _saveNote,
child: const Text(
'保存',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
), ),
const SizedBox(width: 8),
], ],
), ),
body: SingleChildScrollView( body: Column(
padding: const EdgeInsets.all(16), children: [
child: Form( // 顶部信息栏:创建时间 + 标签
key: _formKey, Container(
child: Column( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
crossAxisAlignment: CrossAxisAlignment.start, decoration: const BoxDecoration(
children: [ border: Border(
// 标题 bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
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;
},
), ),
),
const SizedBox(height: 16), child: Row(
children: [
// 标签 // 创建时间
TextFormField( Text(
controller: _tagsController, _formatDateTime(_createdAt),
decoration: const InputDecoration( style: const TextStyle(
labelText: '标签', fontSize: 12,
hintText: '多个标签用逗号分隔', color: Color(0xFF999999),
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),
),
), ),
), ),
), 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<void> _saveNote() async { Future<void> _saveNote() async {
if (!_formKey.currentState!.validate()) { final content = _contentController.text.trim();
if (content.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('笔记内容不能为空')),
);
return; return;
} }
// 解析标签 final now = DateTime.now();
final tags = _tagsController.text
.split(',')
.map((tag) => tag.trim())
.where((tag) => tag.isNotEmpty)
.toList();
if (widget.note == null) { if (_isEditing) {
// 添加新模式 // 更新现有笔记
final now = DateTime.now(); final updatedNote = widget.note!.copyWith(
final newNote = Note( content: content,
id: now.millisecondsSinceEpoch.toString(), tags: _tags,
title: _titleController.text.trim(),
content: _contentController.text.trim(),
tags: tags,
createdAt: now,
updatedAt: now, updatedAt: now,
); );
await context.read<AppProvider>().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<AppProvider>().updateNote(updatedNote); await context.read<AppProvider>().updateNote(updatedNote);
} else {
// 添加新笔记
final newNote = Note(
id: now.millisecondsSinceEpoch.toString(),
content: content,
tags: _tags,
createdAt: _createdAt,
updatedAt: now,
);
await context.read<AppProvider>().addNote(newNote);
} }
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(widget.note == null ? '添加成功' : '更新成功'), content: Text(_isEditing ? '保存成功' : '添加成功'),
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
), ),
); );

View File

@@ -77,47 +77,43 @@ class NoteTabPage extends StatelessWidget {
/// 获取示例笔记数据 /// 获取示例笔记数据
List<Note> _getSampleNotes() { List<Note> _getSampleNotes() {
final now = DateTime.now();
// 示例数据(实际应从数据库获取) // 示例数据(实际应从数据库获取)
return [ return [
Note( Note(
id: '1', id: '1',
title: 'Flutter 学习心得',
content: '今天开始学习 Flutter 框架,感觉和 Vue 有很多相似之处,都是声明式 UI组件化开发。Widget 的概念很有趣,一切皆 Widget。', content: '今天开始学习 Flutter 框架,感觉和 Vue 有很多相似之处,都是声明式 UI组件化开发。Widget 的概念很有趣,一切皆 Widget。',
tags: ['学习', 'Flutter', '编程'], tags: ['学习', 'Flutter', '编程'],
createdAt: DateTime(2024, 3, 1, 10, 30), createdAt: now.subtract(const Duration(days: 2)),
updatedAt: DateTime(2024, 3, 1, 10, 30), updatedAt: now.subtract(const Duration(days: 2)),
), ),
Note( Note(
id: '2', id: '2',
title: '《活着》读后感',
content: '余华的《活着》真的是一部让人深思的作品。福贵的一生经历了太多的苦难,但他依然坚强地活着。生命的意义或许就在于活着本身。', content: '余华的《活着》真的是一部让人深思的作品。福贵的一生经历了太多的苦难,但他依然坚强地活着。生命的意义或许就在于活着本身。',
tags: ['阅读', '感悟', '书籍'], tags: ['阅读', '感悟', '书籍'],
createdAt: DateTime(2024, 2, 20, 15, 20), createdAt: now.subtract(const Duration(days: 5)),
updatedAt: DateTime(2024, 2, 20, 16, 0), updatedAt: now.subtract(const Duration(days: 5)),
), ),
Note( Note(
id: '3', id: '3',
title: '电影《星际穿越》观后感',
content: '诺兰的电影总是充满想象力。《星际穿越》将科幻与亲情完美结合,五维空间的呈现方式令人震撼。配乐也是一绝。', content: '诺兰的电影总是充满想象力。《星际穿越》将科幻与亲情完美结合,五维空间的呈现方式令人震撼。配乐也是一绝。',
tags: ['观影', '科幻', '电影'], tags: ['观影', '科幻', '电影'],
createdAt: DateTime(2024, 2, 15, 20, 0), createdAt: now.subtract(const Duration(days: 10)),
updatedAt: DateTime(2024, 2, 15, 20, 30), updatedAt: now.subtract(const Duration(days: 10)),
), ),
Note( Note(
id: '4', id: '4',
title: 'Python 数据分析笔记',
content: 'Pandas 库的 DataFrame 操作非常强大,可以方便地进行数据清洗和分析。需要多练习熟练掌握常用操作。', content: 'Pandas 库的 DataFrame 操作非常强大,可以方便地进行数据清洗和分析。需要多练习熟练掌握常用操作。',
tags: ['Python', '数据分析', '技术'], tags: ['Python', '数据分析', '技术'],
createdAt: DateTime(2024, 1, 10, 9, 0), createdAt: now.subtract(const Duration(days: 30)),
updatedAt: DateTime(2024, 1, 10, 9, 30), updatedAt: now.subtract(const Duration(days: 30)),
), ),
Note( Note(
id: '5', id: '5',
title: '生活随笔',
content: '春天来了,天气渐暖。周末去公园散步,看到花开得很好。生活中的小确幸值得记录。', content: '春天来了,天气渐暖。周末去公园散步,看到花开得很好。生活中的小确幸值得记录。',
tags: ['生活', '随笔'], tags: ['生活', '随笔'],
createdAt: DateTime(2024, 3, 3, 18, 0), createdAt: now.subtract(const Duration(hours: 5)),
updatedAt: DateTime(2024, 3, 3, 18, 0), updatedAt: now.subtract(const Duration(hours: 5)),
), ),
]; ];
} }

View File

@@ -6,25 +6,17 @@ import 'database_helper.dart';
class BookDao { class BookDao {
final DatabaseHelper _dbHelper = DatabaseHelper.instance; final DatabaseHelper _dbHelper = DatabaseHelper.instance;
// 获取所有书籍记录 // 获取所有未删除的书籍记录
Future<List<Book>> getAllBooks() async { Future<List<Book>> getAllBooks() async {
final db = await _dbHelper.database; final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query('books'); final List<Map<String, dynamic>> maps = await db.query(
'books',
where: 'is_deleted = ?',
whereArgs: [0],
orderBy: 'updated_at DESC',
);
return List.generate(maps.length, (i) { return List.generate(maps.length, (i) => Book.fromJson(maps[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'],
);
});
} }
// 根据状态筛选书籍记录 // 根据状态筛选书籍记录
@@ -32,24 +24,25 @@ class BookDao {
final db = await _dbHelper.database; final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query( final List<Map<String, dynamic>> maps = await db.query(
'books', 'books',
where: 'status = ?', where: 'status = ? AND is_deleted = ?',
whereArgs: [status], whereArgs: [status, 0],
orderBy: 'updated_at DESC',
); );
return List.generate(maps.length, (i) { return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
return Book( }
id: maps[i]['id'].toString(),
title: maps[i]['title'], // 根据ID获取书籍
author: maps[i]['author'], Future<Book?> getBookById(String id) async {
cover: maps[i]['cover'], final db = await _dbHelper.database;
rating: maps[i]['rating']?.toDouble(), final List<Map<String, dynamic>> maps = await db.query(
status: maps[i]['status'], 'books',
readDate: maps[i]['read_date'] != null where: 'id = ? AND is_deleted = ?',
? DateTime.parse(maps[i]['read_date']) whereArgs: [id, 0],
: null, );
note: maps[i]['note'],
); if (maps.isEmpty) return null;
}); return Book.fromJson(maps.first);
} }
// 添加书籍记录 // 添加书籍记录
@@ -69,7 +62,21 @@ class BookDao {
); );
} }
// 删除书籍记录 // 删除书籍记录
Future<int> 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<int> deleteBook(String id) async { Future<int> deleteBook(String id) async {
final db = await _dbHelper.database; final db = await _dbHelper.database;
return await db.delete( return await db.delete(
@@ -78,4 +85,43 @@ class BookDao {
whereArgs: [id], whereArgs: [id],
); );
} }
// 搜索书籍(标题、别名)
Future<List<Book>> searchBooks(String query) async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> 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<List<Book>> getBooksByAuthor(String author) async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> 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<List<Book>> getBooksByGenre(String genre) async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> 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]));
}
} }

View File

@@ -20,7 +20,7 @@ class DatabaseHelper {
return await openDatabase( return await openDatabase(
path, path,
version: 2, version: 4,
onCreate: _createDB, onCreate: _createDB,
onUpgrade: _onUpgrade, onUpgrade: _onUpgrade,
); );
@@ -32,6 +32,110 @@ class DatabaseHelper {
// 升级movies表结构 // 升级movies表结构
await _upgradeMoviesTableV2(db); await _upgradeMoviesTableV2(db);
} }
if (oldVersion < 3) {
// 升级books表结构
await _upgradeBooksTableV3(db);
}
if (oldVersion < 4) {
// 升级notes表结构
await _upgradeNotesTableV4(db);
}
}
/// 升级notes表到V4
Future<void> _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<void> _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 /// 升级movies表到V2
@@ -120,26 +224,31 @@ class DatabaseHelper {
// 书籍表 // 书籍表
await db.execute(''' await db.execute('''
CREATE TABLE books ( CREATE TABLE books (
id $idType, id TEXT PRIMARY KEY,
title $textType, title TEXT NOT NULL,
author TEXT, cover_path TEXT,
cover TEXT, authors TEXT,
alternate_titles TEXT,
publisher TEXT,
genres TEXT,
summary TEXT,
rating REAL, rating REAL,
status $textType, status TEXT NOT NULL,
read_date TEXT, created_at TEXT NOT NULL,
note TEXT updated_at TEXT NOT NULL,
is_deleted INTEGER DEFAULT 0
) )
'''); ''');
// 笔记表 // 笔记表
await db.execute(''' await db.execute('''
CREATE TABLE notes ( CREATE TABLE notes (
id $idType, id TEXT PRIMARY KEY,
title $textType, content TEXT NOT NULL,
content $textType, content_type TEXT DEFAULT 'markdown',
tags TEXT, tags TEXT,
created_at $textType, created_at TEXT NOT NULL,
updated_at $textType updated_at TEXT NOT NULL
) )
'''); ''');
} }

View File

@@ -14,31 +14,26 @@ class NoteDao {
orderBy: 'updated_at DESC', orderBy: 'updated_at DESC',
); );
return List.generate(maps.length, (i) { return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
return Note( }
id: maps[i]['id'].toString(),
title: maps[i]['title'], // 根据ID获取笔记
content: maps[i]['content'], Future<Note?> getNoteById(String id) async {
tags: maps[i]['tags'] != null final db = await _dbHelper.database;
? List<String>.from(maps[i]['tags'].split(',')) final List<Map<String, dynamic>> maps = await db.query(
: [], 'notes',
createdAt: DateTime.parse(maps[i]['created_at']), where: 'id = ?',
updatedAt: DateTime.parse(maps[i]['updated_at']), whereArgs: [id],
); );
});
if (maps.isEmpty) return null;
return Note.fromJson(maps.first);
} }
// 添加笔记 // 添加笔记
Future<int> insertNote(Note note) async { Future<int> insertNote(Note note) async {
final db = await _dbHelper.database; final db = await _dbHelper.database;
return await db.insert('notes', { return await db.insert('notes', note.toJson());
'id': note.id,
'title': note.title,
'content': note.content,
'tags': note.tags.join(','),
'created_at': note.createdAt.toIso8601String(),
'updated_at': note.updatedAt.toIso8601String(),
});
} }
// 更新笔记 // 更新笔记
@@ -46,12 +41,7 @@ class NoteDao {
final db = await _dbHelper.database; final db = await _dbHelper.database;
return await db.update( return await db.update(
'notes', 'notes',
{ note.toJson(),
'title': note.title,
'content': note.content,
'tags': note.tags.join(','),
'updated_at': note.updatedAt.toIso8601String(),
},
where: 'id = ?', where: 'id = ?',
whereArgs: [note.id], whereArgs: [note.id],
); );
@@ -66,4 +56,30 @@ class NoteDao {
whereArgs: [id], whereArgs: [id],
); );
} }
// 搜索笔记
Future<List<Note>> searchNotes(String query) async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> 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<List<Note>> getNotesByTag(String tag) async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'notes',
where: 'tags LIKE ?',
whereArgs: ['%$tag%'],
orderBy: 'updated_at DESC',
);
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
}
} }

View File

@@ -1,217 +1,220 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/app_provider.dart'; import '../providers/app_provider.dart';
import '../models/data_models.dart'; import '../models/data_models.dart';
import '../utils/app_theme.dart';
/// 书籍列表项组件 /// 书籍列表项组件 - 极简主义设计
class BookListItem extends StatelessWidget { class BookListItem extends StatelessWidget {
final Book book; final Book book;
const BookListItem({super.key, required this.book}); const BookListItem({super.key, required this.book});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return InkWell(
margin: const EdgeInsets.only(bottom: 12), onTap: () {
child: InkWell( Navigator.pushNamed(context, '/book-detail', arguments: book);
onTap: () { },
// 跳转到详情页 child: Container(
Navigator.pushNamed(context, '/book-detail', arguments: book); padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
}, decoration: const BoxDecoration(
borderRadius: BorderRadius.circular(12), border: Border(
child: Padding( bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
padding: const EdgeInsets.all(12), ),
child: Row( ),
crossAxisAlignment: CrossAxisAlignment.start, child: Row(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
// 封面占位图 children: [
_buildCover(), // 封面
_buildCover(),
const SizedBox(width: 12),
const SizedBox(width: 16),
// 书籍信息
Expanded( // 信息
child: Column( Expanded(
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ 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( Text(
book.title, book.authors.join(' / '),
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 13,
color: Color(0xFF666666),
), ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
if (book.author != null) ...[ const SizedBox(height: 8),
const SizedBox(height: 4),
Text( // 评分和状态
book.author!, Row(
style: Theme.of(context).textTheme.bodySmall?.copyWith( children: [
color: Theme.of(context).colorScheme.onSurfaceVariant, if (book.rating != null) ...[
const Icon(
Icons.star,
size: 14,
color: Color(0xFF1A1A1A),
), ),
), const SizedBox(width: 2),
], Text(
book.rating!.toStringAsFixed(1),
const SizedBox(height: 8), style: const TextStyle(
fontSize: 13,
// 评分和状态 fontWeight: FontWeight.w500,
Row( color: Color(0xFF1A1A1A),
children: [
if (book.rating != null) ...[
Icon(
Icons.star,
size: 16,
color: Colors.amber[700],
), ),
const SizedBox(width: 4), ),
Text( const SizedBox(width: 12),
book.rating.toString(),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.amber[700],
),
),
const SizedBox(width: 12),
],
// 状态标签
_buildStatusTag(context),
], ],
_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() { Widget _buildCover() {
return Container( return Container(
width: 60, width: 60,
height: 90, height: 80,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[300], color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(8), 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( child: Icon(
Icons.menu_book, Icons.menu_book,
color: Colors.grey[500], size: 24,
size: 32, color: Color(0xFFCCCCCC),
), ),
); );
} }
/// 构建状态标签 /// 构建状态标签
Widget _buildStatusTag(BuildContext context) { Widget _buildStatusTag() {
Color statusColor; String label;
String statusText; Color color;
switch (book.status) { switch (book.status) {
case 'read': case 'read':
statusColor = AppTheme.readColor; label = '已读';
statusText = '读完'; color = const Color(0xFF1A1A1A);
break; break;
case 'reading': case 'reading':
statusColor = AppTheme.readingColor; label = '在读';
statusText = '在读'; color = const Color(0xFF666666);
break; break;
case 'want_to_read': case 'want_to_read':
statusColor = AppTheme.wantToReadColor; label = '想读';
statusText = '准备读'; color = const Color(0xFF999999);
break; break;
default: default:
statusColor = Colors.grey; label = '未知';
statusText = '未知'; color = const Color(0xFFCCCCCC);
} }
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: statusColor.withOpacity(0.1), color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4), border: Border.all(color: color),
border: Border.all(
color: statusColor,
width: 1,
),
), ),
child: Text( child: Text(
statusText, label,
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: statusColor, color: color,
fontWeight: FontWeight.bold, 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( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('确认删除'), title: const Text('确认删除'),
content: Text('确定要删除"${book.title}"吗?此操作不可恢复。'), content: Text('确定要删除"${book.title}"吗?'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('取消'), child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
@@ -219,16 +222,10 @@ class BookListItem extends StatelessWidget {
if (!context.mounted) return; if (!context.mounted) return;
Navigator.pop(context); Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(content: Text('已删除')),
content: Text('已删除'),
behavior: SnackBarBehavior.floating,
),
); );
}, },
child: const Text( child: const Text('删除', style: TextStyle(color: Colors.red)),
'删除',
style: TextStyle(color: Colors.red),
),
), ),
], ],
), ),

View File

@@ -1,9 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/app_provider.dart'; import '../providers/app_provider.dart';
import '../utils/app_theme.dart';
/// 阅读状态选择栏 /// 阅读状态选择栏 - 极简主义设计
class BookStatusBar extends StatelessWidget { class BookStatusBar extends StatelessWidget {
const BookStatusBar({super.key}); const BookStatusBar({super.key});
@@ -12,44 +11,34 @@ class BookStatusBar extends StatelessWidget {
return Consumer<AppProvider>( return Consumer<AppProvider>(
builder: (context, provider, child) { builder: (context, provider, child) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: BoxDecoration( decoration: const BoxDecoration(
color: Theme.of(context).colorScheme.surface, color: Colors.white,
boxShadow: [ border: Border(
BoxShadow( bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
color: Colors.black.withOpacity(0.05), ),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
), ),
child: Row( child: Row(
children: [ children: [
_buildStatusItem( _buildStatusItem(
context, context,
'', '',
AppTheme.readColor,
Icons.check_circle,
0, 0,
provider.bookStatusIndex, provider.bookStatusIndex,
() => provider.setBookStatusIndex(0), () => provider.setBookStatusIndex(0),
), ),
const SizedBox(width: 12), const SizedBox(width: 16),
_buildStatusItem( _buildStatusItem(
context, context,
'在读', '在读',
AppTheme.readingColor,
Icons.auto_stories,
1, 1,
provider.bookStatusIndex, provider.bookStatusIndex,
() => provider.setBookStatusIndex(1), () => provider.setBookStatusIndex(1),
), ),
const SizedBox(width: 12), const SizedBox(width: 16),
_buildStatusItem( _buildStatusItem(
context, context,
'准备', '',
AppTheme.wantToReadColor,
Icons.bookmark_border,
2, 2,
provider.bookStatusIndex, provider.bookStatusIndex,
() => provider.setBookStatusIndex(2), () => provider.setBookStatusIndex(2),
@@ -65,46 +54,44 @@ class BookStatusBar extends StatelessWidget {
Widget _buildStatusItem( Widget _buildStatusItem(
BuildContext context, BuildContext context,
String label, String label,
Color color,
IconData icon,
int index, int index,
int currentIndex, int currentIndex,
VoidCallback onTap, VoidCallback onTap,
) { ) {
final isSelected = index == currentIndex; 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( return Expanded(
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 10), padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected ? color.withOpacity(0.1) : Colors.transparent, color: isSelected ? color : Colors.transparent,
borderRadius: BorderRadius.circular(8), border: Border.all(color: color),
border: Border.all(
color: isSelected ? color : Colors.transparent,
width: 2,
),
), ),
child: Column( child: Text(
mainAxisSize: MainAxisSize.min, label,
children: [ textAlign: TextAlign.center,
Icon( style: TextStyle(
icon, fontSize: 14,
color: isSelected ? color : Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal,
size: 20, color: isSelected ? Colors.white : color,
), ),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 13,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? color : Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
), ),
), ),
), ),

View File

@@ -1,17 +1,21 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart'; import '../models/data_models.dart';
/// 观影列表项 - 极简主义设计 /// 观影列表项组件 - 极简主义设计
class MovieListItem extends StatelessWidget { class MovieListItem extends StatelessWidget {
final Movie movie; final Movie movie;
const MovieListItem({super.key, required this.movie}); const MovieListItem({super.key, required this.movie});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( return InkWell(
onTap: () => Navigator.pushNamed(context, '/movie-detail', arguments: movie), onTap: () {
Navigator.pushNamed(context, '/movie-detail', arguments: movie);
},
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: const BoxDecoration( decoration: const BoxDecoration(
@@ -32,7 +36,7 @@ class MovieListItem extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 标题 // 影视名称
Text( Text(
movie.title, movie.title,
style: const TextStyle( style: const TextStyle(
@@ -44,12 +48,7 @@ class MovieListItem extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 6), const SizedBox(height: 4),
// 年份和评分
_buildMetaInfo(),
const SizedBox(height: 8),
// 导演 // 导演
if (movie.directors.isNotEmpty) if (movie.directors.isNotEmpty)
@@ -57,7 +56,7 @@ class MovieListItem extends StatelessWidget {
movie.directors.take(2).join(' / '), movie.directors.take(2).join(' / '),
style: const TextStyle( style: const TextStyle(
fontSize: 13, fontSize: 13,
color: Color(0xFF999999), color: Color(0xFF666666),
), ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@@ -65,102 +64,108 @@ class MovieListItem extends StatelessWidget {
const SizedBox(height: 8), 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( Column(
Icons.chevron_right, children: [
size: 20, IconButton(
color: Color(0xFFCCCCCC), 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() { Widget _buildPoster() {
return Container( return Container(
width: 56, width: 60,
height: 80, height: 80,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF5F5F5), color: const Color(0xFFF5F5F5),
border: Border.all( border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5),
color: const Color(0xFFE5E5E5),
width: 0.5,
),
), ),
child: movie.posterPath != null && movie.posterPath!.isNotEmpty child: movie.posterPath != null && movie.posterPath!.isNotEmpty
? Image.file( ? Image.file(
File(movie.posterPath!), File(movie.posterPath!),
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _buildPlaceholder(), errorBuilder: (_, __, ___) => _buildPosterPlaceholder(),
) )
: _buildPlaceholder(), : _buildPosterPlaceholder(),
); );
} }
Widget _buildPlaceholder() { Widget _buildPosterPlaceholder() {
return const Center( return const Center(
child: Icon( child: Icon(
Icons.movie_outlined, Icons.movie_outlined,
size: 20, size: 24,
color: Color(0xFFCCCCCC), color: Color(0xFFCCCCCC),
), ),
); );
} }
/// 元信息 /// 构建状态标签
Widget _buildMetaInfo() {
final items = <String>[];
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() { Widget _buildStatusTag() {
String label; String label;
Color color; Color color;
switch (movie.status) { switch (movie.status) {
case 'watched': case 'watched':
label = '已看'; label = '已看';
@@ -179,12 +184,50 @@ class MovieListItem extends StatelessWidget {
color = const Color(0xFFCCCCCC); color = const Color(0xFFCCCCCC);
} }
return Text( return Container(
label, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
style: TextStyle( decoration: BoxDecoration(
fontSize: 12, color: color.withValues(alpha: 0.1),
color: color, border: Border.all(color: color),
fontWeight: FontWeight.w500, ),
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<AppProvider>().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)),
),
],
), ),
); );
} }

View File

@@ -12,25 +12,33 @@ class MovieStatusBar extends StatelessWidget {
builder: (context, provider, child) { builder: (context, provider, child) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), 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( child: Row(
children: [ children: [
_buildStatusItem( _buildStatusItem(
context,
'已看', '已看',
0, 0,
provider.movieStatusIndex, provider.movieStatusIndex,
() => provider.setMovieStatusIndex(0), () => provider.setMovieStatusIndex(0),
), ),
const SizedBox(width: 24), const SizedBox(width: 16),
_buildStatusItem( _buildStatusItem(
'想看', context,
'在看',
1, 1,
provider.movieStatusIndex, provider.movieStatusIndex,
() => provider.setMovieStatusIndex(1), () => provider.setMovieStatusIndex(1),
), ),
const SizedBox(width: 24), const SizedBox(width: 16),
_buildStatusItem( _buildStatusItem(
'在看', context,
'想看',
2, 2,
provider.movieStatusIndex, provider.movieStatusIndex,
() => provider.setMovieStatusIndex(2), () => provider.setMovieStatusIndex(2),
@@ -44,6 +52,7 @@ class MovieStatusBar extends StatelessWidget {
/// 构建状态项 /// 构建状态项
Widget _buildStatusItem( Widget _buildStatusItem(
BuildContext context,
String label, String label,
int index, int index,
int currentIndex, int currentIndex,
@@ -51,14 +60,39 @@ class MovieStatusBar extends StatelessWidget {
) { ) {
final isSelected = index == currentIndex; final isSelected = index == currentIndex;
return GestureDetector( Color color;
onTap: onTap, switch (index) {
child: Text( case 0:
label, color = const Color(0xFF1A1A1A);
style: TextStyle( break;
fontSize: 15, case 1:
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, color = const Color(0xFF666666);
color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFF999999), 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,
),
),
), ),
), ),
); );

View File

@@ -3,7 +3,7 @@ import 'package:provider/provider.dart';
import '../providers/app_provider.dart'; import '../providers/app_provider.dart';
import '../models/data_models.dart'; import '../models/data_models.dart';
/// 笔记列表项组件 /// 笔记列表项组件 - 极简主义设计
class NoteListItem extends StatelessWidget { class NoteListItem extends StatelessWidget {
final Note note; final Note note;
@@ -11,104 +11,75 @@ class NoteListItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return InkWell(
margin: const EdgeInsets.only(bottom: 12), onTap: () {
child: InkWell( Navigator.pushNamed(context, '/note-detail', arguments: note);
onTap: () { },
// 跳转到详情页 child: Container(
Navigator.pushNamed(context, '/note-detail', arguments: note); padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
}, decoration: const BoxDecoration(
borderRadius: BorderRadius.circular(12), border: Border(
child: Padding( bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
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(),
),
],
),
],
),
],
), ),
), ),
), child: Column(
); crossAxisAlignment: CrossAxisAlignment.start,
} children: [
// 内容摘要
/// 构建标签 Text(
Widget _buildTag(BuildContext context, String tag) { note.summary,
return Container( style: const TextStyle(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), fontSize: 15,
decoration: BoxDecoration( color: Color(0xFF1A1A1A),
color: Theme.of(context).colorScheme.primaryContainer, height: 1.5,
borderRadius: BorderRadius.circular(12), ),
), maxLines: 3,
child: Text( overflow: TextOverflow.ellipsis,
'#$tag', ),
style: TextStyle(
fontSize: 12, const SizedBox(height: 12),
color: Theme.of(context).colorScheme.onPrimaryContainer,
// 底部信息:标签 + 时间 + 操作
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')}'; 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<AppProvider>().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),
),
),
],
),
);
}
} }