feat(recommend): 对齐沉浸式推荐页视觉布局

This commit is contained in:
Zhou Weidong committed 2026-10-22 19:42:22 +08:00
1 parent 77689a3eaa
commit 5dd3492aea
17 files changed
+831 -398

No files matched your search

+37 -103
View File
@@ -13,14 +13,13 @@ import '../../core/data/services/home_feed_controller.dart';
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 'recommend_types.dart';
import 'widgets/all_dramas_filter_grid.dart';
import 'widgets/recommend_sections_view.dart';
enum RecommendInnerTab { feed, playlists, allDramas }
import 'widgets/recommend_top_nav.dart';
class RecommendPage extends StatefulWidget {
const RecommendPage({
@@ -50,38 +49,49 @@ class _RecommendPageState extends State<RecommendPage> {
return ColoredBox(
key: const Key('recommend-page'),
color: AppColors.appBackground,
child: SafeArea(
bottom: false,
child: Column(
children: [
_header(l10n),
Expanded(
child: ListenableBuilder(
listenable: Listenable.merge([
widget.feedController,
categories,
]),
builder: (context, _) => IndexedStack(
index: _tab.index,
children: [
_recommendFeed(context),
child: Stack(
children: [
Positioned.fill(
child: ListenableBuilder(
listenable: Listenable.merge([widget.feedController, categories]),
builder: (context, _) => IndexedStack(
index: _tab.index,
children: [
_recommendFeed(context),
_withTopNavSpace(
RecommendSectionsView(
feedController: widget.feedController,
categories: categories,
onOpenDrama: _openDrama,
onMore: _handleMore,
),
),
_withTopNavSpace(
AllDramasFilterGrid(
feedController: widget.feedController,
onOpenDrama: _openDrama,
onOpenFilter: widget.onOpenFilter,
),
],
),
),
],
),
),
],
),
),
Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
bottom: false,
child: RecommendTopNav(
selected: _tab,
onVideo: _tab == RecommendInnerTab.feed,
onSelected: (tab) => setState(() => _tab = tab),
onSearch: widget.onSearch,
),
),
),
],
),
);
}
@@ -129,88 +139,12 @@ class _RecommendPageState extends State<RecommendPage> {
);
}
Widget _header(AppLocalizations l10n) {
final items = [
(RecommendInnerTab.feed, l10n.recommendTopTab, 'recommend'),
(RecommendInnerTab.playlists, l10n.playlistTopTab, 'playlist'),
(RecommendInnerTab.allDramas, l10n.allDramasTopTab, 'all'),
];
return SizedBox(
key: const Key('recommend-top-nav'),
height: 56,
Widget _withTopNavSpace(Widget child) {
return SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.feedPageHorizontal,
),
child: Row(
children: [
Expanded(
child: Row(
children: [
for (final item in items)
Expanded(
child: _tabButton(
key: Key('recommend-tab-${item.$3}'),
label: item.$2,
selected: _tab == item.$1,
onTap: () => setState(() => _tab = item.$1),
),
),
],
),
),
const SizedBox(width: AppDimens.spaceSm),
IconButton(
key: const Key('recommend-search'),
tooltip: l10n.search,
onPressed: widget.onSearch,
iconSize: 24,
color: AppColors.textPrimary,
icon: const Icon(Icons.search_rounded),
),
],
),
),
);
}
Widget _tabButton({
required Key key,
required String label,
required bool selected,
required VoidCallback onTap,
}) {
return InkWell(
key: key,
onTap: onTap,
child: Padding(
padding: const EdgeInsets.only(right: AppDimens.spaceSm),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: selected
? AppColors.textPrimary
: AppColors.textTertiary,
fontSize: AppDimens.homePrimaryTabFont,
fontWeight: selected ? FontWeight.w700 : FontWeight.w400,
),
),
const SizedBox(height: 3),
Container(
width: selected ? 20 : 0,
height: 2,
decoration: BoxDecoration(
color: AppColors.textPrimary,
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
),
),
],
),
padding: const EdgeInsets.only(top: RecommendTopNav.height),
child: child,
),
);
}
+3
View File
@@ -0,0 +1,3 @@
// Author: fruit.com.cn
enum RecommendInnerTab { feed, playlists, allDramas }
@@ -0,0 +1,124 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../l10n/app_localizations.dart';
import '../recommend_types.dart';
const double _navHeight = 56;
const double _tabGap = 22;
const double _tabFont = 17;
const double _searchHitSize = 40;
const double _searchIconSize = 26;
class RecommendTopNav extends StatelessWidget {
const RecommendTopNav({
super.key,
required this.selected,
required this.onSelected,
required this.onSearch,
this.onVideo = false,
});
static const double height = _navHeight;
final RecommendInnerTab selected;
final ValueChanged<RecommendInnerTab> onSelected;
final VoidCallback onSearch;
final bool onVideo;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final items = [
(RecommendInnerTab.feed, l10n.recommendTopTab, 'recommend'),
(RecommendInnerTab.playlists, l10n.playlistTopTab, 'playlist'),
(RecommendInnerTab.allDramas, l10n.allDramasTopTab, 'all'),
];
final shadows = onVideo
? const [Shadow(color: AppColors.mediaBlack, blurRadius: 6)]
: null;
return SizedBox(
key: const Key('recommend-top-nav'),
height: height,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.feedPageHorizontal,
),
child: Stack(
alignment: Alignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
for (var index = 0; index < items.length; index++) ...[
if (index > 0) const SizedBox(width: _tabGap),
_tab(
key: Key('recommend-tab-${items[index].$3}'),
label: items[index].$2,
selected: selected == items[index].$1,
shadows: shadows,
onTap: () => onSelected(items[index].$1),
),
],
],
),
Align(
alignment: Alignment.centerRight,
child: SizedBox.square(
dimension: _searchHitSize,
child: IconButton(
key: const Key('recommend-search'),
tooltip: l10n.search,
onPressed: onSearch,
padding: EdgeInsets.zero,
constraints: const BoxConstraints.tightFor(
width: _searchHitSize,
height: _searchHitSize,
),
iconSize: _searchIconSize,
color: AppColors.textPrimary,
icon: const Icon(Icons.search_rounded),
),
),
),
],
),
),
);
}
Widget _tab({
required Key key,
required String label,
required bool selected,
required List<Shadow>? shadows,
required VoidCallback onTap,
}) {
return GestureDetector(
key: key,
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: SizedBox(
height: height,
child: Center(
child: Text(
label,
maxLines: 1,
style: TextStyle(
color: selected
? AppColors.textPrimary
: AppColors.textTabInactive,
fontSize: _tabFont,
fontWeight: selected ? FontWeight.w700 : FontWeight.w400,
letterSpacing: 0,
shadows: shadows,
),
),
),
),
);
}
}