笔记功能初步完善

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

View File

@@ -89,8 +89,8 @@ class Movie {
return File(posterPath!);
}
/// 解析字符串列表
static List<String> _parseStringList(dynamic data) {
/// 解析字符串列表公共静态方法供Book使用
static List<String> parseStringList(dynamic data) {
if (data == null) return [];
if (data is List) {
return data.map((e) => e.toString()).toList();
@@ -109,6 +109,9 @@ class Movie {
}
return [];
}
/// 解析字符串列表(私有别名,保持兼容性)
static List<String> _parseStringList(dynamic data) => parseStringList(data);
/// 复制并修改
Movie copyWith({
@@ -151,37 +154,54 @@ class Movie {
/// 书籍条目模型
class Book {
final String id;
final String title;
final String? author;
final String? cover;
final double? rating;
final String status; // 'read', 'reading', 'want_to_read'
final DateTime? readDate;
final String? note;
final String title; // 书籍名称
final String? coverPath; // 本地封面路径
final List<String> authors; // 作者列表
final List<String> alternateTitles; // 别名
final String? publisher; // 出版社
final List<String> genres; // 类型
final String? summary; // 书籍简介
final double? rating; // 评分 1-10
final String status; // read/reading/want_to_read
final DateTime createdAt;
final DateTime updatedAt;
final bool isDeleted;
Book({
required this.id,
required this.title,
this.author,
this.cover,
this.coverPath,
this.authors = const [],
this.alternateTitles = const [],
this.publisher,
this.genres = const [],
this.summary,
this.rating,
required this.status,
this.readDate,
this.note,
required this.createdAt,
required this.updatedAt,
this.isDeleted = false,
});
factory Book.fromJson(Map<String, dynamic> json) {
return Book(
id: json['id'] ?? '',
title: json['title'] ?? '',
author: json['author'],
cover: json['cover'],
coverPath: json['cover_path'],
authors: Movie.parseStringList(json['authors']),
alternateTitles: Movie.parseStringList(json['alternate_titles']),
publisher: json['publisher'],
genres: Movie.parseStringList(json['genres']),
summary: json['summary'],
rating: json['rating']?.toDouble(),
status: json['status'] ?? 'want_to_read',
readDate: json['read_date'] != null
? DateTime.parse(json['read_date'])
: null,
note: json['note'],
createdAt: json['created_at'] != null
? DateTime.parse(json['created_at'])
: DateTime.now(),
updatedAt: json['updated_at'] != null
? DateTime.parse(json['updated_at'])
: DateTime.now(),
isDeleted: json['is_deleted'] == 1 || json['is_deleted'] == true,
);
}
@@ -189,29 +209,73 @@ class Book {
return {
'id': id,
'title': title,
'author': author,
'cover': cover,
'cover_path': coverPath,
'authors': jsonEncode(authors),
'alternate_titles': jsonEncode(alternateTitles),
'publisher': publisher,
'genres': jsonEncode(genres),
'summary': summary,
'rating': rating,
'status': status,
'read_date': readDate?.toIso8601String(),
'note': note,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'is_deleted': isDeleted ? 1 : 0,
};
}
/// 获取封面文件
File? get coverFile {
if (coverPath == null || coverPath!.isEmpty) return null;
return File(coverPath!);
}
/// 复制并修改
Book copyWith({
String? id,
String? title,
String? coverPath,
List<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 {
final String id;
final String title;
final String content;
final String contentType; // markdown / rich_text
final List<String> tags;
final DateTime createdAt;
final DateTime updatedAt;
Note({
required this.id,
required this.title,
required this.content,
this.contentType = 'markdown',
this.tags = const [],
required this.createdAt,
required this.updatedAt,
@@ -219,12 +283,10 @@ class Note {
factory Note.fromJson(Map<String, dynamic> json) {
return Note(
id: json['id'] ?? '',
title: json['title'] ?? '',
id: json['id']?.toString() ?? '',
content: json['content'] ?? '',
tags: json['tags'] != null
? List<String>.from(json['tags'])
: [],
contentType: json['content_type'] ?? 'markdown',
tags: Movie.parseStringList(json['tags']),
createdAt: json['created_at'] != null
? DateTime.parse(json['created_at'])
: DateTime.now(),
@@ -237,12 +299,37 @@ class Note {
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'content': content,
'tags': tags,
'content_type': contentType,
'tags': jsonEncode(tags),
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
};
}
/// 复制并修改
Note copyWith({
String? id,
String? content,
String? contentType,
List<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:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
import '../utils/app_theme.dart';
/// 书籍详情页
/// 书籍详情页 - 极简主义设计
class BookDetailPage extends StatefulWidget {
final Book book;
const BookDetailPage({super.key, required this.book});
@override
State<BookDetailPage> createState() => _BookDetailPageState();
}
class _BookDetailPageState extends State<BookDetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.book.title),
actions: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _navigateToEdit(context),
),
IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => _showDeleteDialog(context),
),
],
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 封面区域
_buildCoverSection(context),
// 基本信息
_buildInfoSection(context),
// 笔记区域
if (widget.book.note != null && widget.book.note!.isNotEmpty)
_buildNoteSection(context),
],
),
),
);
}
/// 构建封面区域
Widget _buildCoverSection(BuildContext context) {
return Container(
width: double.infinity,
height: 250,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Theme.of(context).colorScheme.primaryContainer,
Theme.of(context).colorScheme.surface,
],
),
),
child: Stack(
children: [
Center(
backgroundColor: Colors.white,
body: CustomScrollView(
slivers: [
// 顶部封面区域
_buildSliverAppBar(),
// 内容区域
SliverToBoxAdapter(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.menu_book,
size: 80,
color: Theme.of(context).colorScheme.primary.withOpacity(0.6),
),
const SizedBox(height: 16),
Text(
widget.book.title,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
// 基本信息
_buildBasicInfo(),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 作者信息
_buildAuthorsSection(),
// 出版社
if (widget.book.publisher != null && widget.book.publisher!.isNotEmpty)
_buildPublisherSection(),
// 类型
if (widget.book.genres.isNotEmpty)
_buildGenresSection(),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 简介
if (widget.book.summary != null && widget.book.summary!.isNotEmpty)
_buildSummarySection(),
// 别名
if (widget.book.alternateTitles.isNotEmpty)
_buildAlternateTitlesSection(),
const SizedBox(height: 48),
],
),
),
Positioned(
top: 16,
right: 16,
child: _buildStatusTag(context),
),
],
),
// 底部操作栏
bottomNavigationBar: _buildBottomBar(),
);
}
/// 构建状态标签
Widget _buildStatusTag(BuildContext context) {
Color statusColor;
String statusText;
switch (widget.book.status) {
case 'read':
statusColor = AppTheme.readColor;
statusText = '读完';
break;
case 'reading':
statusColor = AppTheme.readingColor;
statusText = '在读';
break;
case 'want_to_read':
statusColor = AppTheme.wantToReadColor;
statusText = '准备读';
break;
default:
statusColor = Colors.grey;
statusText = '未知';
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.9),
borderRadius: BorderRadius.circular(20),
/// 构建顶部 AppBar
Widget _buildSliverAppBar() {
return SliverAppBar(
expandedHeight: 280,
pinned: true,
backgroundColor: Colors.white,
flexibleSpace: FlexibleSpaceBar(
background: _buildCoverSection(),
),
child: Text(
statusText,
style: const TextStyle(
fontSize: 14,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
);
}
/// 构建信息区域
Widget _buildInfoSection(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 作者
if (widget.book.author != null && widget.book.author!.isNotEmpty) ...[
_buildInfoItem(
context,
icon: Icons.person,
label: widget.book.author!,
),
const SizedBox(height: 12),
],
// 评分
if (widget.book.rating != null) ...[
_buildInfoItem(
context,
icon: Icons.star,
label: widget.book.rating.toString(),
iconColor: Colors.amber[700],
textColor: Colors.amber[700],
),
const SizedBox(height: 12),
],
// 阅读日期
if (widget.book.readDate != null)
_buildInfoItem(
context,
icon: Icons.event,
label: '阅读日期:${_formatDate(widget.book.readDate!)}',
),
],
),
);
}
/// 构建信息项
Widget _buildInfoItem(
BuildContext context, {
required IconData icon,
required String label,
Color? iconColor,
Color? textColor,
}) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 18, color: iconColor),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
fontSize: 14,
color: textColor ?? Theme.of(context).colorScheme.onSurfaceVariant,
),
actions: [
IconButton(
icon: const Icon(Icons.edit_outlined),
onPressed: () => _navigateToEdit(context),
),
const SizedBox(width: 8),
],
);
}
/// 构建笔记区域
Widget _buildNoteSection(BuildContext context) {
/// 构建封面区域
Widget _buildCoverSection() {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
width: double.infinity,
color: const Color(0xFFF5F5F5),
child: widget.book.coverPath != null && widget.book.coverPath!.isNotEmpty
? Image.file(
File(widget.book.coverPath!),
fit: BoxFit.contain,
errorBuilder: (_, __, ___) => _buildCoverPlaceholder(),
)
: _buildCoverPlaceholder(),
);
}
Widget _buildCoverPlaceholder() {
return const Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
Icon(
Icons.edit_note,
size: 20,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
'笔记',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
Icon(
Icons.menu_book,
size: 64,
color: Color(0xFFCCCCCC),
),
const SizedBox(height: 12),
SizedBox(height: 16),
Text(
widget.book.note!,
style: Theme.of(context).textTheme.bodyMedium,
'暂无封面',
style: TextStyle(
fontSize: 14,
color: Color(0xFF999999),
),
),
],
),
);
}
/// 构建基本信息
Widget _buildBasicInfo() {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 书名
Text(
widget.book.title,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
height: 1.3,
),
),
const SizedBox(height: 16),
// 评分和状态
Row(
children: [
if (widget.book.rating != null) ...[
const Icon(
Icons.star,
size: 20,
color: Color(0xFF1A1A1A),
),
const SizedBox(width: 4),
Text(
widget.book.rating!.toStringAsFixed(1),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
),
const SizedBox(width: 16),
],
_buildStatusTag(),
],
),
const SizedBox(height: 8),
// 时间信息
Text(
'添加于 ${_formatDate(widget.book.createdAt)}',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
],
),
);
}
/// 构建状态标签
Widget _buildStatusTag() {
String label;
Color color;
switch (widget.book.status) {
case 'read':
label = '已读';
color = const Color(0xFF1A1A1A);
break;
case 'reading':
label = '在读';
color = const Color(0xFF666666);
break;
case 'want_to_read':
label = '想读';
color = const Color(0xFF999999);
break;
default:
label = '未知';
color = const Color(0xFFCCCCCC);
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: color,
),
child: Text(
label,
style: const TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
);
}
/// 构建作者区域
Widget _buildAuthorsSection() {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'作者',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFF999999),
letterSpacing: 1,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: widget.book.authors.map((author) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
border: Border.all(color: const Color(0xFFE5E5E5)),
),
child: Text(
author,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF1A1A1A),
),
),
);
}).toList(),
),
],
),
);
}
/// 构建出版社区域
Widget _buildPublisherSection() {
return Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'出版社',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFF999999),
letterSpacing: 1,
),
),
const SizedBox(height: 8),
Text(
widget.book.publisher!,
style: const TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
),
),
],
),
);
}
/// 构建类型区域
Widget _buildGenresSection() {
return Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'类型',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFF999999),
letterSpacing: 1,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: widget.book.genres.map((genre) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE5E5E5)),
),
child: Text(
genre,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF666666),
),
),
);
}).toList(),
),
],
),
);
}
/// 构建简介区域
Widget _buildSummarySection() {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'简介',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFF999999),
letterSpacing: 1,
),
),
const SizedBox(height: 12),
Text(
widget.book.summary!,
style: const TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
height: 1.6,
),
),
],
),
);
}
/// 构建别名区域
Widget _buildAlternateTitlesSection() {
return Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'别名',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFF999999),
letterSpacing: 1,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: widget.book.alternateTitles.map((title) {
return Text(
title,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF666666),
),
);
}).toList(),
),
],
),
);
}
/// 构建底部操作栏
Widget _buildBottomBar() {
return Container(
decoration: const BoxDecoration(
border: Border(
top: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => _navigateToEdit(context),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF1A1A1A),
side: const BorderSide(color: Color(0xFF1A1A1A)),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: const Text('编辑'),
),
),
const SizedBox(width: 16),
Expanded(
child: OutlinedButton(
onPressed: () => _showDeleteDialog(context),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red,
side: const BorderSide(color: Colors.red),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: const Text('删除'),
),
),
],
),
),
),
);
}
/// 格式化日期
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
/// 跳转到编辑页面
void _navigateToEdit(BuildContext context) {
Navigator.pushNamed(context, '/book-form', arguments: widget.book).then((_) {
context.read<AppProvider>().loadBooks();
});
}
/// 显示删除对话框
void _showDeleteDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('确认删除'),
content: Text('确定要删除"${widget.book.title}"吗?此操作不可恢复。'),
content: Text('确定要删除"${widget.book.title}"吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
),
TextButton(
onPressed: () async {
@@ -275,16 +477,10 @@ class _BookDetailPageState extends State<BookDetailPage> {
Navigator.pop(context);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已删除'),
behavior: SnackBarBehavior.floating,
),
const SnackBar(content: Text('已删除')),
);
},
child: const Text(
'删除',
style: TextStyle(color: Colors.red),
),
child: const Text('删除', style: TextStyle(color: Colors.red)),
),
],
),

