feat(flutter): 剧场页按参考图重做

This commit is contained in:
Zhou Weidong committed 2026-10-20 13:24:21 +08:00
1 parent 2abb8c7032
commit 5a51af4264
13 files changed
+499 -258

No files matched your search

+188 -51
View File
@@ -22,6 +22,16 @@ import 'widgets/discover_header.dart';
import 'widgets/discover_search_results.dart';
import 'widgets/drama_grid.dart';
const Color _filterIconColor = Color(0xFF7B61FF);
const Color _newIconColor = Color(0xFF14B8A6);
const double _toolbarHeight = 36;
const double _toolbarGap = 10;
const double _toolbarRadius = 10;
const double _categoryTabHeight = 44;
const double _categoryTabGap = 20;
const double _categorySelectedFontSize = 16;
const double _categoryFontSize = 15;
/// K28-A2 剧场页:搜索、分类/语言 chips 与双列剧库。
class DiscoverPage extends StatefulWidget {
const DiscoverPage({
@@ -96,7 +106,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
focusNode: logic.searchFocus,
onChanged: (_) => logic.onSearchChanged(),
loading: logic.searchLoading,
onProfile: _openProfile,
onIdentify: () => _showComingSoon(l10n),
),
Expanded(child: _feed(l10n, categories)),
if (!embedded) _bottomNav(l10n),
@@ -174,7 +184,8 @@ class _DiscoverPageState extends State<DiscoverPage> {
? categories.feed.dramas
: categories.dramasOf(logic.selectedCategoryId!);
return [
_categoryChips(l10n, categories),
_categoryTabs(l10n, categories),
_toolbar(l10n, categories),
if (dramas.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 40),
@@ -190,73 +201,199 @@ class _DiscoverPageState extends State<DiscoverPage> {
];
}
Widget _categoryChips(AppLocalizations l10n, CategoryController categories) {
return Padding(
padding: const EdgeInsets.fromLTRB(
AppDimens.pageHorizontal,
AppDimens.spaceMd,
AppDimens.pageHorizontal,
0,
Widget _categoryTabs(AppLocalizations l10n, CategoryController categories) {
return SizedBox(
key: const Key('theater-category-chips'),
height: _categoryTabHeight,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 14),
itemCount: categories.categories.length + 1,
separatorBuilder: (_, _) => const SizedBox(width: _categoryTabGap),
itemBuilder: (context, index) {
if (index == 0) {
return _categoryTab(
key: const Key('theater-category-all'),
label: l10n.all,
selected: logic.selectedCategoryId == null,
onTap: () => logic.selectCategory(null),
);
}
final category = categories.categories[index - 1];
return _categoryTab(
key: Key('theater-category-${category.id}'),
label: category.name,
selected: logic.selectedCategoryId == category.id,
onTap: () => logic.selectCategory(category.id),
);
},
),
child: Wrap(
key: const Key('theater-category-chips'),
spacing: 10,
runSpacing: 10,
children: [
_filterChip(
key: const Key('theater-category-all'),
label: l10n.all,
selected: logic.selectedCategoryId == null,
onSelected: () => logic.selectCategory(null),
);
}
Widget _categoryTab({
required Key key,
required String label,
required bool selected,
required VoidCallback onTap,
}) {
return GestureDetector(
key: key,
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Center(
child: Text(
label,
maxLines: 1,
style: TextStyle(
color: selected ? AppColors.onSurface : AppColors.muted,
fontSize: selected ? _categorySelectedFontSize : _categoryFontSize,
fontWeight: selected ? FontWeight.w800 : FontWeight.w500,
),
for (final category in categories.categories)
_filterChip(
key: Key('theater-category-${category.id}'),
label: category.name,
selected: logic.selectedCategoryId == category.id,
onSelected: () => logic.selectCategory(category.id),
),
),
);
}
Widget _toolbar(AppLocalizations l10n, CategoryController categories) {
return Padding(
padding: const EdgeInsets.fromLTRB(14, 10, 14, 12),
child: Row(
children: [
Expanded(
child: _toolButton(
key: const Key('theater-toolbar-filter'),
icon: Icons.tune_rounded,
color: _filterIconColor,
label: l10n.filter,
onTap: () => _openFilterSheet(l10n, categories),
),
),
const SizedBox(width: _toolbarGap),
Expanded(
child: _toolButton(
key: const Key('theater-toolbar-ranking'),
icon: Icons.local_fire_department_rounded,
color: AppColors.blue,
label: l10n.rankingTab,
onTap: () => _showComingSoon(l10n),
),
),
const SizedBox(width: _toolbarGap),
Expanded(
child: _toolButton(
key: const Key('theater-toolbar-new'),
icon: Icons.play_circle_fill_rounded,
color: _newIconColor,
label: l10n.newDramas,
onTap: () => _showComingSoon(l10n),
),
),
const SizedBox(width: _toolbarGap),
Expanded(
child: _toolButton(
key: const Key('theater-toolbar-reserve'),
icon: Icons.calendar_month_rounded,
color: AppColors.blue,
label: l10n.reserve,
onTap: () => _showComingSoon(l10n),
),
),
],
),
);
}
Widget _filterChip({
Widget _toolButton({
required Key key,
required IconData icon,
required Color color,
required String label,
required bool selected,
required VoidCallback onSelected,
required VoidCallback onTap,
}) {
return ChoiceChip(
key: key,
label: Text(label),
selected: selected,
showCheckmark: false,
onSelected: (_) => onSelected(),
backgroundColor: AppColors.card,
selectedColor: AppColors.blue,
side: BorderSide(color: selected ? AppColors.blue : AppColors.border),
labelStyle: TextStyle(
color: selected ? AppColors.onPrimary : AppColors.onSurface,
fontSize: AppDimens.caption,
fontWeight: FontWeight.w700,
return Material(
color: AppColors.card,
borderRadius: BorderRadius.circular(_toolbarRadius),
child: InkWell(
key: key,
onTap: onTap,
borderRadius: BorderRadius.circular(_toolbarRadius),
child: Container(
height: _toolbarHeight,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(_toolbarRadius),
border: Border.all(color: AppColors.border),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 16, color: color),
const SizedBox(width: 4),
Flexible(
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.onSurface,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
shape: const StadiumBorder(),
);
}
Future<void> _openFilterSheet(
AppLocalizations l10n,
CategoryController categories,
) async {
await showModalBottomSheet<void>(
context: context,
backgroundColor: AppColors.card,
showDragHandle: true,
builder: (sheetContext) => SafeArea(
child: ListView(
key: const Key('theater-filter-sheet'),
shrinkWrap: true,
padding: const EdgeInsets.only(bottom: 12),
children: [
ListTile(
key: const Key('theater-filter-option-all'),
title: Text(l10n.all),
onTap: () {
Navigator.pop(sheetContext);
logic.selectCategory(null);
},
),
for (final category in categories.categories)
ListTile(
key: Key('theater-filter-option-${category.id}'),
title: Text(category.name),
onTap: () {
Navigator.pop(sheetContext);
logic.selectCategory(category.id);
},
),
],
),
),
);
}
void _showComingSoon(AppLocalizations l10n) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.comingSoon)));
}
Widget _block(Widget child) =>
SizedBox(height: 280, child: Center(child: child));
void _openProfile() {
final callback = widget.callbacks.onProfile;
if (callback != null) {
callback();
return;
}
_act(DiscoverPageAction.my);
}
void _act(DiscoverPageAction action) {
logic.act(
context,