generated from dellevin/template
功能优化
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
@@ -19,7 +18,6 @@ class _BackupPageState extends State<BackupPage> {
|
||||
bool _isImporting = false;
|
||||
bool _autoBackupEnabled = false;
|
||||
bool _isLoading = true;
|
||||
List<File> _autoBackupFiles = [];
|
||||
String? _backupDirPath;
|
||||
|
||||
@override
|
||||
@@ -30,12 +28,10 @@ class _BackupPageState extends State<BackupPage> {
|
||||
|
||||
Future<void> _loadAutoBackupStatus() async {
|
||||
final enabled = await AutoBackupService.instance.getEnabled();
|
||||
final files = await AutoBackupService.instance.getBackupFiles();
|
||||
final dirPath = await AutoBackupService.instance.getBackupDirectoryPath();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_autoBackupEnabled = enabled;
|
||||
_autoBackupFiles = files;
|
||||
_backupDirPath = dirPath;
|
||||
_isLoading = false;
|
||||
});
|
||||
@@ -59,12 +55,6 @@ class _BackupPageState extends State<BackupPage> {
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 自动备份文件列表
|
||||
if (_autoBackupFiles.isNotEmpty) ...[
|
||||
_buildBackupFilesSection(),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
|
||||
// 导出数据
|
||||
_buildSection(
|
||||
title: '导出数据',
|
||||
@@ -406,119 +396,4 @@ class _BackupPageState extends State<BackupPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建备份文件列表区域
|
||||
Widget _buildBackupFilesSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.folder_outlined,
|
||||
size: 24,
|
||||
color: Color(0xFF1A1A1A),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'自动备份文件 (${_autoBackupFiles.length}/10)',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF1A1A1A),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
..._autoBackupFiles.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final file = entry.value;
|
||||
final fileName = file.path.split('/').last;
|
||||
final stat = file.statSync();
|
||||
final size = _formatFileSize(stat.size);
|
||||
final modified = _formatDateTime(stat.modified);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: index < _autoBackupFiles.length - 1
|
||||
? const BorderSide(color: Color(0xFFE5E5E5))
|
||||
: BorderSide.none,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.backup,
|
||||
size: 18,
|
||||
color: Color(0xFF666666),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
fileName,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1A1A1A),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'$modified · $size',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF999999),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (index == 0)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
||||
),
|
||||
child: const Text(
|
||||
'最新',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xFF666666),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 格式化文件大小
|
||||
String _formatFileSize(int bytes) {
|
||||
if (bytes < 1024) return '$bytes B';
|
||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
}
|
||||
|
||||
/// 格式化日期时间
|
||||
String _formatDateTime(DateTime dateTime) {
|
||||
return '${dateTime.month}/${dateTime.day} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class _HomePageState extends State<HomePage> {
|
||||
return Scaffold(
|
||||
// 左侧弹出菜单(仅在主页显示)
|
||||
drawer: context.watch<AppProvider>().bottomNavIndex == 0
|
||||
? const CustomDrawer()
|
||||
? CustomDrawer()
|
||||
: null,
|
||||
|
||||
// 主体内容 - 根据底部导航切换
|
||||
|
||||
@@ -5,14 +5,86 @@ import '../models/data_models.dart';
|
||||
import '../widgets/note_list_item.dart';
|
||||
|
||||
/// 笔记标签页
|
||||
class NoteTabPage extends StatelessWidget {
|
||||
class NoteTabPage extends StatefulWidget {
|
||||
const NoteTabPage({super.key});
|
||||
|
||||
@override
|
||||
State<NoteTabPage> createState() => _NoteTabPageState();
|
||||
}
|
||||
|
||||
class _NoteTabPageState extends State<NoteTabPage> {
|
||||
// 使用分页加载
|
||||
static const int _pageSize = 50;
|
||||
final List<Note> _displayedNotes = [];
|
||||
bool _isLoading = false;
|
||||
bool _hasMore = true;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_onScroll);
|
||||
// 延迟加载初始数据,避免阻塞UI
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_loadMoreNotes();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.pixels >=
|
||||
_scrollController.position.maxScrollExtent - 200) {
|
||||
_loadMoreNotes();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadMoreNotes() async {
|
||||
if (_isLoading || !_hasMore) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
// 使用微任务延迟加载,避免阻塞UI
|
||||
await Future.microtask(() {
|
||||
final provider = context.read<AppProvider>();
|
||||
final allNotes = provider.notes;
|
||||
|
||||
final startIndex = _displayedNotes.length;
|
||||
final endIndex = (startIndex + _pageSize).clamp(0, allNotes.length);
|
||||
|
||||
if (startIndex >= allNotes.length) {
|
||||
_hasMore = false;
|
||||
} else {
|
||||
final newNotes = allNotes.sublist(startIndex, endIndex);
|
||||
_displayedNotes.addAll(newNotes);
|
||||
_hasMore = endIndex < allNotes.length;
|
||||
}
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
final provider = context.read<AppProvider>();
|
||||
await provider.loadNotes();
|
||||
setState(() {
|
||||
_displayedNotes.clear();
|
||||
_hasMore = true;
|
||||
});
|
||||
await _loadMoreNotes();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// 笔记列表(无状态筛选,显示所有笔记)
|
||||
// 笔记列表(分页加载)
|
||||
Expanded(
|
||||
child: _buildNoteList(context),
|
||||
),
|
||||
@@ -24,21 +96,38 @@ class NoteTabPage extends StatelessWidget {
|
||||
Widget _buildNoteList(BuildContext context) {
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
final notes = provider.notes;
|
||||
final allNotes = provider.notes;
|
||||
|
||||
if (notes.isEmpty) {
|
||||
if (allNotes.isEmpty && _displayedNotes.isEmpty) {
|
||||
return _buildEmptyState(context);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => await provider.loadNotes(),
|
||||
onRefresh: _refresh,
|
||||
color: const Color(0xFF1A1A1A),
|
||||
backgroundColor: Colors.white,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: notes.length,
|
||||
itemCount: _displayedNotes.length + (_hasMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
return NoteListItem(note: notes[index]);
|
||||
if (index >= _displayedNotes.length) {
|
||||
// 底部加载指示器
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Color(0xFF1A1A1A),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return NoteListItem(note: _displayedNotes[index]);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
int _selectedType = 0; // 0: 影视, 1: 书籍, 2: 笔记
|
||||
List<dynamic> _results = [];
|
||||
bool _isSearching = false;
|
||||
String? _selectedTag; // 选中的标签
|
||||
|
||||
final List<String> _typeLabels = ['影视', '书籍', '笔记'];
|
||||
|
||||
@@ -32,7 +33,8 @@ class _SearchPageState extends State<SearchPage> {
|
||||
|
||||
Future<void> _performSearch() async {
|
||||
final keyword = _searchController.text.trim();
|
||||
if (keyword.isEmpty) return;
|
||||
// 笔记搜索允许空关键词(用于标签筛选)
|
||||
if (keyword.isEmpty && _selectedType != 2 && _selectedTag == null) return;
|
||||
|
||||
setState(() => _isSearching = true);
|
||||
|
||||
@@ -85,9 +87,88 @@ class _SearchPageState extends State<SearchPage> {
|
||||
}
|
||||
|
||||
bool _matchNote(Note note, String keyword) {
|
||||
// 如果有选中的标签,先按标签筛选
|
||||
if (_selectedTag != null) {
|
||||
if (!note.tags.contains(_selectedTag)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 关键词为空时,只按标签筛选
|
||||
if (keyword.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
// 按内容搜索
|
||||
return note.content.toLowerCase().contains(keyword.toLowerCase());
|
||||
}
|
||||
|
||||
/// 构建标签筛选区域
|
||||
Widget _buildTagFilter() {
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
// 获取所有笔记的标签
|
||||
final allTags = <String>{};
|
||||
for (final note in provider.notes.where((n) => !n.isDeleted)) {
|
||||
allTags.addAll(note.tags);
|
||||
}
|
||||
|
||||
if (allTags.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final tags = allTags.toList()..sort();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
alignment: WrapAlignment.start,
|
||||
children: tags.map((tag) => GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
// 再次点击同一标签则取消筛选
|
||||
if (_selectedTag == tag) {
|
||||
_selectedTag = null;
|
||||
} else {
|
||||
_selectedTag = tag;
|
||||
}
|
||||
_performSearch();
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: _selectedTag == tag
|
||||
? const Color(0xFF1A1A1A)
|
||||
: const Color(0xFFF5F5F5),
|
||||
border: Border.all(
|
||||
color: _selectedTag == tag
|
||||
? const Color(0xFF1A1A1A)
|
||||
: const Color(0xFFE5E5E5),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
tag,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _selectedTag == tag
|
||||
? Colors.white
|
||||
: const Color(0xFF666666),
|
||||
),
|
||||
),
|
||||
),
|
||||
)).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -113,10 +194,9 @@ class _SearchPageState extends State<SearchPage> {
|
||||
setState(() {
|
||||
_selectedType = index;
|
||||
_results = [];
|
||||
_selectedTag = null; // 切换类型时重置标签
|
||||
});
|
||||
if (_searchController.text.isNotEmpty) {
|
||||
_performSearch();
|
||||
}
|
||||
_searchController.clear();
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
@@ -173,6 +253,9 @@ class _SearchPageState extends State<SearchPage> {
|
||||
),
|
||||
),
|
||||
|
||||
// 笔记标签筛选(仅在笔记搜索时显示)
|
||||
if (_selectedType == 2) _buildTagFilter(),
|
||||
|
||||
// 搜索结果
|
||||
Expanded(
|
||||
child: _isSearching
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import '../models/data_models.dart';
|
||||
|
||||
@@ -13,58 +11,23 @@ class StatisticsPage extends StatefulWidget {
|
||||
State<StatisticsPage> createState() => _StatisticsPageState();
|
||||
}
|
||||
|
||||
class _StatisticsPageState extends State<StatisticsPage> with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
DateTime _selectedMonth = DateTime.now();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
class _StatisticsPageState extends State<StatisticsPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: const Text('数据统计'),
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: const [
|
||||
Tab(text: '概览'),
|
||||
Tab(text: '日历'),
|
||||
Tab(text: '趋势'),
|
||||
],
|
||||
labelColor: const Color(0xFF1A1A1A),
|
||||
unselectedLabelColor: const Color(0xFF999999),
|
||||
indicatorColor: const Color(0xFF1A1A1A),
|
||||
),
|
||||
),
|
||||
body: Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
final movies = provider.movies;
|
||||
final books = provider.books;
|
||||
final notes = provider.notes;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: const Text('数据统计'),
|
||||
),
|
||||
body: Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
final movies = provider.movies;
|
||||
final books = provider.books;
|
||||
final notes = provider.notes;
|
||||
|
||||
return TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildOverviewTab(movies, books, notes),
|
||||
_buildCalendarTab(movies, books, notes),
|
||||
_buildTrendTab(movies, books, notes),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
return _buildOverviewTab(movies, books, notes);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -128,165 +91,6 @@ class _StatisticsPageState extends State<StatisticsPage> with SingleTickerProvid
|
||||
);
|
||||
}
|
||||
|
||||
/// 日历标签页
|
||||
Widget _buildCalendarTab(List<Movie> movies, List<Book> books, List<Note> notes) {
|
||||
// 合并所有数据按日期
|
||||
final Map<DateTime, _DailyData> dailyData = {};
|
||||
|
||||
for (final movie in movies) {
|
||||
final date = DateTime(movie.createdAt.year, movie.createdAt.month, movie.createdAt.day);
|
||||
dailyData.putIfAbsent(date, () => _DailyData()).movies++;
|
||||
}
|
||||
|
||||
for (final book in books) {
|
||||
final date = DateTime(book.createdAt.year, book.createdAt.month, book.createdAt.day);
|
||||
dailyData.putIfAbsent(date, () => _DailyData()).books++;
|
||||
}
|
||||
|
||||
for (final note in notes) {
|
||||
final date = DateTime(note.createdAt.year, note.createdAt.month, note.createdAt.day);
|
||||
dailyData.putIfAbsent(date, () => _DailyData()).notes++;
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// 月份选择器
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_selectedMonth = DateTime(_selectedMonth.year, _selectedMonth.month - 1);
|
||||
});
|
||||
},
|
||||
),
|
||||
Text(
|
||||
'${_selectedMonth.year}年${_selectedMonth.month}月',
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_selectedMonth = DateTime(_selectedMonth.year, _selectedMonth.month + 1);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 热力图图例
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const Text('少', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
const SizedBox(width: 8),
|
||||
...List.generate(4, (index) {
|
||||
final opacity = 0.2 + (index * 0.2);
|
||||
return Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A).withOpacity(opacity),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(width: 8),
|
||||
const Text('多', style: TextStyle(fontSize: 12, color: Color(0xFF999999))),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 日历热力图
|
||||
Expanded(
|
||||
child: _buildHeatMapCalendar(dailyData),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 趋势标签页
|
||||
Widget _buildTrendTab(List<Movie> movies, List<Book> books, List<Note> notes) {
|
||||
// 近30天每日新增数据
|
||||
final now = DateTime.now();
|
||||
final thirtyDaysAgo = now.subtract(const Duration(days: 29));
|
||||
|
||||
final List<_DailyTrend> movieTrend = [];
|
||||
final List<_DailyTrend> bookTrend = [];
|
||||
final List<_DailyTrend> noteTrend = [];
|
||||
|
||||
for (int i = 0; i < 30; i++) {
|
||||
final date = DateTime(thirtyDaysAgo.year, thirtyDaysAgo.month, thirtyDaysAgo.day + i);
|
||||
|
||||
final movieCount = movies.where((m) {
|
||||
final mDate = DateTime(m.createdAt.year, m.createdAt.month, m.createdAt.day);
|
||||
return mDate.isAtSameMomentAs(date);
|
||||
}).length;
|
||||
|
||||
final bookCount = books.where((b) {
|
||||
final bDate = DateTime(b.createdAt.year, b.createdAt.month, b.createdAt.day);
|
||||
return bDate.isAtSameMomentAs(date);
|
||||
}).length;
|
||||
|
||||
final noteCount = notes.where((n) {
|
||||
final nDate = DateTime(n.createdAt.year, n.createdAt.month, n.createdAt.day);
|
||||
return nDate.isAtSameMomentAs(date);
|
||||
}).length;
|
||||
|
||||
movieTrend.add(_DailyTrend(date, movieCount));
|
||||
bookTrend.add(_DailyTrend(date, bookCount));
|
||||
noteTrend.add(_DailyTrend(date, noteCount));
|
||||
}
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
children: [
|
||||
_buildSectionTitle('近30天新增趋势'),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'每日新增数据统计',
|
||||
style: TextStyle(fontSize: 13, color: Color(0xFF999999)),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 折线图
|
||||
SizedBox(
|
||||
height: 250,
|
||||
child: _buildLineChart(movieTrend, bookTrend, noteTrend),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 图例
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildLegendItem('影视', const Color(0xFFE53935)),
|
||||
const SizedBox(width: 24),
|
||||
_buildLegendItem('书籍', const Color(0xFF1E88E5)),
|
||||
const SizedBox(width: 24),
|
||||
_buildLegendItem('笔记', const Color(0xFF43A047)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 近7天统计
|
||||
_buildSectionTitle('近7天新增统计'),
|
||||
const SizedBox(height: 16),
|
||||
_buildWeeklyStats(movieTrend.sublist(23), bookTrend.sublist(23), noteTrend.sublist(23)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建统计卡片
|
||||
Widget _buildStatCard(String title, int count, IconData icon, Color color) {
|
||||
return Container(
|
||||
@@ -388,316 +192,6 @@ class _StatisticsPageState extends State<StatisticsPage> with SingleTickerProvid
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建热力图日历
|
||||
Widget _buildHeatMapCalendar(Map<DateTime, _DailyData> dailyData) {
|
||||
final year = _selectedMonth.year;
|
||||
final month = _selectedMonth.month;
|
||||
final daysInMonth = DateTime(year, month + 1, 0).day;
|
||||
final firstWeekday = DateTime(year, month, 1).weekday % 7;
|
||||
|
||||
// 计算最大数量用于颜色强度
|
||||
int maxCount = 0;
|
||||
for (final data in dailyData.values) {
|
||||
final count = data.total;
|
||||
if (count > maxCount) maxCount = count;
|
||||
}
|
||||
if (maxCount == 0) maxCount = 1;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
children: [
|
||||
// 星期标题
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: const ['日', '一', '二', '三', '四', '五', '六']
|
||||
.map((d) => SizedBox(
|
||||
width: 36,
|
||||
child: Text(
|
||||
d,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFF999999)),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 日历网格
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
// 空白填充
|
||||
...List.generate(firstWeekday, (_) => const SizedBox(width: 36, height: 36)),
|
||||
|
||||
// 日期
|
||||
...List.generate(daysInMonth, (index) {
|
||||
final day = index + 1;
|
||||
final date = DateTime(year, month, day);
|
||||
final data = dailyData[date];
|
||||
final count = data?.total ?? 0;
|
||||
|
||||
// 计算颜色强度
|
||||
double opacity = 0.1;
|
||||
if (count > 0) {
|
||||
opacity = 0.3 + (count / maxCount * 0.7);
|
||||
opacity = opacity.clamp(0.3, 1.0);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (count > 0) {
|
||||
_showDayDetail(context, date, data!);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: count > 0
|
||||
? const Color(0xFF1A1A1A).withOpacity(opacity)
|
||||
: const Color(0xFFF5F5F5),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
day.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: count > 0 ? Colors.white : const Color(0xFF666666),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示某天详情
|
||||
void _showDayDetail(BuildContext context, DateTime date, _DailyData data) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.white,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${date.year}年${date.month}月${date.day}日',
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (data.movies > 0)
|
||||
_buildDetailRow(Icons.movie_outlined, '影视', data.movies),
|
||||
if (data.books > 0)
|
||||
_buildDetailRow(Icons.menu_book_outlined, '书籍', data.books),
|
||||
if (data.notes > 0)
|
||||
_buildDetailRow(Icons.note_outlined, '笔记', data.notes),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'总计: ${data.total} 项',
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF666666)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(IconData icon, String label, int count) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: const Color(0xFF666666)),
|
||||
const SizedBox(width: 8),
|
||||
Text(label, style: const TextStyle(fontSize: 14)),
|
||||
const Spacer(),
|
||||
Text('$count 个', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建折线图
|
||||
Widget _buildLineChart(List<_DailyTrend> movieTrend, List<_DailyTrend> bookTrend, List<_DailyTrend> noteTrend) {
|
||||
final spotsMovie = movieTrend.asMap().entries.map((e) {
|
||||
return FlSpot(e.key.toDouble(), e.value.count.toDouble());
|
||||
}).toList();
|
||||
|
||||
final spotsBook = bookTrend.asMap().entries.map((e) {
|
||||
return FlSpot(e.key.toDouble(), e.value.count.toDouble());
|
||||
}).toList();
|
||||
|
||||
final spotsNote = noteTrend.asMap().entries.map((e) {
|
||||
return FlSpot(e.key.toDouble(), e.value.count.toDouble());
|
||||
}).toList();
|
||||
|
||||
return LineChart(
|
||||
LineChartData(
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: 1,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return FlLine(
|
||||
color: const Color(0xFFE5E5E5),
|
||||
strokeWidth: 0.5,
|
||||
);
|
||||
},
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 30,
|
||||
getTitlesWidget: (value, meta) {
|
||||
return Text(
|
||||
value.toInt().toString(),
|
||||
style: const TextStyle(fontSize: 10, color: Color(0xFF999999)),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 30,
|
||||
interval: 5,
|
||||
getTitlesWidget: (value, meta) {
|
||||
if (value.toInt() >= 0 && value.toInt() < movieTrend.length) {
|
||||
final date = movieTrend[value.toInt()].date;
|
||||
return Text(
|
||||
'${date.month}/${date.day}',
|
||||
style: const TextStyle(fontSize: 10, color: Color(0xFF999999)),
|
||||
);
|
||||
}
|
||||
return const Text('');
|
||||
},
|
||||
),
|
||||
),
|
||||
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
lineBarsData: [
|
||||
// 影视折线 - 红色
|
||||
LineChartBarData(
|
||||
spots: spotsMovie,
|
||||
isCurved: true,
|
||||
color: const Color(0xFFE53935),
|
||||
barWidth: 2.5,
|
||||
isStrokeCapRound: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: const Color(0xFFE53935).withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
// 书籍折线 - 蓝色
|
||||
LineChartBarData(
|
||||
spots: spotsBook,
|
||||
isCurved: true,
|
||||
color: const Color(0xFF1E88E5),
|
||||
barWidth: 2.5,
|
||||
isStrokeCapRound: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: const Color(0xFF1E88E5).withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
// 笔记折线 - 绿色
|
||||
LineChartBarData(
|
||||
spots: spotsNote,
|
||||
isCurved: true,
|
||||
color: const Color(0xFF43A047),
|
||||
barWidth: 2.5,
|
||||
isStrokeCapRound: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: const Color(0xFF43A047).withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建图例项
|
||||
Widget _buildLegendItem(String label, Color color) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 3,
|
||||
decoration: BoxDecoration(color: color),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF666666))),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建近7天统计
|
||||
Widget _buildWeeklyStats(List<_DailyTrend> movieTrend, List<_DailyTrend> bookTrend, List<_DailyTrend> noteTrend) {
|
||||
final totalMovies = movieTrend.fold(0, (sum, item) => sum + item.count);
|
||||
final totalBooks = bookTrend.fold(0, (sum, item) => sum + item.count);
|
||||
final totalNotes = noteTrend.fold(0, (sum, item) => sum + item.count);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildWeeklyRow('影视新增', totalMovies, const Color(0xFFE53935)),
|
||||
const Divider(height: 24),
|
||||
_buildWeeklyRow('书籍新增', totalBooks, const Color(0xFF1E88E5)),
|
||||
const Divider(height: 24),
|
||||
_buildWeeklyRow('笔记新增', totalNotes, const Color(0xFF43A047)),
|
||||
const Divider(height: 24),
|
||||
_buildWeeklyRow('总计', totalMovies + totalBooks + totalNotes, const Color(0xFF1A1A1A), isTotal: true),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWeeklyRow(String label, int count, Color color, {bool isTotal = false}) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: isTotal ? 14 : 13,
|
||||
fontWeight: isTotal ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'$count 个',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionTitle(String title) {
|
||||
return Text(
|
||||
title,
|
||||
@@ -719,19 +213,4 @@ class _StatusData {
|
||||
_StatusData(this.label, this.count, this.color);
|
||||
}
|
||||
|
||||
/// 每日数据
|
||||
class _DailyData {
|
||||
int movies = 0;
|
||||
int books = 0;
|
||||
int notes = 0;
|
||||
|
||||
int get total => movies + books + notes;
|
||||
}
|
||||
|
||||
/// 每日趋势
|
||||
class _DailyTrend {
|
||||
final DateTime date;
|
||||
final int count;
|
||||
|
||||
_DailyTrend(this.date, this.count);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import '../models/data_models.dart';
|
||||
|
||||
/// 自定义左侧弹出菜单 - 极简主义设计
|
||||
class CustomDrawer extends StatelessWidget {
|
||||
const CustomDrawer({super.key});
|
||||
CustomDrawer({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -27,6 +27,11 @@ class CustomDrawer extends StatelessWidget {
|
||||
|
||||
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
|
||||
|
||||
// 日历热力图区域
|
||||
_buildCalendarSection(context),
|
||||
|
||||
const Divider(height: 0.5, thickness: 0.5, color: Color(0xFFE5E5E5)),
|
||||
|
||||
// 菜单项列表
|
||||
Expanded(
|
||||
child: ListView(
|
||||
@@ -411,6 +416,197 @@ class CustomDrawer extends StatelessWidget {
|
||||
void _showToast(BuildContext context, String message) {
|
||||
ToastUtil.show(context, message);
|
||||
}
|
||||
|
||||
/// 构建日历热力图区域
|
||||
Widget _buildCalendarSection(BuildContext context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, provider, child) {
|
||||
// 使用 StatefulBuilder 的状态来管理选中的月份
|
||||
final selectedMonth = _calendarSelectedMonth ?? DateTime.now();
|
||||
|
||||
// 合并所有数据按日期
|
||||
final Map<DateTime, _DailyData> dailyData = {};
|
||||
|
||||
for (final movie in provider.movies.where((m) => !m.isDeleted)) {
|
||||
final date = DateTime(movie.createdAt.year, movie.createdAt.month, movie.createdAt.day);
|
||||
dailyData.putIfAbsent(date, () => _DailyData()).movies++;
|
||||
}
|
||||
|
||||
for (final book in provider.books.where((b) => !b.isDeleted)) {
|
||||
final date = DateTime(book.createdAt.year, book.createdAt.month, book.createdAt.day);
|
||||
dailyData.putIfAbsent(date, () => _DailyData()).books++;
|
||||
}
|
||||
|
||||
for (final note in provider.notes.where((n) => !n.isDeleted)) {
|
||||
final date = DateTime(note.createdAt.year, note.createdAt.month, note.createdAt.day);
|
||||
dailyData.putIfAbsent(date, () => _DailyData()).notes++;
|
||||
}
|
||||
|
||||
// 计算最大数量用于颜色强度
|
||||
int maxCount = 0;
|
||||
for (final data in dailyData.values) {
|
||||
final count = data.total;
|
||||
if (count > maxCount) maxCount = count;
|
||||
}
|
||||
if (maxCount == 0) maxCount = 1;
|
||||
|
||||
final year = selectedMonth.year;
|
||||
final month = selectedMonth.month;
|
||||
final daysInMonth = DateTime(year, month + 1, 0).day;
|
||||
final firstWeekday = DateTime(year, month, 1).weekday % 7;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题和月份切换
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.calendar_today,
|
||||
size: 16,
|
||||
color: Color(0xFF666666),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 上个月按钮
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_calendarSelectedMonth = DateTime(year, month - 1);
|
||||
});
|
||||
},
|
||||
child: const Icon(
|
||||
Icons.chevron_left,
|
||||
size: 20,
|
||||
color: Color(0xFF666666),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'$year年$month月',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF666666),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 下个月按钮
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_calendarSelectedMonth = DateTime(year, month + 1);
|
||||
});
|
||||
},
|
||||
child: const Icon(
|
||||
Icons.chevron_right,
|
||||
size: 20,
|
||||
color: Color(0xFF666666),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 星期标题 - 使用与日期相同的Wrap布局
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: const ['日', '一', '二', '三', '四', '五', '六']
|
||||
.map((d) => SizedBox(
|
||||
width: 32,
|
||||
height: 20,
|
||||
child: Text(
|
||||
d,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 10, color: Color(0xFF999999)),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// 日历网格
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
// 空白填充
|
||||
...List.generate(firstWeekday, (_) => const SizedBox(width: 32, height: 28)),
|
||||
|
||||
// 日期
|
||||
...List.generate(daysInMonth, (index) {
|
||||
final day = index + 1;
|
||||
final date = DateTime(year, month, day);
|
||||
final data = dailyData[date];
|
||||
final count = data?.total ?? 0;
|
||||
|
||||
// 计算颜色强度
|
||||
double opacity = 0.1;
|
||||
if (count > 0) {
|
||||
opacity = 0.3 + (count / maxCount * 0.7);
|
||||
opacity = opacity.clamp(0.3, 1.0);
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: 32,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: count > 0
|
||||
? const Color(0xFF1A1A1A).withOpacity(opacity)
|
||||
: const Color(0xFFF5F5F5),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
day.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: count > 0 ? Colors.white : const Color(0xFF666666),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
|
||||
// 图例
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const Text('少', style: TextStyle(fontSize: 10, color: Color(0xFF999999))),
|
||||
const SizedBox(width: 4),
|
||||
...List.generate(4, (index) {
|
||||
final opacity = 0.2 + (index * 0.2);
|
||||
return Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A1A).withOpacity(opacity),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(width: 4),
|
||||
const Text('多', style: TextStyle(fontSize: 10, color: Color(0xFF999999))),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 日历选中的月份状态(用于 StatefulBuilder)
|
||||
static DateTime? _calendarSelectedMonth;
|
||||
}
|
||||
|
||||
/// 回顾项数据类
|
||||
@@ -427,3 +623,12 @@ class _MemoryItem {
|
||||
this.imagePath,
|
||||
});
|
||||
}
|
||||
|
||||
/// 每日数据
|
||||
class _DailyData {
|
||||
int movies = 0;
|
||||
int books = 0;
|
||||
int notes = 0;
|
||||
|
||||
int get total => movies + books + notes;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,21 @@ class NoteListItem extends StatelessWidget {
|
||||
|
||||
const NoteListItem({super.key, required this.note});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 使用 RepaintBoundary 减少重绘
|
||||
return RepaintBoundary(
|
||||
child: _NoteListItemContent(note: note),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 笔记列表项内容 - 分离出来便于优化
|
||||
class _NoteListItemContent extends StatelessWidget {
|
||||
final Note note;
|
||||
|
||||
const _NoteListItemContent({required this.note});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isPlainText = note.contentType == 'plain_text';
|
||||
@@ -50,9 +65,9 @@ class NoteListItem extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 时间
|
||||
// 时间 - 使用缓存的格式化结果
|
||||
Text(
|
||||
_formatDate(note.updatedAt),
|
||||
_formatDateCached(note.updatedAt),
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF999999),
|
||||
@@ -100,18 +115,11 @@ class NoteListItem extends StatelessWidget {
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: note.images.length > 2 ? 2 : note.images.length,
|
||||
physics: const NeverScrollableScrollPhysics(), // 禁用滚动,提高性能
|
||||
itemBuilder: (context, index) {
|
||||
return Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
||||
),
|
||||
child: Image.file(
|
||||
File(note.images[index]),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
return _NoteImage(
|
||||
imagePath: note.images[index],
|
||||
index: index,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -148,26 +156,6 @@ class NoteListItem extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// 格式化日期
|
||||
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) {
|
||||
showDialog(
|
||||
@@ -196,3 +184,75 @@ class NoteListItem extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 笔记图片组件 - 独立出来便于优化
|
||||
class _NoteImage extends StatelessWidget {
|
||||
final String imagePath;
|
||||
final int index;
|
||||
|
||||
const _NoteImage({
|
||||
required this.imagePath,
|
||||
required this.index,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
margin: EdgeInsets.only(right: index < 1 ? 8 : 0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
||||
),
|
||||
child: Image.file(
|
||||
File(imagePath),
|
||||
fit: BoxFit.cover,
|
||||
// 使用低内存缓存
|
||||
cacheWidth: 120,
|
||||
cacheHeight: 120,
|
||||
errorBuilder: (_, __, ___) => const Icon(
|
||||
Icons.broken_image,
|
||||
size: 24,
|
||||
color: Color(0xFFCCCCCC),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 日期格式化缓存
|
||||
final Map<DateTime, String> _dateFormatCache = {};
|
||||
|
||||
/// 格式化日期(带缓存)
|
||||
String _formatDateCached(DateTime date) {
|
||||
// 使用日期部分作为缓存键(忽略时分秒)
|
||||
final cacheKey = DateTime(date.year, date.month, date.day);
|
||||
|
||||
if (_dateFormatCache.containsKey(cacheKey)) {
|
||||
return _dateFormatCache[cacheKey]!;
|
||||
}
|
||||
|
||||
final result = _formatDate(date);
|
||||
_dateFormatCache[cacheKey] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 格式化日期
|
||||
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')}';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user