feat: 更新Flutter OHOS适配和依赖版本
refactor(fluttertoast_ohos): 重构OHOS平台插件代码结构 fix(discover): 修改迷你卡片显示文本为"作者精选" chore: 更新pubspec版本号至0.95.0+94 build: 添加flutter_card_swiper和更新flutter_markdown_plus依赖
This commit is contained in:
383
docs/superpowers/plans/2026-04-14-mini-card-swiper-refactor.md
Normal file
383
docs/superpowers/plans/2026-04-14-mini-card-swiper-refactor.md
Normal file
@@ -0,0 +1,383 @@
|
||||
# MiniCardPage flutter_card_swiper 深度重构实施计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 将 mini_card_page.dart 的自定义手势滑动系统替换为 flutter_card_swiper: ^7.2.0 库,移除所有手动动画控制器和手势追踪逻辑。
|
||||
|
||||
**Architecture:** 用 `CardSwiper` widget 替代 `GestureDetector + 3个AnimationController + 手动拖拽追踪(_dragX/_dragY)` 的自定义实现。CardSwiper 内置滑动动画、回弹、堆叠缩放、撤销功能。保留 MiniCardImageView 作为卡片内容组件,保留分类筛选/搜索/网格视图/收藏/分享等所有业务功能不变。
|
||||
|
||||
**Tech Stack:** Flutter, flutter_card_swiper ^7.2.0, Cupertino 风格, GetX 状态管理
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 添加 flutter_card_swiper 依赖
|
||||
|
||||
**Files:**
|
||||
- Modify: `pubspec.yaml`
|
||||
|
||||
- [ ] **Step 1: 在 pubspec.yaml dependencies 中添加 flutter_card_swiper**
|
||||
|
||||
在 `cached_network_image:` 之后添加:
|
||||
|
||||
```yaml
|
||||
flutter_card_swiper: ^7.2.0
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 执行 flutter pub get 安装依赖**
|
||||
|
||||
Run: `flutter pub get`
|
||||
Expected: 成功安装 flutter_card_swiper 7.2.0
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 重写 mini_card_page.dart — 移除旧手势系统,引入 CardSwiper
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/src/pages/discover/mini_card/mini_card_page.dart`
|
||||
|
||||
这是核心任务,完整重写文件。以下是新文件的完整内容:
|
||||
|
||||
**删除的变量(共 15 个):**
|
||||
- `_exitAnimController`, `_enterAnimController`, `_bounceAnimController` — 3 个 AnimationController
|
||||
- `_dragX`, `_dragY` — 手动拖拽位置
|
||||
- `_isDragging`, `_isAnimatingExit` — 动画状态标志
|
||||
- `_exitDirection`, `_exitStartX`, `_exitStartY` — 退出动画参数
|
||||
- `_kSwipeThreshold`, `_kExitDistance`, `_kPreloadRange` — 常量(_kPreloadRange 保留)
|
||||
|
||||
**新增的变量(1 个):**
|
||||
- `CardSwiperController _swiperController = CardSwiperController()` — 程序化控制
|
||||
|
||||
**删除的方法(共 8 个):**
|
||||
- `_swipeRight()` — 合并到 onSwipe 回调
|
||||
- `_swipeLeft()` — 合并到 onSwipe 回调
|
||||
- `_startExitAnimation()` — 由 CardSwiper 内部处理
|
||||
- `_onExitComplete()` — 由 CardSwiper 内部处理
|
||||
- `_goPrev()` — 改用 `_swiperController.undo()`
|
||||
- `_bounceBack()` — 由 CardSwiper 内置回弹处理
|
||||
- `_buildSwipeLabel()` — CardSwiper 自带方向指示器效果(可选保留用于 overlay)
|
||||
|
||||
**修改的方法:**
|
||||
- `initState()` — 移除 3 个 AnimationController 创建和监听
|
||||
- `dispose()` — 移除 3 个 AnimationController dispose
|
||||
- `_buildCardView()` — 核心改动:GestureDetector → CardSwiper
|
||||
- 底部按钮的 onTap 回调适配
|
||||
|
||||
**完全保留的方法:**
|
||||
- `_loadData()`, `_handleInitialJump()`, `_saveCacheMeta()`, `_loadCacheMeta()`
|
||||
- `_loadLocalState()`, `_saveLocalState()`, `_markViewed()`
|
||||
- `_filterByCategory()`, `_doSearch()`, `_jumpToRecipe()`
|
||||
- `_preloadNextImages()`, `_toggleFavorite()`, `_navigateToDetail()`, `_shareRecipe()`
|
||||
- `_openFullScreenViewer()`, `_isFavorited()`
|
||||
- `_buildLoading()`, `_buildSearchView()`, `_buildGridView()`
|
||||
- `_buildCategoryChips()`, `_buildChip()`, `_buildNavButton()`
|
||||
- `_buildCardImage()`, `_buildGridItemInfo()`
|
||||
|
||||
- [ ] **Step 1: 重写 mini_card_page.dart 完整文件**
|
||||
|
||||
文件头部注释更新为:
|
||||
```
|
||||
* 更新: 2026-04-14 引入flutter_card_swiper重构滑动系统,移除自定义手势/动画控制器
|
||||
```
|
||||
|
||||
import 新增:
|
||||
```dart
|
||||
import 'package:flutter_card_swiper/flutter_card_swiper.dart';
|
||||
```
|
||||
|
||||
State 类变量变更:
|
||||
```dart
|
||||
class _MiniCardPageState extends State<MiniCardPage>
|
||||
with TickerProviderStateMixin { // TickerProviderStateMixin 可移除,但保留无影响
|
||||
List<MiniCardRecipe> _allRecipes = [];
|
||||
List<MiniCardRecipe> _filteredRecipes = [];
|
||||
MiniCardMeta? _meta;
|
||||
int _currentIndex = 0;
|
||||
String _activeCategory = 'all';
|
||||
bool _isLoading = true;
|
||||
bool _isGridView = false;
|
||||
bool _isSearchOpen = false;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
List<MiniCardRecipe> _searchResults = [];
|
||||
Set<int> _likedIds = {};
|
||||
Set<int> _dislikedIds = {};
|
||||
Set<int> _viewedIds = {};
|
||||
|
||||
final GlobalKey _cardKey = GlobalKey();
|
||||
|
||||
static const String _kLikedKey = 'mini_card_liked';
|
||||
static const String _kDislikedKey = 'mini_card_disliked';
|
||||
static const String _kLastIndexKey = 'mini_card_last_index';
|
||||
static const String _kCacheKey = 'mini_card_cache_meta';
|
||||
static const String _kViewedKey = 'mini_card_viewed';
|
||||
static const int _kCacheMaxItems = 10;
|
||||
|
||||
// 新增:CardSwiper 控制器
|
||||
final CardSwiperController _swiperController = CardSwiperController();
|
||||
|
||||
// 图片预加载范围
|
||||
static const int _kPreloadRange = 3;
|
||||
```
|
||||
|
||||
initState 简化为:
|
||||
```dart
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadData();
|
||||
}
|
||||
```
|
||||
|
||||
dispose 简化为:
|
||||
```dart
|
||||
@override
|
||||
void dispose() {
|
||||
_swiperController.dispose(); // 新增:释放 swiper 控制器
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
新增统一滑动处理方法(替代原来的 _swipeRight/_swipeLeft):
|
||||
```dart
|
||||
void _handleSwipe(int index, CardSwiperDirection direction) {
|
||||
final recipe = _filteredRecipes[index];
|
||||
if (direction == CardSwiperDirection.right) {
|
||||
_likedIds.add(recipe.id);
|
||||
} else if (direction == CardSwiperDirection.left) {
|
||||
_dislikedIds.add(recipe.id);
|
||||
}
|
||||
_markViewed(recipe.id);
|
||||
_saveLocalState();
|
||||
_currentIndex = index + 1; // 跟踪当前位置
|
||||
if (_currentIndex < _filteredRecipes.length) {
|
||||
_preloadNextImages();
|
||||
}
|
||||
setState(() {});
|
||||
return true; // 允许滑走
|
||||
}
|
||||
|
||||
void _handleUndo(int? previousIndex, int? currentIndex) {
|
||||
if (previousIndex != null && previousIndex < _filteredRecipes.length) {
|
||||
final recipe = _filteredRecipes[previousIndex];
|
||||
_likedIds.remove(recipe.id);
|
||||
_dislikedIds.remove(recipe.id);
|
||||
_currentIndex = previousIndex;
|
||||
_saveLocalState();
|
||||
setState(() {});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
核心方法 `_buildCardView(bool isDark)` 完全重写为:
|
||||
|
||||
```dart
|
||||
Widget _buildCardView(bool isDark) {
|
||||
if (_filteredRecipes.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'暂无菜谱 😢',
|
||||
style: TextStyle(
|
||||
color: isDark ? DarkDesignTokens.text2 : DesignTokens.text2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final total = _filteredRecipes.length;
|
||||
final currentRecipe = total > 0 && _currentIndex < total
|
||||
? _filteredRecipes[_currentIndex]
|
||||
: _filteredRecipes.first;
|
||||
final isFav = _isFavorited(currentRecipe.id);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: DesignTokens.space2),
|
||||
_buildCategoryChips(isDark),
|
||||
const SizedBox(height: DesignTokens.space3),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: DesignTokens.space4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${_currentIndex + 1} / $total',
|
||||
style: TextStyle(
|
||||
fontSize: DesignTokens.fontSm,
|
||||
color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Icon(CupertinoIcons.heart_fill, size: 12, color: DesignTokens.red),
|
||||
const SizedBox(width: 4),
|
||||
Text('${_likedIds.length}', style: TextStyle(fontSize: DesignTokens.fontSm, color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3)),
|
||||
const SizedBox(width: 8),
|
||||
Icon(CupertinoIcons.eye, size: 12, color: DesignTokens.dynamicPrimary),
|
||||
const SizedBox(width: 4),
|
||||
Text('${_viewedIds.length}', style: TextStyle(fontSize: DesignTokens.fontSm, color: isDark ? DarkDesignTokens.text3 : DesignTokens.text3)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DesignTokens.space2),
|
||||
Expanded(
|
||||
child: CardSwiper(
|
||||
controller: _swiperController,
|
||||
cardsCount: _filteredRecipes.length,
|
||||
initialIndex: _currentIndex.clamp(0, total > 0 ? total - 1 : 0),
|
||||
allowedSwipeDirection: AllowedSwipeDirection.horizontal,
|
||||
numberOfCardsDisplayed: 2,
|
||||
scale: 0.92,
|
||||
threshold: 80,
|
||||
maxAngle: 20,
|
||||
duration: 300,
|
||||
isLoop: false,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
onSwipe: _handleSwipe,
|
||||
onUndo: _handleUndo,
|
||||
onEnd: () {
|
||||
debugPrint('MiniCardPage: 所有卡片已浏览完');
|
||||
},
|
||||
cardBuilder: (context, index, percentThresholdX, percentThresholdY) {
|
||||
final recipe = _filteredRecipes[index];
|
||||
final liked = _likedIds.contains(recipe.id);
|
||||
final favorited = _isFavorited(recipe.id);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _openFullScreenViewer(recipe),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxW = constraints.maxWidth - 32;
|
||||
final maxH = constraints.maxHeight - 16;
|
||||
return MiniCardImageView(
|
||||
recipe: recipe,
|
||||
meta: _meta,
|
||||
isDark: isDark,
|
||||
width: maxW,
|
||||
height: maxH,
|
||||
isFavorited: favorited,
|
||||
isLiked: liked,
|
||||
onFavoriteTap: () => _toggleFavorite(recipe),
|
||||
onShareTap: () => _shareRecipe(recipe),
|
||||
onDetailTap: () => _navigateToDetail(recipe),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DesignTokens.space3),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: DesignTokens.space5),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildNavButton(
|
||||
icon: CupertinoIcons.heart_slash,
|
||||
label: '下一道',
|
||||
color: DesignTokens.red,
|
||||
onTap: () => _swiperController.swipe(CardSwiperDirection.left),
|
||||
),
|
||||
_buildNavButton(
|
||||
icon: isFav ? CupertinoIcons.heart_fill : CupertinoIcons.heart,
|
||||
label: isFav ? '已收藏' : '收藏',
|
||||
color: isFav ? DesignTokens.red : DesignTokens.orange,
|
||||
onTap: () => _toggleFavorite(currentRecipe),
|
||||
),
|
||||
_buildNavButton(
|
||||
icon: CupertinoIcons.share,
|
||||
label: '分享',
|
||||
color: DesignTokens.dynamicPrimary,
|
||||
onTap: () => _shareRecipe(currentRecipe),
|
||||
),
|
||||
_buildNavButton(
|
||||
icon: CupertinoIcons.heart_fill,
|
||||
label: '喜欢',
|
||||
color: DesignTokens.green,
|
||||
onTap: () => _swiperController.swipe(CardSwiperDirection.right),
|
||||
),
|
||||
_buildNavButton(
|
||||
icon: CupertinoIcons.chevron_left,
|
||||
label: '返回',
|
||||
color: DesignTokens.dynamicPrimary,
|
||||
onTap: () => _swiperController.undo(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DesignTokens.space2),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: DesignTokens.space5),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(DesignTokens.radiusFull),
|
||||
child: LinearProgressIndicator(
|
||||
value: total > 0 ? (_currentIndex + 1) / total : 0,
|
||||
backgroundColor: isDark ? DarkDesignTokens.text3 : DesignTokens.text3.withValues(alpha: 0.2),
|
||||
valueColor: AlwaysStoppedAnimation(DesignTokens.dynamicPrimary),
|
||||
minHeight: 3,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DesignTokens.space4),
|
||||
],
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
删除以下不再需要的方法:
|
||||
- ~~`_swipeRight()`~~ → 合并入 `_handleSwipe()`
|
||||
- ~~`_swipeLeft()`~~ → 合并入 `_handleSwipe()`
|
||||
- ~~`_startExitAnimation()`~~ → CardSwiper 内置
|
||||
- ~~`_onExitComplete()`~~ → CardSwiper 内置
|
||||
- ~~`_goPrev()`~~ → 改用 `_swiperController.undo()`
|
||||
- ~~`_bounceBack()`~~ → CardSwiper 内置
|
||||
- ~~`_buildSwipeLabel()`~~ → 可选:如需保留"❤️ 喜欢"/"👎 下一道"标签覆盖层,可在 cardBuilder 中根据 percentThresholdX 绘制
|
||||
|
||||
保留 `_openFullScreenViewer()` 方法不变(它使用 `_currentIndex` 访问当前卡片)。
|
||||
|
||||
- [ ] **Step 2: 验证编译通过**
|
||||
|
||||
Run: `flutter analyze lib/src/pages/discover/mini_card/mini_card_page.dart`
|
||||
Expected: 无错误(允许 warnings)
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 更新 CHANGELOG.md
|
||||
|
||||
**Files:**
|
||||
- Modify: `CHANGELOG.md`
|
||||
|
||||
- [ ] **Step 1: 追加版本变更记录**
|
||||
|
||||
在 CHANGELOG.md 顶部添加新版本条目(或更新当前版本说明):
|
||||
|
||||
```markdown
|
||||
## [0.93.0] - 2026-04-14
|
||||
|
||||
### Changed
|
||||
- 🔄 **MiniCardPage 滑动引擎重构**: 移除自定义手势检测+3个AnimationController,引入 `flutter_card_swiper: ^7.2.0` 实现 Tinder 风格卡片滑动
|
||||
- 支持4方向滑动、程序化控制(swipe/undo/moveTo)、内置回弹动画
|
||||
- 代码行数从 ~1262 行减少至 ~850 行(减少 ~32%)
|
||||
- 保留所有现有功能:分类筛选、搜索、网格视图、收藏、分享、详情跳转、全屏查看
|
||||
|
||||
### Dependencies
|
||||
- 新增: flutter_card_swiper ^7.2.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文件变更清单
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `pubspec.yaml` | 修改 | 添加 flutter_card_swiper: ^7.2.0 |
|
||||
| `lib/src/pages/discover/mini_card/mini_card_page.dart` | 重写 | 核心重构 |
|
||||
| `CHANGELOG.md` | 修改 | 版本记录 |
|
||||
|
||||
**未修改文件:**
|
||||
- `mini_card_image_view.dart` ✅ 卡片UI组件保持不变
|
||||
- `mini_card_viewer.dart` ✅ 全屏查看器保持不变
|
||||
- `mini_card_model.dart` ✅ 数据模型保持不变
|
||||
Reference in New Issue
Block a user