From d06967c6ab76702d99140c030410062ef265837f Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Thu, 22 Oct 2026 17:36:58 +0800 Subject: [PATCH] =?UTF-8?q?fix(recommend):=20=E4=BF=AE=E6=AD=A3=E6=8E=A8?= =?UTF-8?q?=E8=8D=90=E5=AD=90=E9=A1=B5=E5=86=85=E5=AE=B9=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/PROGRESS.md | 28 +++++----- lib/pages/home/home_view.dart | 6 +- lib/pages/player/player_view.dart | 8 ++- lib/pages/recommend/recommend_page.dart | 62 ++++++++++++++++++--- test/home/home_preview_layout_test.dart | 13 ++++- test/home/k28_a_navigation_test.dart | 10 +++- test/player/player_fullscreen_test.dart | 19 ++----- test/recommend/recommend_sections_test.dart | 30 +++++++++- 8 files changed, 128 insertions(+), 48 deletions(-) diff --git a/docs/plans/PROGRESS.md b/docs/plans/PROGRESS.md index 5971618..3ecc516 100644 --- a/docs/plans/PROGRESS.md +++ b/docs/plans/PROGRESS.md @@ -938,30 +938,30 @@ $ flutter test 解读:背景、卡片 token、圆角/描边、无渐变阴影、三宫格等宽、菜单 52 行高、 入口保留和收藏页跳转均有测试覆盖。`pubspec.lock` 未提交,未操作真机。 -## 2026-10-22 推荐页分区流 / 全部短剧 / 剧单 +## 2026-10-22 推荐页沉浸流 / 剧单 / 全部短剧 -- 分支:`feature/recommend-section-feed` -- 基线:`develop` / `origin/develop` = `0ef2438` -- 实现提交:`f1b5418 feat(recommend): 新增分区流全部短剧与剧单` +- 分支:`feature/recommend-section-feed`、`feature/recommend-tab-feed-correction` +- 修正基线:`develop` / `origin/develop` = `af6b9bb` -新增固定顶部三 tab:推荐 / 剧单 / 全部短剧。推荐区包含本週爆款推薦、 -本週精選新劇、古裝神劇大賞三条横滑卡片,数据分别本地按播放量、上架时间和 -古装分类/题材排序过滤;点击卡片进入 `PlayerPage(homeMode: true)`。 +固定顶部三 tab 为推荐 / 剧单 / 全部短剧,右侧保留搜索。推荐 tab 直接内嵌 +`PlayerPage(homeMode: true, embedded: true)`,支持一屏一剧、上下滑切换、 +收藏/点赞/选集/简介/进度条等现有播放器能力;播放器内部频道标题条关闭, +只保留视频与浮层。剧单 tab 包含本週爆款推薦、本週精選新劇、古裝神劇大賞 +三条横滑卡片,数据分别本地按播放量、上架时间和古装分类/题材排序过滤。 全部短剧提供四行可用筛选 chips、三列首页同款卡片和 20 条触底分页; -无后端字段的本地/地区/字幕/配音筛选项不展示。剧单内嵌可复用的 -`RankingPage(embedded: true)`,根底栏保持可见。搜索复用现有发现搜索页。 +无后端字段的本地/地区/字幕/配音筛选项不展示。根底栏始终保留四个入口。 ```text $ flutter analyze -Analyzing reelshort-bottom-nav... -No issues found! (ran in 2.0s) +Analyzing reelshort-recommend-correction... +No issues found! (ran in 3.2s) $ flutter test -00:52 +638: All tests passed! +01:03 +644: All tests passed! ``` -解读:推荐三分区、横滑卡片、四行筛选、三列网格、tab 状态保留、搜索、 -卡片播放和内嵌榜单均有测试覆盖;所有生效文件低于 500 行。 +解读:沉浸式推荐播放器、剧单三分区横滑、四行筛选、三列网格、tab 状态保留、 +搜索、卡片播放、播放器路由覆盖/恢复均有测试覆盖;所有生效文件低于 500 行。 `pubspec.lock` 未提交,未操作真机。 ## 2026-10-22 追剧页三 tab 与双列网格 diff --git a/lib/pages/home/home_view.dart b/lib/pages/home/home_view.dart index 15aef75..ebe5245 100644 --- a/lib/pages/home/home_view.dart +++ b/lib/pages/home/home_view.dart @@ -296,6 +296,7 @@ class _HomeTabsState extends State<_HomeTabs> { Widget _recommend(BuildContext context) { return RecommendPage( feedController: widget.feedController, + active: logic.tab == 1, onSearch: () => _openSearch(context), onOpenFilter: () => _pushPage(context, const FilterPage()), ); @@ -323,9 +324,8 @@ class _HomeTabsState extends State<_HomeTabs> { Future _openWallet(BuildContext context) async { if (!await ensureSignedIn(context) || !context.mounted) return; - await Navigator.of( - context, - ).push(MaterialPageRoute(builder: (_) => const WalletPage())); + await Navigator.of(context) + .push(MaterialPageRoute(builder: (_) => const WalletPage())); } Widget _my(BuildContext context) { diff --git a/lib/pages/player/player_view.dart b/lib/pages/player/player_view.dart index 42fe56a..cebd854 100644 --- a/lib/pages/player/player_view.dart +++ b/lib/pages/player/player_view.dart @@ -66,6 +66,7 @@ class PlayerPage extends StatefulWidget { this.feedDramas, this.feedChannel, this.headerChannel, + this.showHomeFeedHeader = true, this.onDramaChanged, this.onImmersiveChanged, this.onOpenFullPlayer, @@ -90,6 +91,7 @@ class PlayerPage extends StatefulWidget { final List? feedDramas; final HomeFeedChannel? feedChannel; final HomeFeedChannel? headerChannel; + final bool showHomeFeedHeader; final ValueChanged? onDramaChanged; final ValueChanged? onImmersiveChanged; final Future Function(Drama drama, Episode episode)? onOpenFullPlayer; @@ -360,7 +362,11 @@ class _PlayerPageState extends State fit: StackFit.expand, children: [ content, - if (widget.homeMode && !_boosting && !logic.pip && !logic.pipPreparing) + if (widget.homeMode && + widget.showHomeFeedHeader && + !_boosting && + !logic.pip && + !logic.pipPreparing) Positioned( top: 0, left: 0, diff --git a/lib/pages/recommend/recommend_page.dart b/lib/pages/recommend/recommend_page.dart index 8f3aad9..464ce9d 100644 --- a/lib/pages/recommend/recommend_page.dart +++ b/lib/pages/recommend/recommend_page.dart @@ -14,9 +14,9 @@ import '../../core/data/services/wallet_controller.dart'; import '../../core/navigation/fade_page_route.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_dimens.dart'; +import '../../core/widgets/empty_state.dart'; import '../../l10n/app_localizations.dart'; import '../player/player_view.dart'; -import '../ranking/ranking_page.dart'; import 'widgets/all_dramas_filter_grid.dart'; import 'widgets/recommend_sections_view.dart'; @@ -26,11 +26,13 @@ class RecommendPage extends StatefulWidget { const RecommendPage({ super.key, required this.feedController, + required this.active, required this.onSearch, required this.onOpenFilter, }); final HomeFeedController feedController; + final bool active; final VoidCallback onSearch; final VoidCallback onOpenFilter; @@ -57,13 +59,13 @@ class _RecommendPageState extends State { child: IndexedStack( index: _tab.index, children: [ + _recommendFeed(context), RecommendSectionsView( feedController: widget.feedController, categories: categories, onOpenDrama: _openDrama, onMore: _handleMore, ), - const RankingPage(embedded: true), AllDramasFilterGrid( feedController: widget.feedController, onOpenDrama: _openDrama, @@ -78,6 +80,49 @@ class _RecommendPageState extends State { ); } + Widget _recommendFeed(BuildContext context) { + final dramas = widget.feedController.dramas; + if (dramas.isEmpty) { + final l10n = AppLocalizations.of(context)!; + if (widget.feedController.loadingFirstPage) { + return const ColoredBox( + color: AppColors.mediaBlack, + child: Center( + child: CircularProgressIndicator( + key: Key('recommend-feed-loading'), + color: AppColors.muted, + ), + ), + ); + } + return ColoredBox( + color: AppColors.mediaBlack, + child: EmptyState( + key: const Key('recommend-feed-empty'), + icon: Icons.movie_outlined, + title: l10n.noStories, + actionLabel: l10n.retry, + onAction: () => unawaited(widget.feedController.refreshCurrent()), + ), + ); + } + return PlayerPage( + key: const Key('recommend-feed-player'), + homeMode: true, + embedded: true, + active: widget.active && _tab == RecommendInnerTab.feed, + showHomeFeedHeader: false, + drama: dramas.first, + initialIndex: 0, + feedDramas: dramas, + feedChannel: widget.feedController.channel, + onOpenFullPlayer: (drama, episode) => + _openDrama(drama, episodeId: episode.id), + onRefreshFeed: widget.feedController.refreshCurrent, + onLoadMore: widget.feedController.loadMore, + ); + } + Widget _header(AppLocalizations l10n) { final items = [ (RecommendInnerTab.feed, l10n.recommendTopTab, 'recommend'), @@ -165,14 +210,10 @@ class _RecommendPageState extends State { } void _handleMore(RecommendSectionKind section) { - setState(() { - _tab = section == RecommendSectionKind.hits - ? RecommendInnerTab.playlists - : RecommendInnerTab.allDramas; - }); + setState(() => _tab = RecommendInnerTab.allDramas); } - Future _openDrama(Drama drama) async { + Future _openDrama(Drama drama, {String? episodeId}) async { final catalog = context.read(); final wallet = context.read(); final downloads = context.read(); @@ -183,11 +224,14 @@ class _RecommendPageState extends State { // 卡片预览仍可直接进入播放器。 } if (!mounted) return; + final episodeIndex = episodeId == null + ? 0 + : playable.episodes.indexWhere((episode) => episode.id == episodeId); await Navigator.of(context).push( FadePageRoute( builder: (_) => PlayerPage( drama: playable, - initialIndex: 0, + initialIndex: episodeIndex < 0 ? 0 : episodeIndex, homeMode: true, feedDramas: [playable], wallet: wallet, diff --git a/test/home/home_preview_layout_test.dart b/test/home/home_preview_layout_test.dart index c5552ee..dd58684 100644 --- a/test/home/home_preview_layout_test.dart +++ b/test/home/home_preview_layout_test.dart @@ -96,7 +96,7 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('should_show_section_feed_and_open_player_from_recommend', ( + testWidgets('should_show_immersive_player_and_open_playlist_in_recommend', ( tester, ) async { SharedPreferencesAsyncPlatform.instance = @@ -126,8 +126,17 @@ void main() { expect(find.byKey(const Key('home-grid')), findsNothing); expect(find.byKey(const Key('recommend-page')), findsOneWidget); + expect(find.byKey(const Key('recommend-feed-player')), findsOneWidget); + expect(find.byType(PlayerPage), findsOneWidget); + expect( + tester.widget(find.byType(PlayerPage)).homeMode, + isTrue, + ); + expect(find.byKey(const Key('recommend-section-hits')), findsNothing); + + await tester.tap(find.byKey(const Key('recommend-tab-playlist'))); + await tester.pumpAndSettle(); expect(find.byKey(const Key('recommend-section-hits')), findsOneWidget); - expect(find.byType(PlayerPage), findsNothing); await tester.tap( find.byKey(const Key('recommend-card-hits-the-signal-demo')), diff --git a/test/home/k28_a_navigation_test.dart b/test/home/k28_a_navigation_test.dart index 7473163..b88287a 100644 --- a/test/home/k28_a_navigation_test.dart +++ b/test/home/k28_a_navigation_test.dart @@ -70,7 +70,7 @@ void main() { expect(tester.getRect(grid), before); }); - testWidgets('should_render_recommend_sections_and_inner_top_nav', ( + testWidgets('should_render_recommend_immersive_player_and_inner_top_nav', ( tester, ) async { await _pumpApp(tester); @@ -81,6 +81,10 @@ void main() { expect(find.byKey(const Key('recommend-tab-recommend')), findsOneWidget); expect(find.byKey(const Key('recommend-tab-playlist')), findsOneWidget); expect(find.byKey(const Key('recommend-tab-all')), findsOneWidget); + expect(find.byKey(const Key('recommend-feed-player')), findsOneWidget); + + await tester.tap(find.byKey(const Key('recommend-tab-playlist'))); + await tester.pumpAndSettle(); expect(find.byKey(const Key('recommend-section-hits')), findsOneWidget); expect(find.byKey(const Key('recommend-section-new')), findsOneWidget); expect(find.byKey(const Key('recommend-section-costume')), findsOneWidget); @@ -95,7 +99,7 @@ void main() { await tester.tap(find.byKey(const Key('recommend-tab-playlist'))); await tester.pumpAndSettle(); - expect(find.byKey(const Key('ranking-page')), findsOneWidget); + expect(find.byKey(const Key('recommend-section-hits')), findsOneWidget); expect(find.byType(AppBottomNav), findsOneWidget); await tester.tap(find.byKey(const Key('recommend-tab-all'))); @@ -143,7 +147,7 @@ void main() { await tester.tap(find.byKey(const Key('tab-recommend'))); await tester.pumpAndSettle(); - expect(find.byKey(const Key('recommend-section-hits')), findsOneWidget); + expect(find.byKey(const Key('recommend-feed-player')), findsOneWidget); expect(tester.takeException(), isNull); }); } diff --git a/test/player/player_fullscreen_test.dart b/test/player/player_fullscreen_test.dart index 433f3e6..691023d 100644 --- a/test/player/player_fullscreen_test.dart +++ b/test/player/player_fullscreen_test.dart @@ -36,19 +36,18 @@ void main() { InMemorySharedPreferencesAsync.empty(); }); - testWidgets('should_open_fullscreen_overlay_from_recommend_feed', ( + testWidgets('should_render_immersive_overlay_in_recommend_feed', ( tester, ) async { await _pumpApp(tester); await tester.tap(find.byKey(const Key('tab-recommend'))); await tester.pumpAndSettle(); - await tester.tap( - find.byKey(const Key('recommend-card-hits-the-signal-demo')), - ); - await tester.pumpAndSettle(); + expect(find.byKey(const Key('recommend-feed-player')), findsOneWidget); expect( - tester.widget(find.byType(PlayerPage).last).homeMode, + tester + .widget(find.byKey(const Key('recommend-feed-player'))) + .homeMode, isTrue, ); expect(find.byType(PageView), findsOneWidget); @@ -80,10 +79,6 @@ void main() { await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); await tester.tap(find.byKey(const Key('tab-recommend'))); - await tester.pumpAndSettle(); - await tester.tap( - find.byKey(const Key('recommend-card-hits-the-signal-demo')), - ); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); @@ -170,10 +165,6 @@ void main() { ); await tester.tap(find.byKey(const Key('tab-recommend'))); await tester.pumpAndSettle(); - await tester.tap( - find.byKey(const Key('recommend-card-hits-the-signal-demo')), - ); - await tester.pumpAndSettle(); final player = players.firstWhere((item) => item.playing); expect(player.playing, isTrue); diff --git a/test/recommend/recommend_sections_test.dart b/test/recommend/recommend_sections_test.dart index fd2e8f4..b5029ef 100644 --- a/test/recommend/recommend_sections_test.dart +++ b/test/recommend/recommend_sections_test.dart @@ -23,7 +23,7 @@ void main() { InMemorySharedPreferencesAsync.empty(); }); - testWidgets('should_render_three_recommend_sections_and_horizontal_cards', ( + testWidgets('should_render_immersive_player_as_recommend_tab', ( tester, ) async { await _pumpRecommend(tester); @@ -32,6 +32,25 @@ void main() { expect(find.byKey(const Key('recommend-tab-recommend')), findsOneWidget); expect(find.byKey(const Key('recommend-tab-playlist')), findsOneWidget); expect(find.byKey(const Key('recommend-tab-all')), findsOneWidget); + expect(find.byKey(const Key('recommend-feed-player')), findsOneWidget); + final player = tester.widget( + find.byKey(const Key('recommend-feed-player')), + ); + expect(player.homeMode, isTrue); + expect(player.embedded, isTrue); + expect(player.showHomeFeedHeader, isFalse); + expect(find.byType(PageView), findsOneWidget); + expect(find.byKey(const Key('recommend-section-hits')), findsNothing); + expect(find.byType(AppBottomNav), findsOneWidget); + }); + + testWidgets('should_render_playlist_sections_and_horizontal_cards', ( + tester, + ) async { + await _pumpRecommend(tester); + await tester.tap(find.byKey(const Key('recommend-tab-playlist'))); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('recommend-section-hits')), findsOneWidget); expect(find.byKey(const Key('recommend-section-new')), findsOneWidget); expect(find.byKey(const Key('recommend-section-costume')), findsOneWidget); @@ -54,6 +73,10 @@ void main() { ) async { await _pumpRecommend(tester); + await tester.tap(find.byKey(const Key('recommend-tab-playlist'))); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('recommend-section-hits')), findsOneWidget); + await tester.tap(find.byKey(const Key('recommend-tab-all'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('all-dramas-grid')), findsOneWidget); @@ -68,7 +91,8 @@ void main() { await tester.tap(find.byKey(const Key('recommend-tab-playlist'))); await tester.pumpAndSettle(); - expect(find.byKey(const Key('ranking-page')), findsOneWidget); + expect(find.byKey(const Key('recommend-section-hits')), findsOneWidget); + expect(find.byKey(const Key('recommend-feed-player')), findsNothing); expect(find.byType(AppBottomNav), findsOneWidget); await tester.tap(find.byKey(const Key('recommend-tab-all'))); @@ -82,6 +106,8 @@ void main() { ) async { await _pumpRecommend(tester); + await tester.tap(find.byKey(const Key('recommend-tab-playlist'))); + await tester.pumpAndSettle(); await tester.tap( find.byKey(const Key('recommend-card-hits-the-signal-demo')), );