View File

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

View File

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

View File

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

View File

@@ -7,328 +7,527 @@ import '../models/data_models.dart';
/// 影视详情页 - 极简主义设计
class MovieDetailPage extends StatefulWidget {
final Movie movie;
const MovieDetailPage({super.key, required this.movie});
@override
State<MovieDetailPage> createState() => _MovieDetailPageState();
}
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
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('详情'),
actions: [
TextButton(
onPressed: () => _navigateToEdit(context),
child: const Text('编辑'),
backgroundColor: Colors.white,
body: CustomScrollView(
slivers: [
// 顶部海报区域
_buildSliverAppBar(),
// 内容区域
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 基本信息
_buildBasicInfo(),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 导演
if (widget.movie.directors.isNotEmpty)
_buildDirectorsSection(),
// 编剧
if (widget.movie.writers.isNotEmpty)
_buildWritersSection(),
// 主演
if (widget.movie.actors.isNotEmpty)
_buildActorsSection(),
// 类型
if (widget.movie.genres.isNotEmpty)
_buildGenresSection(),
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
// 简介
if (widget.movie.summary != null && widget.movie.summary!.isNotEmpty)
_buildSummarySection(),
// 别名
if (widget.movie.alternateTitles.isNotEmpty)
_buildAlternateTitlesSection(),
const SizedBox(height: 48),
],
),
),
const SizedBox(width: 8),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 海报
_buildPoster(),
const SizedBox(height: 40),
// 标题
Text(
_movie.title,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
height: 1.3,
),
),
const SizedBox(height: 12),
// 状态标签
_buildStatusTag(),
const SizedBox(height: 32),
// 基本信息
_buildInfoRow(),
// 别名
if (_movie.alternateTitles.isNotEmpty) ...[
const SizedBox(height: 32),
_buildSectionTitle('别名'),
const SizedBox(height: 12),
_buildTextList(_movie.alternateTitles),
],
// 导演
if (_movie.directors.isNotEmpty) ...[
const SizedBox(height: 32),
_buildSectionTitle('导演'),
const SizedBox(height: 12),
_buildTextList(_movie.directors),
],
// 编剧
if (_movie.writers.isNotEmpty) ...[
const SizedBox(height: 32),
_buildSectionTitle('编剧'),
const SizedBox(height: 12),
_buildTextList(_movie.writers),
],
// 主演
if (_movie.actors.isNotEmpty) ...[
const SizedBox(height: 32),
_buildSectionTitle('主演'),
const SizedBox(height: 12),
_buildTextList(_movie.actors),
],
// 类型
if (_movie.genres.isNotEmpty) ...[
const SizedBox(height: 32),
_buildSectionTitle('类型'),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: _movie.genres.map((genre) => Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
border: Border.all(
color: const Color(0xFFE5E5E5),
width: 0.5,
),
),
child: Text(
genre,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF666666),
),
),
)).toList(),
),
],
// 剧情简介
if (_movie.summary != null && _movie.summary!.isNotEmpty) ...[
const SizedBox(height: 32),
_buildSectionTitle('简介'),
const SizedBox(height: 12),
Text(
_movie.summary!,
style: const TextStyle(
fontSize: 15,
color: Color(0xFF666666),
height: 1.7,
),
),
],
const SizedBox(height: 48),
// 删除按钮
Center(
child: TextButton(
onPressed: () => _showDeleteDialog(context),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFFDC2626),
),
child: const Text('删除此影片'),
),
),
const SizedBox(height: 24),
],
),
),
// 底部操作栏
bottomNavigationBar: _buildBottomBar(),
);
}
/// 海报
Widget _buildPoster() {
return Center(
child: Container(
width: 140,
height: 200,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
border: Border.all(
color: const Color(0xFFE5E5E5),
width: 0.5,
),
),
child: _movie.posterPath != null && _movie.posterPath!.isNotEmpty
? Image.file(
File(_movie.posterPath!),
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _buildPlaceholder(),
)
: _buildPlaceholder(),
/// 构建顶部 AppBar
Widget _buildSliverAppBar() {
return SliverAppBar(
expandedHeight: 280,
pinned: true,
backgroundColor: Colors.white,
flexibleSpace: FlexibleSpaceBar(
background: _buildPosterSection(),
),
actions: [
IconButton(
icon: const Icon(Icons.edit_outlined),
onPressed: () => _navigateToEdit(context),
),
const SizedBox(width: 8),
],
);
}
Widget _buildPlaceholder() {
return const Center(
child: Text(
'无海报',
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
),
),
);
}
/// 状态标签
Widget _buildStatusTag() {
String label;
Color bgColor;
Color textColor;
switch (_movie.status) {
case 'watched':
label = '已看';
bgColor = const Color(0xFF1A1A1A);
textColor = Colors.white;
break;
case 'watching':
label = '在看';
bgColor = const Color(0xFF666666);
textColor = Colors.white;
break;
case 'want_to_watch':
label = '想看';
bgColor = const Color(0xFFF5F5F5);
textColor = const Color(0xFF666666);
break;
default:
label = '未知';
bgColor = const Color(0xFFF5F5F5);
textColor = const Color(0xFF999999);
}
/// 构建海报区域
Widget _buildPosterSection() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.zero,
),
child: Text(
label,
style: TextStyle(
fontSize: 12,
color: textColor,
fontWeight: FontWeight.w500,
),
width: double.infinity,
color: const Color(0xFFF5F5F5),
child: widget.movie.posterPath != null && widget.movie.posterPath!.isNotEmpty
? Image.file(
File(widget.movie.posterPath!),
fit: BoxFit.contain,
errorBuilder: (_, __, ___) => _buildPosterPlaceholder(),
)
: _buildPosterPlaceholder(),
);
}
Widget _buildPosterPlaceholder() {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.movie_outlined,
size: 64,
color: Color(0xFFCCCCCC),
),
SizedBox(height: 16),
Text(
'暂无海报',
style: TextStyle(
fontSize: 14,
color: Color(0xFF999999),
),
),
],
),
);
}
/// 基本信息
Widget _buildInfoRow() {
final items = <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: [
/// 构建基本信息
Widget _buildBasicInfo() {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 影视名称
Text(
widget.movie.title,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
height: 1.3,
),
),
const SizedBox(height: 16),
// 评分和状态
Row(
children: [
if (widget.movie.rating != null) ...[
const Icon(
Icons.star,
size: 20,
color: Color(0xFF1A1A1A),
),
const SizedBox(width: 4),
Text(
widget.movie.rating!.toStringAsFixed(1),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
),
const SizedBox(width: 16),
],
_buildStatusTag(),
],
),
const SizedBox(height: 8),
// 上映日期
if (widget.movie.releaseDate != null)
Text(
entry.value,
'${widget.movie.releaseDate!.year}年上映',
style: const TextStyle(
fontSize: 14,
color: Color(0xFF999999),
),
),
if (entry.key < items.length - 1)
const Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: Text(
'·',
style: TextStyle(
fontSize: 14,
color: Color(0xFFCCCCCC),
),
),
),
],
);
}).toList(),
);
}
/// 区块标题
Widget _buildSectionTitle(String title) {
return Text(
title.toUpperCase(),
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: Color(0xFF999999),
letterSpacing: 1,
const SizedBox(height: 8),
// 时间信息
Text(
'添加于 ${_formatDate(widget.movie.createdAt)}',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
],
),
);
}
/// 文本列表
Widget _buildTextList(List<String> items) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: items.map((item) => Text(
item,
/// 构建状态标签
Widget _buildStatusTag() {
String label;
Color color;
switch (widget.movie.status) {
case 'watched':
label = '已看';
color = const Color(0xFF1A1A1A);
break;
case 'watching':
label = '在看';
color = const Color(0xFF666666);
break;
case 'want_to_watch':
label = '想看';
color = const Color(0xFF999999);
break;
default:
label = '未知';
color = const Color(0xFFCCCCCC);
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: color,
),
child: Text(
label,
style: const TextStyle(
fontSize: 15,
color: Color(0xFF333333),
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.w500,
),
)).toList(),
),
);
}
/// 跳转到编辑
/// 构建导演区域
Widget _buildDirectorsSection() {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'导演',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFF999999),
letterSpacing: 1,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: widget.movie.directors.map((director) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
border: Border.all(color: const Color(0xFFE5E5E5)),
),
child: Text(
director,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF1A1A1A),
),
),
);
}).toList(),
),
],
),
);
}
/// 构建编剧区域
Widget _buildWritersSection() {
return Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'编剧',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFF999999),
letterSpacing: 1,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: widget.movie.writers.map((writer) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
border: Border.all(color: const Color(0xFFE5E5E5)),
),
child: Text(
writer,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF1A1A1A),
),
),
);
}).toList(),
),
],
),
);
}
/// 构建主演区域
Widget _buildActorsSection() {
return Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'主演',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFF999999),
letterSpacing: 1,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: widget.movie.actors.map((actor) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
border: Border.all(color: const Color(0xFFE5E5E5)),
),
child: Text(
actor,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF1A1A1A),
),
),
);
}).toList(),
),
],
),
);
}
/// 构建类型区域
Widget _buildGenresSection() {
return Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'类型',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFF999999),
letterSpacing: 1,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: widget.movie.genres.map((genre) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE5E5E5)),
),
child: Text(
genre,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF666666),
),
),
);
}).toList(),
),
],
),
);
}
/// 构建简介区域
Widget _buildSummarySection() {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'简介',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFF999999),
letterSpacing: 1,
),
),
const SizedBox(height: 12),
Text(
widget.movie.summary!,
style: const TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
height: 1.6,
),
),
],
),
);
}
/// 构建别名区域
Widget _buildAlternateTitlesSection() {
return Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'别名',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFF999999),
letterSpacing: 1,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: widget.movie.alternateTitles.map((title) {
return Text(
title,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF666666),
),
);
}).toList(),
),
],
),
);
}
/// 构建底部操作栏
Widget _buildBottomBar() {
return Container(
decoration: const BoxDecoration(
border: Border(
top: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => _navigateToEdit(context),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF1A1A1A),
side: const BorderSide(color: Color(0xFF1A1A1A)),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: const Text('编辑'),
),
),
const SizedBox(width: 16),
Expanded(
child: OutlinedButton(
onPressed: () => _showDeleteDialog(context),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red,
side: const BorderSide(color: Colors.red),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: const Text('删除'),
),
),
],
),
),
),
);
}
/// 格式化日期
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
/// 跳转到编辑页面
void _navigateToEdit(BuildContext context) {
Navigator.pushNamed(context, '/movie-form', arguments: _movie).then((_) {
_refreshMovie();
Navigator.pushNamed(context, '/movie-form', arguments: widget.movie).then((_) {
context.read<AppProvider>().loadMovies();
});
}
/// 删除对话框
/// 显示删除对话框
void _showDeleteDialog(BuildContext context) {
showDialog(
context: context,
@@ -336,40 +535,24 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text(
'确认删除',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
),
content: Text(
'确定要删除"${_movie.title}"吗?',
style: const TextStyle(
fontSize: 15,
color: Color(0xFF666666),
),
),
title: const Text('确认删除'),
content: Text('确定要删除"${widget.movie.title}"吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(
'取消',
style: TextStyle(color: Color(0xFF666666)),
),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
),
TextButton(
onPressed: () async {
await context.read<AppProvider>().removeMovie(_movie.id);
if (!context.mounted) return;
await context.read<AppProvider>().removeMovie(widget.movie.id);
if (!mounted) return;
Navigator.pop(context);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已删除')),
);
},
child: const Text(
'删除',
style: TextStyle(color: Color(0xFFDC2626)),
),
child: const Text('删除', style: TextStyle(color: Colors.red)),
),
],
),

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

