generated from dellevin/template
基础功能
This commit is contained in:
41
lib/main.dart
Normal file
41
lib/main.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'pages/home_page.dart';
|
||||
import 'utils/app_theme.dart';
|
||||
import 'utils/app_router.dart';
|
||||
import 'providers/app_provider.dart';
|
||||
|
||||
void main() async {
|
||||
// 确保 Flutter 绑定初始化完成
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// 初始化数据库
|
||||
final appProvider = AppProvider();
|
||||
await appProvider.initDatabase();
|
||||
|
||||
runApp(MyApp(appProvider: appProvider));
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
final AppProvider appProvider;
|
||||
|
||||
const MyApp({super.key, required this.appProvider});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: appProvider),
|
||||
],
|
||||
child: MaterialApp(
|
||||
title: 'MookNote',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppTheme.lightTheme,
|
||||
darkTheme: AppTheme.darkTheme,
|
||||
themeMode: ThemeMode.system,
|
||||
home: const HomePage(),
|
||||
onGenerateRoute: AppRouter.generateRoute,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
148
lib/models/data_models.dart
Normal file
148
lib/models/data_models.dart
Normal file
@@ -0,0 +1,148 @@
|
||||
/// 影视条目模型
|
||||
class Movie {
|
||||
final String id;
|
||||
final String title;
|
||||
final String? poster;
|
||||
final double? rating;
|
||||
final int? year;
|
||||
final String status; // 'watched', 'want_to_watch', 'watching'
|
||||
final DateTime? watchDate;
|
||||
final String? note;
|
||||
|
||||
Movie({
|
||||
required this.id,
|
||||
required this.title,
|
||||
this.poster,
|
||||
this.rating,
|
||||
this.year,
|
||||
required this.status,
|
||||
this.watchDate,
|
||||
this.note,
|
||||
});
|
||||
|
||||
factory Movie.fromJson(Map<String, dynamic> json) {
|
||||
return Movie(
|
||||
id: json['id'] ?? '',
|
||||
title: json['title'] ?? '',
|
||||
poster: json['poster'],
|
||||
rating: json['rating']?.toDouble(),
|
||||
year: json['year'],
|
||||
status: json['status'] ?? 'want_to_watch',
|
||||
watchDate: json['watch_date'] != null
|
||||
? DateTime.parse(json['watch_date'])
|
||||
: null,
|
||||
note: json['note'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'poster': poster,
|
||||
'rating': rating,
|
||||
'year': year,
|
||||
'status': status,
|
||||
'watch_date': watchDate?.toIso8601String(),
|
||||
'note': note,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 书籍条目模型
|
||||
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;
|
||||
|
||||
Book({
|
||||
required this.id,
|
||||
required this.title,
|
||||
this.author,
|
||||
this.cover,
|
||||
this.rating,
|
||||
required this.status,
|
||||
this.readDate,
|
||||
this.note,
|
||||
});
|
||||
|
||||
factory Book.fromJson(Map<String, dynamic> json) {
|
||||
return Book(
|
||||
id: json['id'] ?? '',
|
||||
title: json['title'] ?? '',
|
||||
author: json['author'],
|
||||
cover: json['cover'],
|
||||
rating: json['rating']?.toDouble(),
|
||||
status: json['status'] ?? 'want_to_read',
|
||||
readDate: json['read_date'] != null
|
||||
? DateTime.parse(json['read_date'])
|
||||
: null,
|
||||
note: json['note'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'author': author,
|
||||
'cover': cover,
|
||||
'rating': rating,
|
||||
'status': status,
|
||||
'read_date': readDate?.toIso8601String(),
|
||||
'note': note,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 笔记模型
|
||||
class Note {
|
||||
final String id;
|
||||
final String title;
|
||||
final String content;
|
||||
final List<String> tags;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
Note({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.content,
|
||||
this.tags = const [],
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory Note.fromJson(Map<String, dynamic> json) {
|
||||
return Note(
|
||||
id: json['id'] ?? '',
|
||||
title: json['title'] ?? '',
|
||||
content: json['content'] ?? '',
|
||||
tags: json['tags'] != null
|
||||
? List<String>.from(json['tags'])
|
||||
: [],
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: DateTime.now(),
|
||||
updatedAt: json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'])
|
||||
: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'content': content,
|
||||
'tags': tags,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
78
lib/models/data_models_extension.dart
Normal file
78
lib/models/data_models_extension.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
293
lib/pages/book_detail_page.dart
Normal file
293
lib/pages/book_detail_page.dart
Normal file
@@ -0,0 +1,293 @@
|
||||
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(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 16,
|
||||
right: 16,
|
||||
child: _buildStatusTag(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建状态标签
|
||||
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),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建笔记区域
|
||||
Widget _buildNoteSection(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.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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
widget.book.note!,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 格式化日期
|
||||
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(
|
||||
title: const Text('确认删除'),
|
||||
content: Text('确定要删除"${widget.book.title}"吗?此操作不可恢复。'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await context.read<AppProvider>().removeBook(widget.book.id);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('已删除'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text(
|
||||
'删除',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
287
lib/pages/book_form_page.dart
Normal file
287
lib/pages/book_form_page.dart
Normal file
@@ -0,0 +1,287 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import '../models/data_models.dart';
|
||||
|
||||
/// 添加/编辑书籍记录页面
|
||||
class BookFormPage extends StatefulWidget {
|
||||
final Book? book; // 如果为 null,则是添加模式;否则是编辑模式
|
||||
|
||||
const BookFormPage({super.key, this.book});
|
||||
|
||||
@override
|
||||
State<BookFormPage> createState() => _BookFormPageState();
|
||||
}
|
||||
|
||||
class _BookFormPageState extends State<BookFormPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late TextEditingController _titleController;
|
||||
late TextEditingController _authorController;
|
||||
late TextEditingController _ratingController;
|
||||
late TextEditingController _noteController;
|
||||
late String _status;
|
||||
DateTime? _readDate;
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_titleController.dispose();
|
||||
_authorController.dispose();
|
||||
_ratingController.dispose();
|
||||
_noteController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEdit = widget.book != null;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(isEdit ? '编辑书籍' : '添加书籍'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.save),
|
||||
onPressed: _saveBook,
|
||||
),
|
||||
],
|
||||
),
|
||||
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 之间';
|
||||
}
|
||||
}
|
||||
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(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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 选择阅读日期
|
||||
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> _saveBook() async {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final rating = _ratingController.text.isNotEmpty ? double.tryParse(_ratingController.text) : null;
|
||||
|
||||
if (widget.book == null) {
|
||||
// 添加新模式
|
||||
final newBook = Book(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
title: _titleController.text.trim(),
|
||||
author: _authorController.text.trim(),
|
||||
rating: rating,
|
||||
status: _status,
|
||||
readDate: _readDate,
|
||||
note: _noteController.text.trim(),
|
||||
);
|
||||
|
||||
await context.read<AppProvider>().addBook(newBook);
|
||||
} else {
|
||||
// 编辑现有模式
|
||||
final updatedBook = Book(
|
||||
id: widget.book!.id,
|
||||
title: _titleController.text.trim(),
|
||||
author: _authorController.text.trim(),
|
||||
rating: rating,
|
||||
status: _status,
|
||||
readDate: _readDate,
|
||||
note: _noteController.text.trim(),
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
142
lib/pages/book_tab_page.dart
Normal file
142
lib/pages/book_tab_page.dart
Normal file
@@ -0,0 +1,142 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import '../models/data_models.dart';
|
||||
import '../widgets/book_status_bar.dart';
|
||||
import '../widgets/book_list_item.dart';
|
||||
|
||||
/// 阅读标签页
|
||||
class BookTabPage extends StatelessWidget {
|
||||
const BookTabPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// 状态选择栏(读完、在读、准备读)
|
||||
const BookStatusBar(),
|
||||
|
||||
// 书籍列表
|
||||
Expanded(
|
||||
child: _buildBookList(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建书籍列表
|
||||
Widget _buildBookList(BuildContext context) {
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
// 根据状态筛选书籍
|
||||
final statusMap = {
|
||||
0: 'read',
|
||||
1: 'reading',
|
||||
2: 'want_to_read',
|
||||
};
|
||||
final currentStatus = statusMap[provider.bookStatusIndex]!;
|
||||
final books = provider.getBooksByStatus(currentStatus);
|
||||
|
||||
if (books.isEmpty) {
|
||||
return _buildEmptyState(context, provider.bookStatusIndex);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => await provider.loadBooks(),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: books.length,
|
||||
itemBuilder: (context, index) {
|
||||
return BookListItem(book: books[index]);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建空状态提示
|
||||
Widget _buildEmptyState(BuildContext context, int statusIndex) {
|
||||
final statusText = ['读完', '在读', '准备读'][statusIndex];
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.menu_book_outlined,
|
||||
size: 80,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'暂无$statusText的书籍',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('添加记录'),
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(context, '/book-form');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取示例书籍数据
|
||||
List<Book> _getSampleBooks(int statusIndex) {
|
||||
final statusMap = ['read', 'reading', 'want_to_read'];
|
||||
final currentStatus = statusMap[statusIndex];
|
||||
|
||||
// 示例数据(实际应从数据库获取)
|
||||
final allBooks = [
|
||||
Book(
|
||||
id: '1',
|
||||
title: '活着',
|
||||
author: '余华',
|
||||
rating: 9.2,
|
||||
status: 'read',
|
||||
readDate: DateTime(2024, 1, 20),
|
||||
note: '非常感人的故事,让人思考生命的意义',
|
||||
),
|
||||
Book(
|
||||
id: '2',
|
||||
title: '百年孤独',
|
||||
author: '加西亚·马尔克斯',
|
||||
rating: 9.3,
|
||||
status: 'read',
|
||||
readDate: DateTime(2024, 2, 15),
|
||||
),
|
||||
Book(
|
||||
id: '3',
|
||||
title: '人类简史',
|
||||
author: '尤瓦尔·赫拉利',
|
||||
rating: 9.0,
|
||||
status: 'reading',
|
||||
),
|
||||
Book(
|
||||
id: '4',
|
||||
title: '三体',
|
||||
author: '刘慈欣',
|
||||
rating: 9.5,
|
||||
status: 'want_to_read',
|
||||
),
|
||||
Book(
|
||||
id: '5',
|
||||
title: '追风筝的人',
|
||||
author: '卡勒德·胡赛尼',
|
||||
rating: 8.9,
|
||||
status: 'read',
|
||||
readDate: DateTime(2024, 3, 5),
|
||||
note: '关于救赎与成长的故事',
|
||||
),
|
||||
];
|
||||
|
||||
return allBooks.where((b) => b.status == currentStatus).toList();
|
||||
}
|
||||
}
|
||||
247
lib/pages/home_page.dart
Normal file
247
lib/pages/home_page.dart
Normal file
@@ -0,0 +1,247 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import '../widgets/custom_drawer.dart';
|
||||
import '../widgets/bottom_nav_bar.dart';
|
||||
import 'movie_tab_page.dart';
|
||||
import 'book_tab_page.dart';
|
||||
import 'note_tab_page.dart';
|
||||
|
||||
/// 主页 - 包含顶部菜单、三个标签页、底部导航
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
// 左侧弹出菜单
|
||||
drawer: const CustomDrawer(),
|
||||
|
||||
// 主体内容
|
||||
body: Column(
|
||||
children: [
|
||||
// 顶部 AppBar
|
||||
_buildAppBar(),
|
||||
|
||||
// 三个标签页的标题栏
|
||||
_buildTabBar(),
|
||||
|
||||
// 标签页内容
|
||||
Expanded(
|
||||
child: _buildTabContent(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 底部导航栏
|
||||
bottomNavigationBar: const CustomBottomNavBar(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建顶部 AppBar
|
||||
Widget _buildAppBar() {
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
return AppBar(
|
||||
title: Text(_getAppBarTitle(provider)),
|
||||
actions: [
|
||||
// 添加按钮
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () => _showAddDialog(context, provider),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: () {
|
||||
// TODO: 搜索功能
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取 AppBar 标题
|
||||
String _getAppBarTitle(AppProvider provider) {
|
||||
switch (provider.mainTabIndex) {
|
||||
case 0:
|
||||
return '观影';
|
||||
case 1:
|
||||
return '阅读';
|
||||
case 2:
|
||||
return '笔记';
|
||||
default:
|
||||
return 'MookNote';
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建标签栏
|
||||
Widget _buildTabBar() {
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 观影
|
||||
_buildTabItem(
|
||||
context,
|
||||
'观影',
|
||||
Icons.movie,
|
||||
0,
|
||||
provider.mainTabIndex,
|
||||
() => provider.setMainTabIndex(0),
|
||||
),
|
||||
|
||||
// 阅读
|
||||
_buildTabItem(
|
||||
context,
|
||||
'阅读',
|
||||
Icons.menu_book,
|
||||
1,
|
||||
provider.mainTabIndex,
|
||||
() => provider.setMainTabIndex(1),
|
||||
),
|
||||
|
||||
// 笔记
|
||||
_buildTabItem(
|
||||
context,
|
||||
'笔记',
|
||||
Icons.note,
|
||||
2,
|
||||
provider.mainTabIndex,
|
||||
() => provider.setMainTabIndex(2),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建单个标签项
|
||||
Widget _buildTabItem(
|
||||
BuildContext context,
|
||||
String label,
|
||||
IconData icon,
|
||||
int index,
|
||||
int currentIndex,
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
final isSelected = index == currentIndex;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Expanded(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (isSelected)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
width: 32,
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建标签页内容
|
||||
Widget _buildTabContent() {
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
switch (provider.mainTabIndex) {
|
||||
case 0:
|
||||
return const MovieTabPage();
|
||||
case 1:
|
||||
return const BookTabPage();
|
||||
case 2:
|
||||
return const NoteTabPage();
|
||||
default:
|
||||
return const MovieTabPage();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示添加对话框
|
||||
void _showAddDialog(BuildContext context, AppProvider provider) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Wrap(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.movie, color: Colors.green),
|
||||
title: const Text('添加观影'),
|
||||
subtitle: const Text('记录你看过的电影'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pushNamed(context, '/movie-form');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.menu_book, color: Colors.orange),
|
||||
title: const Text('添加阅读'),
|
||||
subtitle: const Text('记录你读过的书'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pushNamed(context, '/book-form');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.note, color: Colors.blue),
|
||||
title: const Text('添加笔记'),
|
||||
subtitle: const Text('记录你的想法和笔记'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pushNamed(context, '/note-form');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
284
lib/pages/movie_detail_page.dart
Normal file
284
lib/pages/movie_detail_page.dart
Normal file
@@ -0,0 +1,284 @@
|
||||
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 MovieDetailPage extends StatefulWidget {
|
||||
final Movie movie;
|
||||
|
||||
const MovieDetailPage({super.key, required this.movie});
|
||||
|
||||
@override
|
||||
State<MovieDetailPage> createState() => _MovieDetailPageState();
|
||||
}
|
||||
|
||||
class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.movie.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: [
|
||||
// 海报区域
|
||||
_buildPosterSection(context),
|
||||
|
||||
// 基本信息
|
||||
_buildInfoSection(context),
|
||||
|
||||
// 笔记区域
|
||||
if (widget.movie.note != null && widget.movie.note!.isNotEmpty)
|
||||
_buildNoteSection(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建海报区域
|
||||
Widget _buildPosterSection(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 300,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: Icon(
|
||||
Icons.movie,
|
||||
size: 80,
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 16,
|
||||
right: 16,
|
||||
child: _buildStatusTag(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建状态标签
|
||||
Widget _buildStatusTag(BuildContext context) {
|
||||
Color statusColor;
|
||||
String statusText;
|
||||
|
||||
switch (widget.movie.status) {
|
||||
case 'watched':
|
||||
statusColor = AppTheme.watchedColor;
|
||||
statusText = '已看';
|
||||
break;
|
||||
case 'want_to_watch':
|
||||
statusColor = AppTheme.wantToWatchColor;
|
||||
statusText = '想看';
|
||||
break;
|
||||
case 'watching':
|
||||
statusColor = AppTheme.watchingColor;
|
||||
statusText = '在看';
|
||||
break;
|
||||
default:
|
||||
statusColor = Colors.grey;
|
||||
statusText = '未知';
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
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: [
|
||||
// 标题
|
||||
Text(
|
||||
widget.movie.title,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 年份和评分
|
||||
Row(
|
||||
children: [
|
||||
if (widget.movie.year != null) ...[
|
||||
_buildInfoItem(
|
||||
context,
|
||||
icon: Icons.calendar_today,
|
||||
label: '${widget.movie.year}年',
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
if (widget.movie.rating != null) ...[
|
||||
_buildInfoItem(
|
||||
context,
|
||||
icon: Icons.star,
|
||||
label: widget.movie.rating.toString(),
|
||||
iconColor: Colors.amber[700],
|
||||
textColor: Colors.amber[700],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 观看日期
|
||||
if (widget.movie.watchDate != null)
|
||||
_buildInfoItem(
|
||||
context,
|
||||
icon: Icons.event,
|
||||
label: '观看日期:${_formatDate(widget.movie.watchDate!)}',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建信息项
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建笔记区域
|
||||
Widget _buildNoteSection(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.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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
widget.movie.note!,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 格式化日期
|
||||
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: widget.movie).then((_) {
|
||||
// 返回后刷新数据
|
||||
context.read<AppProvider>().loadMovies();
|
||||
});
|
||||
}
|
||||
|
||||
/// 显示删除对话框
|
||||
void _showDeleteDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('确认删除'),
|
||||
content: Text('确定要删除"${widget.movie.title}"吗?此操作不可恢复。'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await context.read<AppProvider>().removeMovie(widget.movie.id);
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context); // 关闭对话框
|
||||
Navigator.pop(context); // 返回上一页
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('已删除'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text(
|
||||
'删除',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
290
lib/pages/movie_form_page.dart
Normal file
290
lib/pages/movie_form_page.dart
Normal file
@@ -0,0 +1,290 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import '../models/data_models.dart';
|
||||
|
||||
/// 添加/编辑影视记录页面
|
||||
class MovieFormPage extends StatefulWidget {
|
||||
final Movie? movie; // 如果为 null,则是添加模式;否则是编辑模式
|
||||
|
||||
const MovieFormPage({super.key, this.movie});
|
||||
|
||||
@override
|
||||
State<MovieFormPage> createState() => _MovieFormPageState();
|
||||
}
|
||||
|
||||
class _MovieFormPageState extends State<MovieFormPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late TextEditingController _titleController;
|
||||
late TextEditingController _yearController;
|
||||
late TextEditingController _ratingController;
|
||||
late TextEditingController _noteController;
|
||||
late String _status;
|
||||
DateTime? _watchDate;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_titleController = TextEditingController(text: widget.movie?.title ?? '');
|
||||
_yearController = TextEditingController(text: widget.movie?.year?.toString() ?? '');
|
||||
_ratingController = TextEditingController(text: widget.movie?.rating?.toString() ?? '');
|
||||
_noteController = TextEditingController(text: widget.movie?.note ?? '');
|
||||
_status = widget.movie?.status ?? 'want_to_watch';
|
||||
_watchDate = widget.movie?.watchDate;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_titleController.dispose();
|
||||
_yearController.dispose();
|
||||
_ratingController.dispose();
|
||||
_noteController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEdit = widget.movie != null;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(isEdit ? '编辑影片' : '添加影片'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.save),
|
||||
onPressed: _saveMovie,
|
||||
),
|
||||
],
|
||||
),
|
||||
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.movie),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '请输入影片名称';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 年份和评分
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _yearController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '年份',
|
||||
hintText: '例如:2024',
|
||||
prefixIcon: Icon(Icons.calendar_today),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
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 之间';
|
||||
}
|
||||
}
|
||||
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: 'watched', child: Text('已看')),
|
||||
DropdownMenuItem(value: 'want_to_watch', child: Text('想看')),
|
||||
DropdownMenuItem(value: 'watching', child: Text('在看')),
|
||||
],
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_status = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 观看日期选择
|
||||
InkWell(
|
||||
onTap: _selectWatchDate,
|
||||
child: InputDecorator(
|
||||
decoration: const InputDecoration(
|
||||
labelText: '观看日期',
|
||||
prefixIcon: Icon(Icons.event),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_watchDate != null
|
||||
? '${_watchDate!.year}-${_watchDate!.month.toString().padLeft(2, '0')}-${_watchDate!.day.toString().padLeft(2, '0')}'
|
||||
: '选择日期',
|
||||
style: TextStyle(
|
||||
color: _watchDate != null
|
||||
? Theme.of(context).colorScheme.onSurface
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (_watchDate != null)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear, size: 20),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_watchDate = 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: _saveMovie,
|
||||
icon: const Icon(Icons.save),
|
||||
label: Text(isEdit ? '保存修改' : '添加记录'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 选择观看日期
|
||||
Future<void> _selectWatchDate() async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _watchDate ?? DateTime.now(),
|
||||
firstDate: DateTime(1900),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_watchDate = picked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存影视记录
|
||||
Future<void> _saveMovie() async {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final year = _yearController.text.isNotEmpty ? int.tryParse(_yearController.text) : null;
|
||||
final rating = _ratingController.text.isNotEmpty ? double.tryParse(_ratingController.text) : null;
|
||||
|
||||
if (widget.movie == null) {
|
||||
// 添加新模式
|
||||
final newMovie = Movie(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
title: _titleController.text.trim(),
|
||||
year: year,
|
||||
rating: rating,
|
||||
status: _status,
|
||||
watchDate: _watchDate,
|
||||
note: _noteController.text.trim(),
|
||||
);
|
||||
|
||||
await context.read<AppProvider>().addMovie(newMovie);
|
||||
} else {
|
||||
// 编辑现有模式
|
||||
final updatedMovie = Movie(
|
||||
id: widget.movie!.id,
|
||||
title: _titleController.text.trim(),
|
||||
year: year,
|
||||
rating: rating,
|
||||
status: _status,
|
||||
watchDate: _watchDate,
|
||||
note: _noteController.text.trim(),
|
||||
);
|
||||
|
||||
await context.read<AppProvider>().updateMovie(updatedMovie);
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(widget.movie == null ? '添加成功' : '更新成功'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
90
lib/pages/movie_tab_page.dart
Normal file
90
lib/pages/movie_tab_page.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import '../models/data_models.dart';
|
||||
import '../widgets/movie_status_bar.dart';
|
||||
import '../widgets/movie_list_item.dart';
|
||||
|
||||
/// 观影标签页
|
||||
class MovieTabPage extends StatelessWidget {
|
||||
const MovieTabPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// 状态选择栏(已看、想看、在看)
|
||||
const MovieStatusBar(),
|
||||
|
||||
// 影片列表
|
||||
Expanded(
|
||||
child: _buildMovieList(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建影片列表
|
||||
Widget _buildMovieList(BuildContext context) {
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
// 根据状态筛选影片
|
||||
final statusMap = {
|
||||
0: 'watched',
|
||||
1: 'want_to_watch',
|
||||
2: 'watching',
|
||||
};
|
||||
final currentStatus = statusMap[provider.movieStatusIndex]!;
|
||||
final movies = provider.getMoviesByStatus(currentStatus);
|
||||
|
||||
if (movies.isEmpty) {
|
||||
return _buildEmptyState(context, provider.movieStatusIndex);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => await provider.loadMovies(),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: movies.length,
|
||||
itemBuilder: (context, index) {
|
||||
return MovieListItem(movie: movies[index]);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建空状态提示
|
||||
Widget _buildEmptyState(BuildContext context, int statusIndex) {
|
||||
final statusText = ['已看', '想看', '在看'][statusIndex];
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.movie_creation_outlined,
|
||||
size: 80,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'暂无$statusText的影片',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('添加记录'),
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(context, '/movie-form');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
199
lib/pages/note_detail_page.dart
Normal file
199
lib/pages/note_detail_page.dart
Normal file
@@ -0,0 +1,199 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import '../models/data_models.dart';
|
||||
|
||||
/// 笔记详情页
|
||||
class NoteDetailPage extends StatefulWidget {
|
||||
final Note note;
|
||||
|
||||
const NoteDetailPage({super.key, required this.note});
|
||||
|
||||
@override
|
||||
State<NoteDetailPage> createState() => _NoteDetailPageState();
|
||||
}
|
||||
|
||||
class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.note.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: [
|
||||
// 标签区域
|
||||
if (widget.note.tags.isNotEmpty) _buildTagsSection(context),
|
||||
|
||||
// 内容区域
|
||||
_buildContentSection(context),
|
||||
|
||||
// 时间信息
|
||||
_buildTimeSection(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建标签区域
|
||||
Widget _buildTagsSection(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: widget.note.tags.map((tag) {
|
||||
return Chip(
|
||||
label: Text(tag),
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
labelStyle: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建内容区域
|
||||
Widget _buildContentSection(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.note.content,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
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,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'创建时间',
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_formatDateTime(widget.note.updatedAt),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 格式化日期时间
|
||||
String _formatDateTime(DateTime dateTime) {
|
||||
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
/// 跳转到编辑页面
|
||||
void _navigateToEdit(BuildContext context) {
|
||||
Navigator.pushNamed(context, '/note-form', arguments: widget.note).then((_) {
|
||||
context.read<AppProvider>().loadNotes();
|
||||
});
|
||||
}
|
||||
|
||||
/// 显示删除对话框
|
||||
void _showDeleteDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('确认删除'),
|
||||
content: Text('确定要删除"${widget.note.title}"吗?此操作不可恢复。'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await context.read<AppProvider>().removeNote(widget.note.id);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('已删除'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text(
|
||||
'删除',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
187
lib/pages/note_form_page.dart
Normal file
187
lib/pages/note_form_page.dart
Normal file
@@ -0,0 +1,187 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import '../models/data_models.dart';
|
||||
|
||||
/// 添加/编辑笔记页面
|
||||
class NoteFormPage extends StatefulWidget {
|
||||
final Note? note; // 如果为 null,则是添加模式;否则是编辑模式
|
||||
|
||||
const NoteFormPage({super.key, this.note});
|
||||
|
||||
@override
|
||||
State<NoteFormPage> createState() => _NoteFormPageState();
|
||||
}
|
||||
|
||||
class _NoteFormPageState extends State<NoteFormPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late TextEditingController _titleController;
|
||||
late TextEditingController _contentController;
|
||||
late TextEditingController _tagsController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_titleController = TextEditingController(text: widget.note?.title ?? '');
|
||||
_contentController = TextEditingController(text: widget.note?.content ?? '');
|
||||
_tagsController = TextEditingController(
|
||||
text: widget.note?.tags.join(', ') ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_titleController.dispose();
|
||||
_contentController.dispose();
|
||||
_tagsController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEdit = widget.note != null;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(isEdit ? '编辑笔记' : '添加笔记'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.save),
|
||||
onPressed: _saveNote,
|
||||
),
|
||||
],
|
||||
),
|
||||
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;
|
||||
},
|
||||
),
|
||||
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 保存笔记
|
||||
Future<void> _saveNote() async {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析标签
|
||||
final tags = _tagsController.text
|
||||
.split(',')
|
||||
.map((tag) => tag.trim())
|
||||
.where((tag) => tag.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
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,
|
||||
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);
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(widget.note == null ? '添加成功' : '更新成功'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
124
lib/pages/note_tab_page.dart
Normal file
124
lib/pages/note_tab_page.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import '../models/data_models.dart';
|
||||
import '../widgets/note_list_item.dart';
|
||||
|
||||
/// 笔记标签页
|
||||
class NoteTabPage extends StatelessWidget {
|
||||
const NoteTabPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// 笔记列表(无状态筛选,显示所有笔记)
|
||||
Expanded(
|
||||
child: _buildNoteList(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建笔记列表
|
||||
Widget _buildNoteList(BuildContext context) {
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
final notes = provider.notes;
|
||||
|
||||
if (notes.isEmpty) {
|
||||
return _buildEmptyState(context);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => await provider.loadNotes(),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: notes.length,
|
||||
itemBuilder: (context, index) {
|
||||
return NoteListItem(note: notes[index]);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建空状态提示
|
||||
Widget _buildEmptyState(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.note_outlined,
|
||||
size: 80,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'暂无笔记',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('添加笔记'),
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(context, '/note-form');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取示例笔记数据
|
||||
List<Note> _getSampleNotes() {
|
||||
// 示例数据(实际应从数据库获取)
|
||||
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),
|
||||
),
|
||||
Note(
|
||||
id: '2',
|
||||
title: '《活着》读后感',
|
||||
content: '余华的《活着》真的是一部让人深思的作品。福贵的一生经历了太多的苦难,但他依然坚强地活着。生命的意义或许就在于活着本身。',
|
||||
tags: ['阅读', '感悟', '书籍'],
|
||||
createdAt: DateTime(2024, 2, 20, 15, 20),
|
||||
updatedAt: DateTime(2024, 2, 20, 16, 0),
|
||||
),
|
||||
Note(
|
||||
id: '3',
|
||||
title: '电影《星际穿越》观后感',
|
||||
content: '诺兰的电影总是充满想象力。《星际穿越》将科幻与亲情完美结合,五维空间的呈现方式令人震撼。配乐也是一绝。',
|
||||
tags: ['观影', '科幻', '电影'],
|
||||
createdAt: DateTime(2024, 2, 15, 20, 0),
|
||||
updatedAt: DateTime(2024, 2, 15, 20, 30),
|
||||
),
|
||||
Note(
|
||||
id: '4',
|
||||
title: 'Python 数据分析笔记',
|
||||
content: 'Pandas 库的 DataFrame 操作非常强大,可以方便地进行数据清洗和分析。需要多练习熟练掌握常用操作。',
|
||||
tags: ['Python', '数据分析', '技术'],
|
||||
createdAt: DateTime(2024, 1, 10, 9, 0),
|
||||
updatedAt: DateTime(2024, 1, 10, 9, 30),
|
||||
),
|
||||
Note(
|
||||
id: '5',
|
||||
title: '生活随笔',
|
||||
content: '春天来了,天气渐暖。周末去公园散步,看到花开得很好。生活中的小确幸值得记录。',
|
||||
tags: ['生活', '随笔'],
|
||||
createdAt: DateTime(2024, 3, 3, 18, 0),
|
||||
updatedAt: DateTime(2024, 3, 3, 18, 0),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
163
lib/providers/app_provider.dart
Normal file
163
lib/providers/app_provider.dart
Normal file
@@ -0,0 +1,163 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/data_models.dart';
|
||||
import '../utils/movie_dao.dart';
|
||||
import '../utils/book_dao.dart';
|
||||
import '../utils/note_dao.dart';
|
||||
|
||||
/// 应用全局状态管理
|
||||
class AppProvider extends ChangeNotifier {
|
||||
// 数据库访问对象
|
||||
final MovieDao _movieDao = MovieDao();
|
||||
final BookDao _bookDao = BookDao();
|
||||
final NoteDao _noteDao = NoteDao();
|
||||
|
||||
// 数据列表
|
||||
List<Movie> _movies = [];
|
||||
List<Book> _books = [];
|
||||
List<Note> _notes = [];
|
||||
|
||||
// 当前主界面选中的标签 (0: 观影,1: 阅读,2: 笔记)
|
||||
int _mainTabIndex = 0;
|
||||
|
||||
// 当前底部导航选中的索引 (0: 主页,1: 新增,2: 我的)
|
||||
int _bottomNavIndex = 0;
|
||||
|
||||
// 观影选中的状态 (0: 已看,1: 想看,2: 在看)
|
||||
int _movieStatusIndex = 0;
|
||||
|
||||
// 阅读选中的状态 (0: 读完,1: 在读,2: 准备读)
|
||||
int _bookStatusIndex = 0;
|
||||
|
||||
// 侧边菜单是否打开
|
||||
bool _drawerOpen = false;
|
||||
|
||||
// 初始化数据库
|
||||
Future<void> initDatabase() async {
|
||||
await loadMovies();
|
||||
await loadBooks();
|
||||
await loadNotes();
|
||||
}
|
||||
|
||||
// 加载影视数据
|
||||
Future<void> loadMovies() async {
|
||||
_movies = await _movieDao.getAllMovies();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 加载书籍数据
|
||||
Future<void> loadBooks() async {
|
||||
_books = await _bookDao.getAllBooks();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 加载笔记数据
|
||||
Future<void> loadNotes() async {
|
||||
_notes = await _noteDao.getAllNotes();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Getters
|
||||
int get mainTabIndex => _mainTabIndex;
|
||||
int get bottomNavIndex => _bottomNavIndex;
|
||||
int get movieStatusIndex => _movieStatusIndex;
|
||||
int get bookStatusIndex => _bookStatusIndex;
|
||||
bool get drawerOpen => _drawerOpen;
|
||||
List<Movie> get movies => _movies;
|
||||
List<Book> get books => _books;
|
||||
List<Note> get notes => _notes;
|
||||
|
||||
// 根据状态获取影视列表
|
||||
List<Movie> getMoviesByStatus(String status) {
|
||||
return _movies.where((movie) => movie.status == status).toList();
|
||||
}
|
||||
|
||||
// 根据状态获取书籍列表
|
||||
List<Book> getBooksByStatus(String status) {
|
||||
return _books.where((book) => book.status == status).toList();
|
||||
}
|
||||
|
||||
// Setters
|
||||
void setMainTabIndex(int index) {
|
||||
_mainTabIndex = index;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setBottomNavIndex(int index) {
|
||||
_bottomNavIndex = index;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setMovieStatusIndex(int index) {
|
||||
_movieStatusIndex = index;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setBookStatusIndex(int index) {
|
||||
_bookStatusIndex = index;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void toggleDrawer() {
|
||||
_drawerOpen = !_drawerOpen;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void closeDrawer() {
|
||||
_drawerOpen = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 添加影视记录
|
||||
Future<void> addMovie(Movie movie) async {
|
||||
await _movieDao.insertMovie(movie);
|
||||
await loadMovies();
|
||||
}
|
||||
|
||||
// 更新影视记录
|
||||
Future<void> updateMovie(Movie movie) async {
|
||||
await _movieDao.updateMovie(movie);
|
||||
await loadMovies();
|
||||
}
|
||||
|
||||
// 删除影视记录
|
||||
Future<void> removeMovie(String id) async {
|
||||
await _movieDao.deleteMovie(id);
|
||||
await loadMovies();
|
||||
}
|
||||
|
||||
// 添加书籍记录
|
||||
Future<void> addBook(Book book) async {
|
||||
await _bookDao.insertBook(book);
|
||||
await loadBooks();
|
||||
}
|
||||
|
||||
// 更新书籍记录
|
||||
Future<void> updateBook(Book book) async {
|
||||
await _bookDao.updateBook(book);
|
||||
await loadBooks();
|
||||
}
|
||||
|
||||
// 删除书籍记录
|
||||
Future<void> removeBook(String id) async {
|
||||
await _bookDao.deleteBook(id);
|
||||
await loadBooks();
|
||||
}
|
||||
|
||||
// 添加笔记
|
||||
Future<void> addNote(Note note) async {
|
||||
await _noteDao.insertNote(note);
|
||||
await loadNotes();
|
||||
}
|
||||
|
||||
// 更新笔记
|
||||
Future<void> updateNote(Note note) async {
|
||||
await _noteDao.updateNote(note);
|
||||
await loadNotes();
|
||||
}
|
||||
|
||||
// 删除笔记
|
||||
Future<void> removeNote(String id) async {
|
||||
await _noteDao.deleteNote(id);
|
||||
await loadNotes();
|
||||
}
|
||||
}
|
||||
60
lib/utils/app_router.dart
Normal file
60
lib/utils/app_router.dart
Normal file
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/data_models.dart';
|
||||
import '../pages/movie_form_page.dart';
|
||||
import '../pages/book_form_page.dart';
|
||||
import '../pages/note_form_page.dart';
|
||||
import '../pages/movie_detail_page.dart';
|
||||
import '../pages/book_detail_page.dart';
|
||||
import '../pages/note_detail_page.dart';
|
||||
|
||||
/// 路由生成器
|
||||
class AppRouter {
|
||||
static Route<dynamic> generateRoute(RouteSettings settings) {
|
||||
switch (settings.name) {
|
||||
case '/movie-form':
|
||||
final movie = settings.arguments as Movie?;
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => MovieFormPage(movie: movie),
|
||||
);
|
||||
|
||||
case '/book-form':
|
||||
final book = settings.arguments as Book?;
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => BookFormPage(book: book),
|
||||
);
|
||||
|
||||
case '/note-form':
|
||||
final note = settings.arguments as Note?;
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => NoteFormPage(note: note),
|
||||
);
|
||||
|
||||
case '/movie-detail':
|
||||
final movie = settings.arguments as Movie;
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => MovieDetailPage(movie: movie),
|
||||
);
|
||||
|
||||
case '/book-detail':
|
||||
final book = settings.arguments as Book;
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => BookDetailPage(book: book),
|
||||
);
|
||||
|
||||
case '/note-detail':
|
||||
final note = settings.arguments as Note;
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => NoteDetailPage(note: note),
|
||||
);
|
||||
|
||||
default:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => Scaffold(
|
||||
body: Center(
|
||||
child: Text('未找到页面:${settings.name}'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
66
lib/utils/app_theme.dart
Normal file
66
lib/utils/app_theme.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppTheme {
|
||||
// 主色调
|
||||
static const Color primaryColor = Color(0xFF6200EE);
|
||||
static const Color secondaryColor = Color(0xFF03DAC6);
|
||||
|
||||
// 状态颜色
|
||||
static const Color watchedColor = Color(0xFF4CAF50); // 已看 - 绿色
|
||||
static const Color wantToWatchColor = Color(0xFFFF9800); // 想看 - 橙色
|
||||
static const Color watchingColor = Color(0xFF2196F3); // 在看 - 蓝色
|
||||
|
||||
static const Color readColor = Color(0xFF4CAF50); // 读完 - 绿色
|
||||
static const Color wantToReadColor = Color(0xFFFF9800); // 准备读 - 橙色
|
||||
static const Color readingColor = Color(0xFF2196F3); // 在读 - 蓝色
|
||||
|
||||
// 亮色主题
|
||||
static final ThemeData lightTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.light,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: primaryColor,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
centerTitle: false,
|
||||
elevation: 0,
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
||||
elevation: 8,
|
||||
selectedItemColor: primaryColor,
|
||||
unselectedItemColor: Colors.grey,
|
||||
),
|
||||
);
|
||||
|
||||
// 暗色主题
|
||||
static final ThemeData darkTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: primaryColor,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
centerTitle: false,
|
||||
elevation: 0,
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
||||
elevation: 8,
|
||||
selectedItemColor: secondaryColor,
|
||||
unselectedItemColor: Colors.grey,
|
||||
),
|
||||
);
|
||||
}
|
||||
81
lib/utils/book_dao.dart
Normal file
81
lib/utils/book_dao.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import '../models/data_models.dart';
|
||||
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');
|
||||
|
||||
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'],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 根据状态筛选书籍记录
|
||||
Future<List<Book>> getBooksByStatus(String status) async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'books',
|
||||
where: 'status = ?',
|
||||
whereArgs: [status],
|
||||
);
|
||||
|
||||
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'],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 添加书籍记录
|
||||
Future<int> insertBook(Book book) async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.insert('books', book.toJson());
|
||||
}
|
||||
|
||||
// 更新书籍记录
|
||||
Future<int> updateBook(Book book) async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'books',
|
||||
book.toJson(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [book.id],
|
||||
);
|
||||
}
|
||||
|
||||
// 删除书籍记录
|
||||
Future<int> deleteBook(String id) async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.delete(
|
||||
'books',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
}
|
||||
81
lib/utils/database_helper.dart
Normal file
81
lib/utils/database_helper.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
/// 数据库帮助类 - 管理数据库的创建和版本控制
|
||||
class DatabaseHelper {
|
||||
static final DatabaseHelper instance = DatabaseHelper._init();
|
||||
static Database? _database;
|
||||
|
||||
DatabaseHelper._init();
|
||||
|
||||
Future<Database> get database async {
|
||||
if (_database != null) return _database!;
|
||||
_database = await _initDB('mooknote.db');
|
||||
return _database!;
|
||||
}
|
||||
|
||||
Future<Database> _initDB(String filePath) async {
|
||||
final dbPath = await getDatabasesPath();
|
||||
final path = join(dbPath, filePath);
|
||||
|
||||
return await openDatabase(
|
||||
path,
|
||||
version: 1,
|
||||
onCreate: _createDB,
|
||||
);
|
||||
}
|
||||
|
||||
// 创建数据库表
|
||||
Future<void> _createDB(Database db, int version) async {
|
||||
const idType = 'INTEGER PRIMARY KEY AUTOINCREMENT';
|
||||
const textType = 'TEXT NOT NULL';
|
||||
const integerType = 'INTEGER NOT NULL';
|
||||
const booleanType = 'INTEGER NOT NULL';
|
||||
|
||||
// 影视表
|
||||
await db.execute('''
|
||||
CREATE TABLE movies (
|
||||
id $idType,
|
||||
title $textType,
|
||||
poster TEXT,
|
||||
rating REAL,
|
||||
year INTEGER,
|
||||
status $textType,
|
||||
watch_date TEXT,
|
||||
note TEXT
|
||||
)
|
||||
''');
|
||||
|
||||
// 书籍表
|
||||
await db.execute('''
|
||||
CREATE TABLE books (
|
||||
id $idType,
|
||||
title $textType,
|
||||
author TEXT,
|
||||
cover TEXT,
|
||||
rating REAL,
|
||||
status $textType,
|
||||
read_date TEXT,
|
||||
note TEXT
|
||||
)
|
||||
''');
|
||||
|
||||
// 笔记表
|
||||
await db.execute('''
|
||||
CREATE TABLE notes (
|
||||
id $idType,
|
||||
title $textType,
|
||||
content $textType,
|
||||
tags TEXT,
|
||||
created_at $textType,
|
||||
updated_at $textType
|
||||
)
|
||||
''');
|
||||
}
|
||||
|
||||
// 关闭数据库
|
||||
Future close() async {
|
||||
final db = await instance.database;
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
81
lib/utils/movie_dao.dart
Normal file
81
lib/utils/movie_dao.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import '../models/data_models.dart';
|
||||
import 'database_helper.dart';
|
||||
|
||||
/// 影视数据访问对象
|
||||
class MovieDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
// 获取所有影视记录
|
||||
Future<List<Movie>> getAllMovies() async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query('movies');
|
||||
|
||||
return List.generate(maps.length, (i) {
|
||||
return Movie(
|
||||
id: maps[i]['id'].toString(),
|
||||
title: maps[i]['title'],
|
||||
poster: maps[i]['poster'],
|
||||
rating: maps[i]['rating']?.toDouble(),
|
||||
year: maps[i]['year'],
|
||||
status: maps[i]['status'],
|
||||
watchDate: maps[i]['watch_date'] != null
|
||||
? DateTime.parse(maps[i]['watch_date'])
|
||||
: null,
|
||||
note: maps[i]['note'],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 根据状态筛选影视记录
|
||||
Future<List<Movie>> getMoviesByStatus(String status) async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'movies',
|
||||
where: 'status = ?',
|
||||
whereArgs: [status],
|
||||
);
|
||||
|
||||
return List.generate(maps.length, (i) {
|
||||
return Movie(
|
||||
id: maps[i]['id'].toString(),
|
||||
title: maps[i]['title'],
|
||||
poster: maps[i]['poster'],
|
||||
rating: maps[i]['rating']?.toDouble(),
|
||||
year: maps[i]['year'],
|
||||
status: maps[i]['status'],
|
||||
watchDate: maps[i]['watch_date'] != null
|
||||
? DateTime.parse(maps[i]['watch_date'])
|
||||
: null,
|
||||
note: maps[i]['note'],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 添加影视记录
|
||||
Future<int> insertMovie(Movie movie) async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.insert('movies', movie.toJson());
|
||||
}
|
||||
|
||||
// 更新影视记录
|
||||
Future<int> updateMovie(Movie movie) async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.update(
|
||||
'movies',
|
||||
movie.toJson(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [movie.id],
|
||||
);
|
||||
}
|
||||
|
||||
// 删除影视记录
|
||||
Future<int> deleteMovie(String id) async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.delete(
|
||||
'movies',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
}
|
||||
69
lib/utils/note_dao.dart
Normal file
69
lib/utils/note_dao.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import '../models/data_models.dart';
|
||||
import 'database_helper.dart';
|
||||
|
||||
/// 笔记数据访问对象
|
||||
class NoteDao {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
|
||||
|
||||
// 获取所有笔记
|
||||
Future<List<Note>> getAllNotes() async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'notes',
|
||||
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']),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 添加笔记
|
||||
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(),
|
||||
});
|
||||
}
|
||||
|
||||
// 更新笔记
|
||||
Future<int> updateNote(Note note) async {
|
||||
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(),
|
||||
},
|
||||
where: 'id = ?',
|
||||
whereArgs: [note.id],
|
||||
);
|
||||
}
|
||||
|
||||
// 删除笔记
|
||||
Future<int> deleteNote(String id) async {
|
||||
final db = await _dbHelper.database;
|
||||
return await db.delete(
|
||||
'notes',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
}
|
||||
237
lib/widgets/book_list_item.dart
Normal file
237
lib/widgets/book_list_item.dart
Normal file
@@ -0,0 +1,237 @@
|
||||
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: [
|
||||
// 标题
|
||||
Text(
|
||||
book.title,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
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) ...[
|
||||
Icon(
|
||||
Icons.star,
|
||||
size: 16,
|
||||
color: Colors.amber[700],
|
||||
),
|
||||
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),
|
||||
],
|
||||
),
|
||||
|
||||
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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建封面占位图
|
||||
Widget _buildCover() {
|
||||
return Container(
|
||||
width: 60,
|
||||
height: 90,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.menu_book,
|
||||
color: Colors.grey[500],
|
||||
size: 32,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建状态标签
|
||||
Widget _buildStatusTag(BuildContext context) {
|
||||
Color statusColor;
|
||||
String statusText;
|
||||
|
||||
switch (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: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: statusColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 格式化日期
|
||||
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) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('确认删除'),
|
||||
content: Text('确定要删除"${book.title}"吗?此操作不可恢复。'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await context.read<AppProvider>().removeBook(book.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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
113
lib/widgets/book_status_bar.dart
Normal file
113
lib/widgets/book_status_bar.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
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});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildStatusItem(
|
||||
context,
|
||||
'读完',
|
||||
AppTheme.readColor,
|
||||
Icons.check_circle,
|
||||
0,
|
||||
provider.bookStatusIndex,
|
||||
() => provider.setBookStatusIndex(0),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_buildStatusItem(
|
||||
context,
|
||||
'在读',
|
||||
AppTheme.readingColor,
|
||||
Icons.auto_stories,
|
||||
1,
|
||||
provider.bookStatusIndex,
|
||||
() => provider.setBookStatusIndex(1),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_buildStatusItem(
|
||||
context,
|
||||
'准备读',
|
||||
AppTheme.wantToReadColor,
|
||||
Icons.bookmark_border,
|
||||
2,
|
||||
provider.bookStatusIndex,
|
||||
() => provider.setBookStatusIndex(2),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建状态项
|
||||
Widget _buildStatusItem(
|
||||
BuildContext context,
|
||||
String label,
|
||||
Color color,
|
||||
IconData icon,
|
||||
int index,
|
||||
int currentIndex,
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
final isSelected = index == currentIndex;
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
95
lib/widgets/bottom_nav_bar.dart
Normal file
95
lib/widgets/bottom_nav_bar.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
|
||||
/// 自定义底部导航栏
|
||||
class CustomBottomNavBar extends StatelessWidget {
|
||||
const CustomBottomNavBar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
return BottomNavigationBar(
|
||||
currentIndex: provider.bottomNavIndex,
|
||||
onTap: (index) {
|
||||
if (index == 1) {
|
||||
// 新增按钮 - 显示选择对话框
|
||||
_showAddDialog(context, provider);
|
||||
} else {
|
||||
provider.setBottomNavIndex(index);
|
||||
if (index == 0) {
|
||||
// 主页 - 重置到首页
|
||||
provider.setMainTabIndex(0);
|
||||
}
|
||||
// index == 2 是我的页面(待实现)
|
||||
}
|
||||
},
|
||||
type: BottomNavigationBarType.fixed,
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.home_outlined),
|
||||
activeIcon: Icon(Icons.home),
|
||||
label: '主页',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.add_circle_outline),
|
||||
activeIcon: Icon(Icons.add_circle),
|
||||
label: '新增',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.person_outline),
|
||||
activeIcon: Icon(Icons.person),
|
||||
label: '我的',
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示新增对话框
|
||||
void _showAddDialog(BuildContext context, AppProvider provider) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Wrap(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.movie, color: Colors.green),
|
||||
title: const Text('添加观影'),
|
||||
subtitle: const Text('记录你看过的电影'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// 直接打开添加观影表单
|
||||
Navigator.pushNamed(context, '/movie-form');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.menu_book, color: Colors.orange),
|
||||
title: const Text('添加阅读'),
|
||||
subtitle: const Text('记录你读过的书'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// 直接打开添加阅读表单
|
||||
Navigator.pushNamed(context, '/book-form');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.note, color: Colors.blue),
|
||||
title: const Text('添加笔记'),
|
||||
subtitle: const Text('记录你的想法和笔记'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// 直接打开添加笔记表单
|
||||
Navigator.pushNamed(context, '/note-form');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
213
lib/widgets/custom_drawer.dart
Normal file
213
lib/widgets/custom_drawer.dart
Normal file
@@ -0,0 +1,213 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
|
||||
/// 自定义左侧弹出菜单
|
||||
class CustomDrawer extends StatelessWidget {
|
||||
const CustomDrawer({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Drawer(
|
||||
child: Column(
|
||||
children: [
|
||||
// 顶部用户信息区域(含热力图)
|
||||
_buildHeader(context),
|
||||
|
||||
// 分割线
|
||||
Divider(height: 1, color: colorScheme.outlineVariant),
|
||||
|
||||
// 菜单项列表
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.analytics),
|
||||
title: const Text('统计'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// TODO: 跳转到统计页面
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_outline),
|
||||
title: const Text('回收站'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// TODO: 跳转到回收站页面
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: const Text('设置'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// TODO: 跳转到设置页面
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 底部版本信息
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
'MookNote v1.0.0',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建头部(含热力图)
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(16, 48, 16, 16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.primaryContainer,
|
||||
Theme.of(context).colorScheme.surface,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 用户头像和名称
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 32,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'用户',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'记录生活点滴',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// GitHub 风格热力图
|
||||
_buildHeatmap(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建热力图
|
||||
Widget _buildHeatmap(BuildContext context) {
|
||||
const int weeks = 52; // 一年 52 周
|
||||
const int daysPerWeek = 7;
|
||||
|
||||
// 生成随机数据(实际应从数据库获取)
|
||||
final random = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'年度记录',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: daysPerWeek * 14, // 每个格子 14x14
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: List.generate(weeks, (weekIndex) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 2),
|
||||
child: Column(
|
||||
children: List.generate(daysPerWeek, (dayIndex) {
|
||||
// 根据随机值决定颜色深度
|
||||
final intensity = (random + weekIndex * 7 + dayIndex) % 100 / 100;
|
||||
final color = _getHeatmapColor(intensity, context);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 2),
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Less',
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
Text(
|
||||
'More',
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 根据强度获取热力图颜色
|
||||
Color _getHeatmapColor(double intensity, BuildContext context) {
|
||||
if (intensity == 0) {
|
||||
return Theme.of(context).colorScheme.surfaceContainerHighest;
|
||||
} else if (intensity < 0.25) {
|
||||
return Theme.of(context).colorScheme.primaryContainer.withOpacity(0.4);
|
||||
} else if (intensity < 0.5) {
|
||||
return Theme.of(context).colorScheme.primaryContainer.withOpacity(0.6);
|
||||
} else if (intensity < 0.75) {
|
||||
return Theme.of(context).colorScheme.primaryContainer.withOpacity(0.8);
|
||||
} else {
|
||||
return Theme.of(context).colorScheme.primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
237
lib/widgets/movie_list_item.dart
Normal file
237
lib/widgets/movie_list_item.dart
Normal file
@@ -0,0 +1,237 @@
|
||||
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 MovieListItem extends StatelessWidget {
|
||||
final Movie movie;
|
||||
|
||||
const MovieListItem({super.key, required this.movie});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
// 跳转到详情页
|
||||
Navigator.pushNamed(context, '/movie-detail', arguments: movie);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 海报占位图
|
||||
_buildPoster(),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 影片信息
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题和年份
|
||||
Text(
|
||||
movie.title,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
if (movie.year != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${movie.year}年',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 评分和状态
|
||||
Row(
|
||||
children: [
|
||||
if (movie.rating != null) ...[
|
||||
Icon(
|
||||
Icons.star,
|
||||
size: 16,
|
||||
color: Colors.amber[700],
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
movie.rating.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.amber[700],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
|
||||
// 状态标签
|
||||
_buildStatusTag(context),
|
||||
],
|
||||
),
|
||||
|
||||
if (movie.watchDate != null) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'观看日期:${_formatDate(movie.watchDate!)}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
if (movie.note != null && movie.note!.isNotEmpty) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
movie.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, '/movie-form', arguments: movie);
|
||||
},
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outline, size: 20),
|
||||
color: Colors.red,
|
||||
onPressed: () => _showDeleteDialog(context, movie),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建海报占位图
|
||||
Widget _buildPoster() {
|
||||
return Container(
|
||||
width: 60,
|
||||
height: 90,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.movie,
|
||||
color: Colors.grey[500],
|
||||
size: 32,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建状态标签
|
||||
Widget _buildStatusTag(BuildContext context) {
|
||||
Color statusColor;
|
||||
String statusText;
|
||||
|
||||
switch (movie.status) {
|
||||
case 'watched':
|
||||
statusColor = AppTheme.watchedColor;
|
||||
statusText = '已看';
|
||||
break;
|
||||
case 'want_to_watch':
|
||||
statusColor = AppTheme.wantToWatchColor;
|
||||
statusText = '想看';
|
||||
break;
|
||||
case 'watching':
|
||||
statusColor = AppTheme.watchingColor;
|
||||
statusText = '在看';
|
||||
break;
|
||||
default:
|
||||
statusColor = Colors.grey;
|
||||
statusText = '未知';
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 格式化日期
|
||||
String _formatDate(DateTime date) {
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
/// 显示删除对话框
|
||||
void _showDeleteDialog(BuildContext context, Movie movie) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('确认删除'),
|
||||
content: Text('确定要删除"${movie.title}"吗?此操作不可恢复。'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
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('已删除'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text(
|
||||
'删除',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
113
lib/widgets/movie_status_bar.dart
Normal file
113
lib/widgets/movie_status_bar.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import '../utils/app_theme.dart';
|
||||
|
||||
/// 观影状态选择栏
|
||||
class MovieStatusBar extends StatelessWidget {
|
||||
const MovieStatusBar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildStatusItem(
|
||||
context,
|
||||
'已看',
|
||||
AppTheme.watchedColor,
|
||||
Icons.check_circle,
|
||||
0,
|
||||
provider.movieStatusIndex,
|
||||
() => provider.setMovieStatusIndex(0),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_buildStatusItem(
|
||||
context,
|
||||
'想看',
|
||||
AppTheme.wantToWatchColor,
|
||||
Icons.bookmark_border,
|
||||
1,
|
||||
provider.movieStatusIndex,
|
||||
() => provider.setMovieStatusIndex(1),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_buildStatusItem(
|
||||
context,
|
||||
'在看',
|
||||
AppTheme.watchingColor,
|
||||
Icons.play_circle_outline,
|
||||
2,
|
||||
provider.movieStatusIndex,
|
||||
() => provider.setMovieStatusIndex(2),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建状态项
|
||||
Widget _buildStatusItem(
|
||||
BuildContext context,
|
||||
String label,
|
||||
Color color,
|
||||
IconData icon,
|
||||
int index,
|
||||
int currentIndex,
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
final isSelected = index == currentIndex;
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
170
lib/widgets/note_list_item.dart
Normal file
170
lib/widgets/note_list_item.dart
Normal file
@@ -0,0 +1,170 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import '../models/data_models.dart';
|
||||
|
||||
/// 笔记列表项组件
|
||||
class NoteListItem extends StatelessWidget {
|
||||
final Note note;
|
||||
|
||||
const NoteListItem({super.key, required this.note});
|
||||
|
||||
@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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建标签
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 格式化日期
|
||||
String _formatDate(DateTime date) {
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(date);
|
||||
|
||||
if (difference.inDays == 0) {
|
||||
if (difference.inHours == 0) {
|
||||
if (difference.inMinutes == 0) {
|
||||
return '刚刚';
|
||||
}
|
||||
return '${difference.inMinutes}分钟前';
|
||||
}
|
||||
return '${difference.inHours}小时前';
|
||||
} else if (difference.inDays < 7) {
|
||||
return '${difference.inDays}天前';
|
||||
} else {
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user