From 2b956eb96c193a52ce7201caf49fcef42e03008f Mon Sep 17 00:00:00 2001 From: ddshi <8811906+ddshi@user.noreply.gitee.com> Date: Thu, 28 Aug 2025 20:04:53 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A6=96=E9=A1=B5=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 5 +- lib/main.dart | 109 ++++++++++++---- lib/pages/categories_page.dart | 131 +++++++++++++------ lib/pages/photo_page.dart | 231 +++++++++++++++++++++++++++------ 4 files changed, 369 insertions(+), 107 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5ee4b82..e657894 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -119,4 +119,7 @@ flutter pub upgrade - **Flutter**: 3.2.5+ - **Dart**: 3.2.5+ - **目标平台**: iOS、Android(支持 Web) -- **构建工具**: 标准 Flutter 工具链 \ No newline at end of file +- **构建工具**: 标准 Flutter 工具链 + +## 特殊说明 +- 代码中使用中文注释,且简介、有效的说明逻辑,并支持flutter复杂、重点的运用和开发技巧,方便后续复盘和学习 \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index f9d5a4a..0f085fe 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,6 +14,7 @@ class SnapWishApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'SnapWish', + debugShowCheckedModeBanner: false, theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, @@ -44,28 +45,92 @@ class _MainPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: _pages[_currentIndex], - floatingActionButton: FloatingActionButton( - onPressed: () { - Navigator.pushNamed(context, '/add-photo'); - }, - child: const Icon(Icons.add), - ), - bottomNavigationBar: NavigationBar( - selectedIndex: _currentIndex, - onDestinationSelected: (index) { - setState(() { - _currentIndex = index; - }); - }, - destinations: const [ - NavigationDestination( - icon: Icon(Icons.photo_library), - label: '照片', - ), - NavigationDestination( - icon: Icon(Icons.folder), - label: '分类', + extendBody: true, // 允许内容延伸到导航栏区域 + body: Stack( + children: [ + _pages[_currentIndex], + // 悬浮底部导航组件 + Positioned( + left: 20, + right: 20, + bottom: 80, + child: Container( + height: 70, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.95), + borderRadius: BorderRadius.circular(35), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // 照片标签按钮 + IconButton( + onPressed: () { + setState(() { + _currentIndex = 0; + }); + }, + icon: Icon( + Icons.photo_library, + color: _currentIndex == 0 + ? Colors.deepPurple + : Colors.grey, + size: 28, + ), + ), + + // 添加按钮 + GestureDetector( + onTap: () { + Navigator.pushNamed(context, '/add-photo'); + }, + child: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Colors.deepPurple, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.deepPurple.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: const Icon( + Icons.add, + color: Colors.white, + size: 30, + ), + ), + ), + + // 分类标签按钮 + IconButton( + onPressed: () { + setState(() { + _currentIndex = 1; + }); + }, + icon: Icon( + Icons.folder, + color: _currentIndex == 1 + ? Colors.deepPurple + : Colors.grey, + size: 28, + ), + ), + ], + ), + ), ), ], ), diff --git a/lib/pages/categories_page.dart b/lib/pages/categories_page.dart index a14ed44..1cbeaf7 100644 --- a/lib/pages/categories_page.dart +++ b/lib/pages/categories_page.dart @@ -93,50 +93,99 @@ class _CategoriesPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('分类'), - actions: [ - IconButton( - icon: const Icon(Icons.create_new_folder), - onPressed: _createCategory, + body: Stack( + children: [ + ListView.builder( + padding: const EdgeInsets.only(top: 140, bottom: 16), + itemCount: _categories.length + 1, // +1 for header + itemBuilder: (context, index) { + if (index == 0) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '分类', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.deepPurple, + ), + ), + IconButton( + icon: const Icon(Icons.create_new_folder, color: Colors.deepPurple), + onPressed: _createCategory, + ), + ], + ), + ); + } + + final categoryIndex = index - 1; + final category = _categories[categoryIndex]; + return Card( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: ListTile( + leading: CircleAvatar( + backgroundColor: category['isDefault'] + ? Colors.blue + : Colors.deepPurple, + child: const Icon(Icons.folder, color: Colors.white), + ), + title: Text( + category['name'], + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text('${category['count']} 张照片'), + trailing: category['isDefault'] + ? null + : IconButton( + icon: const Icon(Icons.delete_outline, color: Colors.red), + onPressed: () => _deleteCategory(categoryIndex), + ), + onTap: () { + // TODO: 打开该分类的照片列表 + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('打开 ${category['name']}')) + ); + }, + ), + ); + }, + ), + // 悬浮标题栏 + Positioned( + top: 0, + left: 0, + right: 0, + child: Container( + height: 80, + padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.95), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: const Center( + child: Text( + 'SnapWish', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.deepPurple, + ), + ), + ), + ), ), ], ), - body: ListView.builder( - padding: const EdgeInsets.all(8), - itemCount: _categories.length, - itemBuilder: (context, index) { - final category = _categories[index]; - return Card( - margin: const EdgeInsets.symmetric(vertical: 4), - child: ListTile( - leading: CircleAvatar( - backgroundColor: category['isDefault'] - ? Colors.blue - : Colors.deepPurple, - child: const Icon(Icons.folder, color: Colors.white), - ), - title: Text( - category['name'], - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text('${category['count']} 张照片'), - trailing: category['isDefault'] - ? null - : IconButton( - icon: const Icon(Icons.delete_outline, color: Colors.red), - onPressed: () => _deleteCategory(index), - ), - onTap: () { - // TODO: 打开该分类的照片列表 - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('打开 ${category['name']}')), - ); - }, - ), - ); - }, - ), ); } } \ No newline at end of file diff --git a/lib/pages/photo_page.dart b/lib/pages/photo_page.dart index 7045725..7bc446d 100644 --- a/lib/pages/photo_page.dart +++ b/lib/pages/photo_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'dart:math'; class PhotoPage extends StatelessWidget { const PhotoPage({super.key}); @@ -6,68 +7,212 @@ class PhotoPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('照片'), + body: Stack( + children: [ + const PhotoTimeline(), + Positioned( + top: 0, + left: 0, + right: 0, + child: Container( + height: 80, + padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.95), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: const Center( + child: Text( + 'SnapWish', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.deepPurple, + ), + ), + ), + ), + ), + ], ), - body: const PhotoGrid(), ); } } -class PhotoGrid extends StatelessWidget { - const PhotoGrid({super.key}); +class PhotoTimeline extends StatelessWidget { + const PhotoTimeline({super.key}); + + // 生成带日期的模拟照片数据 + Map>> _groupPhotosByDate() { + final random = Random(); + final now = DateTime.now(); + final photos = >[]; + + // 生成过去30天的模拟数据 + for (int i = 0; i < 30; i++) { + final date = now.subtract(Duration(days: i)); + final photoCount = random.nextInt(5) + 1; // 每天1-5张照片 + + for (int j = 0; j < photoCount; j++) { + photos.add({ + 'id': 'photo_${i}_$j', + 'title': '照片 ${i * 5 + j + 1}', + 'date': date, + }); + } + } + + // 按日期分组 + final grouped = >>{}; + for (final photo in photos) { + final date = photo['date'] as DateTime; + final key = _formatDateKey(date); + grouped.putIfAbsent(key, () => []).add(photo); + } + + return grouped; + } + + String _formatDateKey(DateTime date) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final yesterday = today.subtract(const Duration(days: 1)); + final dateDay = DateTime(date.year, date.month, date.day); + + if (dateDay == today) { + return '今天'; + } else if (dateDay == yesterday) { + return '昨天'; + } else if (date.year == now.year) { + return '${date.month}月${date.day}日'; + } else { + return '${date.year}年${date.month}月${date.day}日'; + } + } @override Widget build(BuildContext context) { - // TODO: 替换为真实照片数据 - final mockPhotos = List.generate(20, (index) => '照片 ${index + 1}'); + final groupedPhotos = _groupPhotosByDate(); + final sortedKeys = groupedPhotos.keys.toList() + ..sort((a, b) { + // 将中文日期转换为可排序的格式 + final dateA = _parseDateKey(a); + final dateB = _parseDateKey(b); + return dateB.compareTo(dateA); // 降序排列 + }); - return GridView.builder( - padding: const EdgeInsets.all(8), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: 4, - mainAxisSpacing: 4, - childAspectRatio: 1, - ), - itemCount: mockPhotos.length, + return ListView.builder( + padding: const EdgeInsets.only(top: 88, bottom: 16), + itemCount: sortedKeys.length, itemBuilder: (context, index) { - return GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PhotoDetailPage( - photos: mockPhotos, - initialIndex: index, + final dateKey = sortedKeys[index]; + final photos = groupedPhotos[dateKey]!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Text( + dateKey, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.deepPurple, ), ), - ); - }, - child: Container( - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(8), ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.photo, size: 40, color: Colors.grey), - const SizedBox(height: 4), - Text( - mockPhotos[index], - style: const TextStyle(fontSize: 12), - overflow: TextOverflow.ellipsis, - ), - ], + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 16), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 4, + mainAxisSpacing: 4, + childAspectRatio: 1, ), + itemCount: photos.length, + itemBuilder: (context, photoIndex) { + final photo = photos[photoIndex]; + return GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PhotoDetailPage( + photos: photos.map((p) => p['title'] as String).toList(), + initialIndex: photoIndex, + ), + ), + ); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.photo, size: 40, color: Colors.grey), + const SizedBox(height: 4), + Text( + photo['title'] as String, + style: const TextStyle(fontSize: 12), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + ); + }, ), - ), + ], ); }, ); } + + DateTime _parseDateKey(String key) { + final now = DateTime.now(); + + if (key == '今天') { + return now; + } else if (key == '昨天') { + return now.subtract(const Duration(days: 1)); + } else if (key.contains('年')) { + // 格式:2024年5月15日 + final match = RegExp(r'(\d+)年(\d+)月(\d+)日').firstMatch(key); + if (match != null) { + return DateTime( + int.parse(match.group(1)!), + int.parse(match.group(2)!), + int.parse(match.group(3)!), + ); + } + } else { + // 格式:5月15日 + final match = RegExp(r'(\d+)月(\d+)日').firstMatch(key); + if (match != null) { + return DateTime( + now.year, + int.parse(match.group(1)!), + int.parse(match.group(2)!), + ); + } + } + + return now; + } } class PhotoDetailPage extends StatefulWidget {