View File

@@ -3,9 +3,9 @@ import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
/// 添加/编辑笔记页面
/// 添加/编辑笔记页面 - 极简书写界面
class NoteFormPage extends StatefulWidget {
final Note? note; // 如果为 null则是添加模式否则是编辑模式
final Note? note;
const NoteFormPage({super.key, this.note});
@@ -14,170 +14,270 @@ class NoteFormPage extends StatefulWidget {
}
class _NoteFormPageState extends State<NoteFormPage> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _titleController;
late TextEditingController _contentController;
late TextEditingController _tagsController;
late DateTime _createdAt;
List<String> _tags = [];
bool _isEditing = false;
@override
void initState() {
super.initState();
_titleController = TextEditingController(text: widget.note?.title ?? '');
_contentController = TextEditingController(text: widget.note?.content ?? '');
_tagsController = TextEditingController(
text: widget.note?.tags.join(', ') ?? '',
);
final note = widget.note;
_contentController = TextEditingController(text: note?.content ?? '');
_createdAt = note?.createdAt ?? DateTime.now();
_tags = note != null ? List.from(note.tags) : [];
_isEditing = note != null;
}
@override
void dispose() {
_titleController.dispose();
_contentController.dispose();
_tagsController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isEdit = widget.note != null;
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text(isEdit ? '编辑笔记' : '添加笔记'),
title: Text(_isEditing ? '编辑笔记' : '新建笔记'),
actions: [
IconButton(
icon: const Icon(Icons.save),
TextButton(
onPressed: _saveNote,
child: const Text(
'保存',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(width: 8),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
TextFormField(
controller: _titleController,
decoration: const InputDecoration(
labelText: '标题 *',
hintText: '请输入笔记标题',
prefixIcon: Icon(Icons.title),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入标题';
}
return null;
},
body: Column(
children: [
// 顶部信息栏:创建时间 + 标签
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
),
const SizedBox(height: 16),
// 标签
TextFormField(
controller: _tagsController,
decoration: const InputDecoration(
labelText: '标签',
hintText: '多个标签用逗号分隔',
prefixIcon: Icon(Icons.local_offer),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
// 内容
TextFormField(
controller: _contentController,
decoration: const InputDecoration(
labelText: '内容 *',
hintText: '请输入笔记内容...',
prefixIcon: Icon(Icons.edit_note),
border: OutlineInputBorder(),
alignLabelWithHint: true,
),
maxLines: 15,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入内容';
}
return null;
},
),
const SizedBox(height: 32),
// 保存按钮
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton.icon(
onPressed: _saveNote,
icon: const Icon(Icons.save),
label: Text(isEdit ? '保存修改' : '添加笔记'),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Row(
children: [
// 创建时间
Text(
_formatDateTime(_createdAt),
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
),
],
const SizedBox(width: 16),
// 标签
Expanded(
child: _buildTagSelector(),
),
],
),
),
),
// 书写区域
Expanded(
child: TextField(
controller: _contentController,
maxLines: null,
expands: true,
textAlignVertical: TextAlignVertical.top,
style: const TextStyle(
fontSize: 16,
color: Color(0xFF1A1A1A),
height: 1.6,
),
decoration: const InputDecoration(
hintText: '开始书写...',
hintStyle: TextStyle(
fontSize: 16,
color: Color(0xFFCCCCCC),
),
border: InputBorder.none,
contentPadding: EdgeInsets.all(16),
),
),
),
],
),
);
}
/// 构建标签选择器
Widget _buildTagSelector() {
return Wrap(
spacing: 8,
runSpacing: 4,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
..._tags.asMap().entries.map((entry) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
border: Border.all(color: const Color(0xFFE5E5E5)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
entry.value,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF666666),
),
),
const SizedBox(width: 4),
GestureDetector(
onTap: () => setState(() => _tags.removeAt(entry.key)),
child: const Icon(
Icons.close,
size: 12,
color: Color(0xFF999999),
),
),
],
),
);
}),
// 添加标签按钮
GestureDetector(
onTap: () => _showAddTagDialog(),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE5E5E5)),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.add,
size: 12,
color: Color(0xFF999999),
),
SizedBox(width: 2),
Text(
'标签',
style: TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
],
),
),
),
],
);
}
/// 显示添加标签对话框
void _showAddTagDialog() {
final controller = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text(
'添加标签',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
content: TextField(
controller: controller,
autofocus: true,
decoration: const InputDecoration(
hintText: '输入标签名称',
border: UnderlineInputBorder(),
),
onSubmitted: (value) {
_addTag(value);
Navigator.pop(context);
},
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
),
TextButton(
onPressed: () {
_addTag(controller.text);
Navigator.pop(context);
},
child: const Text('添加'),
),
],
),
);
}
/// 添加标签
void _addTag(String tag) {
final trimmed = tag.trim();
if (trimmed.isNotEmpty && !_tags.contains(trimmed)) {
setState(() => _tags.add(trimmed));
}
}
/// 格式化日期时间
String _formatDateTime(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
}
/// 保存笔记
Future<void> _saveNote() async {
if (!_formKey.currentState!.validate()) {
final content = _contentController.text.trim();
if (content.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('笔记内容不能为空')),
);
return;
}
// 解析标签
final tags = _tagsController.text
.split(',')
.map((tag) => tag.trim())
.where((tag) => tag.isNotEmpty)
.toList();
final now = DateTime.now();
if (widget.note == null) {
// 添加新模式
final now = DateTime.now();
final newNote = Note(
id: now.millisecondsSinceEpoch.toString(),
title: _titleController.text.trim(),
content: _contentController.text.trim(),
tags: tags,
createdAt: now,
if (_isEditing) {
// 更新现有笔记
final updatedNote = widget.note!.copyWith(
content: content,
tags: _tags,
updatedAt: now,
);
await context.read<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);
} 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;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(widget.note == null ? '添加成功' : '更新成功'),
content: Text(_isEditing ? '保存成功' : '添加成功'),
behavior: SnackBarBehavior.floating,
),
);

