generated from dellevin/template
基础功能
This commit is contained in:
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),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user