View File

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

View File

@@ -6,25 +6,17 @@ import 'database_helper.dart';
class BookDao {
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
// 获取所有书籍记录
// 获取所有未删除的书籍记录
Future<List<Book>> getAllBooks() async {
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 Book(
id: maps[i]['id'].toString(),
title: maps[i]['title'],
author: maps[i]['author'],
cover: maps[i]['cover'],
rating: maps[i]['rating']?.toDouble(),
status: maps[i]['status'],
readDate: maps[i]['read_date'] != null
? DateTime.parse(maps[i]['read_date'])
: null,
note: maps[i]['note'],
);
});
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
}
// 根据状态筛选书籍记录
@@ -32,24 +24,25 @@ class BookDao {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'books',
where: 'status = ?',
whereArgs: [status],
where: 'status = ? AND is_deleted = ?',
whereArgs: [status, 0],
orderBy: 'updated_at DESC',
);
return List.generate(maps.length, (i) {
return Book(
id: maps[i]['id'].toString(),
title: maps[i]['title'],
author: maps[i]['author'],
cover: maps[i]['cover'],
rating: maps[i]['rating']?.toDouble(),
status: maps[i]['status'],
readDate: maps[i]['read_date'] != null
? DateTime.parse(maps[i]['read_date'])
: null,
note: maps[i]['note'],
);
});
return List.generate(maps.length, (i) => Book.fromJson(maps[i]));
}
// 根据ID获取书籍
Future<Book?> getBookById(String id) async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'books',
where: 'id = ? AND is_deleted = ?',
whereArgs: [id, 0],
);
if (maps.isEmpty) return null;
return Book.fromJson(maps.first);
}
// 添加书籍记录
@@ -69,7 +62,21 @@ class BookDao {
);
}
// 删除书籍记录
// 删除书籍记录
Future<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 {
final db = await _dbHelper.database;
return await db.delete(
@@ -78,4 +85,43 @@ class BookDao {
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(
path,
version: 2,
version: 4,
onCreate: _createDB,
onUpgrade: _onUpgrade,
);
@@ -32,6 +32,110 @@ class DatabaseHelper {
// 升级movies表结构
await _upgradeMoviesTableV2(db);
}
if (oldVersion < 3) {
// 升级books表结构
await _upgradeBooksTableV3(db);
}
if (oldVersion < 4) {
// 升级notes表结构
await _upgradeNotesTableV4(db);
}
}
/// 升级notes表到V4
Future<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
@@ -120,26 +224,31 @@ class DatabaseHelper {
// 书籍表
await db.execute('''
CREATE TABLE books (
id $idType,
title $textType,
author TEXT,
cover TEXT,
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
cover_path TEXT,
authors TEXT,
alternate_titles TEXT,
publisher TEXT,
genres TEXT,
summary TEXT,
rating REAL,
status $textType,
read_date TEXT,
note TEXT
status TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
is_deleted INTEGER DEFAULT 0
)
''');
// 笔记表
await db.execute('''
CREATE TABLE notes (
id $idType,
title $textType,
content $textType,
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
content_type TEXT DEFAULT 'markdown',
tags TEXT,
created_at $textType,
updated_at $textType
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
''');
}

View File

@@ -14,31 +14,26 @@ class NoteDao {
orderBy: 'updated_at DESC',
);
return List.generate(maps.length, (i) {
return Note(
id: maps[i]['id'].toString(),
title: maps[i]['title'],
content: maps[i]['content'],
tags: maps[i]['tags'] != null
? List<String>.from(maps[i]['tags'].split(','))
: [],
createdAt: DateTime.parse(maps[i]['created_at']),
updatedAt: DateTime.parse(maps[i]['updated_at']),
);
});
return List.generate(maps.length, (i) => Note.fromJson(maps[i]));
}
// 根据ID获取笔记
Future<Note?> getNoteById(String id) async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'notes',
where: 'id = ?',
whereArgs: [id],
);
if (maps.isEmpty) return null;
return Note.fromJson(maps.first);
}
// 添加笔记
Future<int> insertNote(Note note) async {
final db = await _dbHelper.database;
return await db.insert('notes', {
'id': note.id,
'title': note.title,
'content': note.content,
'tags': note.tags.join(','),
'created_at': note.createdAt.toIso8601String(),
'updated_at': note.updatedAt.toIso8601String(),
});
return await db.insert('notes', note.toJson());
}
// 更新笔记
@@ -46,12 +41,7 @@ class NoteDao {
final db = await _dbHelper.database;
return await db.update(
'notes',
{
'title': note.title,
'content': note.content,
'tags': note.tags.join(','),
'updated_at': note.updatedAt.toIso8601String(),
},
note.toJson(),
where: 'id = ?',
whereArgs: [note.id],
);
@@ -66,4 +56,30 @@ class NoteDao {
whereArgs: [id],
);
}
// 搜索笔记
Future<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:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
import '../utils/app_theme.dart';
/// 书籍列表项组件
/// 书籍列表项组件 - 极简主义设计
class BookListItem extends StatelessWidget {
final Book book;
const BookListItem({super.key, required this.book});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
onTap: () {
// 跳转到详情页
Navigator.pushNamed(context, '/book-detail', arguments: book);
},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 封面占位图
_buildCover(),
const SizedBox(width: 12),
// 书籍信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
return InkWell(
onTap: () {
Navigator.pushNamed(context, '/book-detail', arguments: book);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 封面
_buildCover(),
const SizedBox(width: 16),
// 信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 书名
Text(
book.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF1A1A1A),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
// 作者
if (book.authors.isNotEmpty)
Text(
book.title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
book.authors.join(' / '),
style: const TextStyle(
fontSize: 13,
color: Color(0xFF666666),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (book.author != null) ...[
const SizedBox(height: 4),
Text(
book.author!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
const SizedBox(height: 8),
// 评分和状态
Row(
children: [
if (book.rating != null) ...[
const Icon(
Icons.star,
size: 14,
color: Color(0xFF1A1A1A),
),
),
],
const SizedBox(height: 8),
// 评分和状态
Row(
children: [
if (book.rating != null) ...[
Icon(
Icons.star,
size: 16,
color: Colors.amber[700],
const SizedBox(width: 2),
Text(
book.rating!.toStringAsFixed(1),
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Color(0xFF1A1A1A),
),
const SizedBox(width: 4),
Text(
book.rating.toString(),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.amber[700],
),
),
const SizedBox(width: 12),
],
// 状态标签
_buildStatusTag(context),
),
const SizedBox(width: 12),
],
_buildStatusTag(),
],
),
// 类型
if (book.genres.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
book.genres.take(3).join(' · '),
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (book.readDate != null) ...[
const SizedBox(height: 6),
Text(
'阅读日期:${_formatDate(book.readDate!)}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
if (book.note != null && book.note!.isNotEmpty) ...[
const SizedBox(height: 6),
Text(
book.note!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
// 右侧操作按钮
Column(
children: [
IconButton(
icon: const Icon(Icons.edit, size: 20),
onPressed: () {
Navigator.pushNamed(context, '/book-form', arguments: book);
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
IconButton(
icon: const Icon(Icons.delete_outline, size: 20),
color: Colors.red,
onPressed: () => _showDeleteDialog(context, book),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
],
),
),
// 操作按钮
Column(
children: [
IconButton(
icon: const Icon(Icons.edit_outlined, size: 20),
color: const Color(0xFF666666),
onPressed: () {
Navigator.pushNamed(context, '/book-form', arguments: book);
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
const SizedBox(height: 8),
IconButton(
icon: const Icon(Icons.delete_outline, size: 20),
color: Colors.red,
onPressed: () => _showDeleteDialog(context),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
],
),
),
);
}
/// 构建封面占位图
/// 构建封面
Widget _buildCover() {
return Container(
width: 60,
height: 90,
height: 80,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(8),
color: const Color(0xFFF5F5F5),
border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5),
),
child: book.coverPath != null && book.coverPath!.isNotEmpty
? Image.file(
File(book.coverPath!),
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _buildCoverPlaceholder(),
)
: _buildCoverPlaceholder(),
);
}
Widget _buildCoverPlaceholder() {
return const Center(
child: Icon(
Icons.menu_book,
color: Colors.grey[500],
size: 32,
size: 24,
color: Color(0xFFCCCCCC),
),
);
}
/// 构建状态标签
Widget _buildStatusTag(BuildContext context) {
Color statusColor;
String statusText;
Widget _buildStatusTag() {
String label;
Color color;
switch (book.status) {
case 'read':
statusColor = AppTheme.readColor;
statusText = '读完';
label = '已读';
color = const Color(0xFF1A1A1A);
break;
case 'reading':
statusColor = AppTheme.readingColor;
statusText = '在读';
label = '在读';
color = const Color(0xFF666666);
break;
case 'want_to_read':
statusColor = AppTheme.wantToReadColor;
statusText = '准备读';
label = '想读';
color = const Color(0xFF999999);
break;
default:
statusColor = Colors.grey;
statusText = '未知';
label = '未知';
color = const Color(0xFFCCCCCC);
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: statusColor,
width: 1,
),
color: color.withValues(alpha: 0.1),
border: Border.all(color: color),
),
child: Text(
statusText,
label,
style: TextStyle(
fontSize: 11,
color: statusColor,
fontWeight: FontWeight.bold,
color: color,
fontWeight: FontWeight.w500,
),
),
);
}
/// 格式化日期
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
/// 显示删除对话框
void _showDeleteDialog(BuildContext context, Book book) {
void _showDeleteDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('确认删除'),
content: Text('确定要删除"${book.title}"吗?此操作不可恢复。'),
content: Text('确定要删除"${book.title}"吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
),
TextButton(
onPressed: () async {
@@ -219,16 +222,10 @@ class BookListItem extends StatelessWidget {
if (!context.mounted) return;
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已删除'),
behavior: SnackBarBehavior.floating,
),
const SnackBar(content: Text('已删除')),
);
},
child: const Text(
'删除',
style: TextStyle(color: Colors.red),
),
child: const Text('删除', style: TextStyle(color: Colors.red)),
),
],
),

View File

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

View File

@@ -1,17 +1,21 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
/// 观影列表项 - 极简主义设计
/// 观影列表项组件 - 极简主义设计
class MovieListItem extends StatelessWidget {
final Movie movie;
const MovieListItem({super.key, required this.movie});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => Navigator.pushNamed(context, '/movie-detail', arguments: movie),
onTap: () {
Navigator.pushNamed(context, '/movie-detail', arguments: movie);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: const BoxDecoration(
@@ -32,7 +36,7 @@ class MovieListItem extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
// 影视名称
Text(
movie.title,
style: const TextStyle(
@@ -44,12 +48,7 @@ class MovieListItem extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 6),
// 年份和评分
_buildMetaInfo(),
const SizedBox(height: 8),
const SizedBox(height: 4),
// 导演
if (movie.directors.isNotEmpty)
@@ -57,7 +56,7 @@ class MovieListItem extends StatelessWidget {
movie.directors.take(2).join(' / '),
style: const TextStyle(
fontSize: 13,
color: Color(0xFF999999),
color: Color(0xFF666666),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
@@ -65,102 +64,108 @@ class MovieListItem extends StatelessWidget {
const SizedBox(height: 8),
// 状态
_buildStatusTag(),
// 评分和状态
Row(
children: [
if (movie.rating != null) ...[
const Icon(
Icons.star,
size: 14,
color: Color(0xFF1A1A1A),
),
const SizedBox(width: 2),
Text(
movie.rating!.toStringAsFixed(1),
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Color(0xFF1A1A1A),
),
),
const SizedBox(width: 12),
],
_buildStatusTag(),
],
),
// 类型
if (movie.genres.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
movie.genres.take(3).join(' · '),
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
// 箭头
const Icon(
Icons.chevron_right,
size: 20,
color: Color(0xFFCCCCCC),
// 操作按钮
Column(
children: [
IconButton(
icon: const Icon(Icons.edit_outlined, size: 20),
color: const Color(0xFF666666),
onPressed: () {
Navigator.pushNamed(context, '/movie-form', arguments: movie);
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
const SizedBox(height: 8),
IconButton(
icon: const Icon(Icons.delete_outline, size: 20),
color: Colors.red,
onPressed: () => _showDeleteDialog(context),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
],
),
),
);
}
/// 海报
/// 构建海报
Widget _buildPoster() {
return Container(
width: 56,
width: 60,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
border: Border.all(
color: const Color(0xFFE5E5E5),
width: 0.5,
),
border: Border.all(color: const Color(0xFFE5E5E5), width: 0.5),
),
child: movie.posterPath != null && movie.posterPath!.isNotEmpty
? Image.file(
File(movie.posterPath!),
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _buildPlaceholder(),
errorBuilder: (_, __, ___) => _buildPosterPlaceholder(),
)
: _buildPlaceholder(),
: _buildPosterPlaceholder(),
);
}
Widget _buildPlaceholder() {
Widget _buildPosterPlaceholder() {
return const Center(
child: Icon(
Icons.movie_outlined,
size: 20,
size: 24,
color: Color(0xFFCCCCCC),
),
);
}
/// 元信息
Widget _buildMetaInfo() {
final items = <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() {
String label;
Color color;
switch (movie.status) {
case 'watched':
label = '已看';
@@ -179,12 +184,50 @@ class MovieListItem extends StatelessWidget {
color = const Color(0xFFCCCCCC);
}
return Text(
label,
style: TextStyle(
fontSize: 12,
color: color,
fontWeight: FontWeight.w500,
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
border: Border.all(color: color),
),
child: Text(
label,
style: TextStyle(
fontSize: 11,
color: color,
fontWeight: FontWeight.w500,
),
),
);
}
/// 显示删除对话框
void _showDeleteDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.white,
elevation: 0,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
title: const Text('确认删除'),
content: Text('确定要删除"${movie.title}"吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消', style: TextStyle(color: Color(0xFF666666))),
),
TextButton(
onPressed: () async {
await context.read<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) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
color: Colors.white,
decoration: const BoxDecoration(
color: Colors.white,
border: Border(
bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
),
),
child: Row(
children: [
_buildStatusItem(
context,
'已看',
0,
provider.movieStatusIndex,
() => provider.setMovieStatusIndex(0),
),
const SizedBox(width: 24),
const SizedBox(width: 16),
_buildStatusItem(
'想看',
context,
'在看',
1,
provider.movieStatusIndex,
() => provider.setMovieStatusIndex(1),
),
const SizedBox(width: 24),
const SizedBox(width: 16),
_buildStatusItem(
'在看',
context,
'想看',
2,
provider.movieStatusIndex,
() => provider.setMovieStatusIndex(2),
@@ -44,6 +52,7 @@ class MovieStatusBar extends StatelessWidget {
/// 构建状态项
Widget _buildStatusItem(
BuildContext context,
String label,
int index,
int currentIndex,
@@ -51,14 +60,39 @@ class MovieStatusBar extends StatelessWidget {
) {
final isSelected = index == currentIndex;
return GestureDetector(
onTap: onTap,
child: Text(
label,
style: TextStyle(
fontSize: 15,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
color: isSelected ? const Color(0xFF1A1A1A) : const Color(0xFF999999),
Color color;
switch (index) {
case 0:
color = const Color(0xFF1A1A1A);
break;
case 1:
color = const Color(0xFF666666);
break;
case 2:
color = const Color(0xFF999999);
break;
default:
color = const Color(0xFFCCCCCC);
}
return Expanded(
child: InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: isSelected ? color : Colors.transparent,
border: Border.all(color: color),
),
child: Text(
label,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal,
color: isSelected ? Colors.white : color,
),
),
),
),
);

View File

@@ -3,7 +3,7 @@ import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../models/data_models.dart';
/// 笔记列表项组件
/// 笔记列表项组件 - 极简主义设计
class NoteListItem extends StatelessWidget {
final Note note;
@@ -11,104 +11,75 @@ class NoteListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
onTap: () {
// 跳转到详情页
Navigator.pushNamed(context, '/note-detail', arguments: note);
},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Text(
note.title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
// 内容摘要
Text(
note.content,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
// 标签
if (note.tags.isNotEmpty) ...[
Wrap(
spacing: 6,
runSpacing: 6,
children: note.tags.map((tag) => _buildTag(context, tag)).toList(),
),
],
const SizedBox(height: 12),
// 时间信息和操作按钮
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_formatDate(note.updatedAt),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit, size: 20),
onPressed: () {
Navigator.pushNamed(context, '/note-form', arguments: note);
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
IconButton(
icon: const Icon(Icons.delete_outline, size: 20),
color: Colors.red,
onPressed: () => _showDeleteDialog(context, note),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
],
),
],
return InkWell(
onTap: () {
Navigator.pushNamed(context, '/note-detail', arguments: note);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xFFE5E5E5), width: 0.5),
),
),
),
);
}
/// 构建标签
Widget _buildTag(BuildContext context, String tag) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'#$tag',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onPrimaryContainer,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 内容摘要
Text(
note.summary,
style: const TextStyle(
fontSize: 15,
color: Color(0xFF1A1A1A),
height: 1.5,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
// 底部信息:标签 + 时间 + 操作
Row(
children: [
// 标签
if (note.tags.isNotEmpty) ...[
Expanded(
child: Wrap(
spacing: 8,
runSpacing: 4,
children: note.tags.take(3).map((tag) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
border: Border.all(color: const Color(0xFFE5E5E5)),
),
child: Text(
tag,
style: const TextStyle(
fontSize: 11,
color: Color(0xFF666666),
),
),
);
}).toList(),
),
),
] else
const Spacer(),
// 时间
Text(
_formatDate(note.updatedAt),
style: const TextStyle(
fontSize: 12,
color: Color(0xFF999999),
),
),
],
),
],
),
),
);
@@ -133,38 +104,4 @@ class NoteListItem extends StatelessWidget {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
}
/// 显示删除对话框
void _showDeleteDialog(BuildContext context, Note note) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认删除'),
content: Text('确定要删除"${note.title}"吗?此操作不可恢复。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () async {
await context.read<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),
),
),
],
),
);
}
}