merge: 重排全部短剧筛选与网格

This commit is contained in:
Zhou Weidong committed 2026-10-22 20:39:09 +08:00
commit 099d05064e
5 files changed
+170 -65

No files matched your search

+6
View File
@@ -1206,3 +1206,9 @@ $ flutter test
- 推荐、剧单、全部短剧改为共享 PageController 的横向 PageView,点击与滑动均驱动同一选中态。
- 三页使用 AutomaticKeepAliveClientMixin 保留视频、滚动位置和筛选状态;顶部导航固定在外层。
- `flutter analyze`:No issues found。
## 2026-10-22 全部短剧布局重排
- 四行筛选统一 28 高、8 行距、24 项目间距;选中 chip 改为 `#4A1E1E`、白字 13/w600、圆角 14。
- 最热/最新改为纯文字选中态,三横线筛选固定在右端;网格改为左右 12、列距 6、行距 12。
- 第三行筛选完整展示,卡片继续复用首页组件;`flutter analyze`:No issues found。
+1
View File
@@ -77,6 +77,7 @@ abstract final class AppColors {
static const contentSurface = Color(0xFF1C1C1E);
static const contentChip = Color(0xFF2C2C2E);
static const contentBorder = Color(0x1AFFFFFF);
static const filterChipSelected = Color(0xFF4A1E1E);
/// 「我的」页卡片与描边。
static const mineCard = Color(0xFF16161A);
+10 -2
View File
@@ -138,8 +138,16 @@ abstract final class AppDimens {
/// 全部短剧筛选与追剧双列。
static const double filterChipFont = 13;
static const double filterChipHeight = 30;
static const double filterChipRadius = 15;
static const double filterChipHeight = 28;
static const double filterChipRadius = 14;
static const double filterChipHorizontalPadding = 12;
static const double filterRowGap = 8;
static const double filterItemGap = 24;
static const double allDramasHorizontal = 12;
static const double allDramasGridGap = 6;
static const double allDramasGridRowGap = 12;
static const double allDramasFilterToGridGap = 12;
static const double allDramasFilterIcon = 26;
static const double followingTabFont = 16;
static const double followingGridAspectRatio = 0.5;
@@ -49,8 +49,8 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
builder: (context, constraints) {
final itemWidth =
(constraints.maxWidth -
AppDimens.feedPageHorizontal * 2 -
AppDimens.homeGridGap * 2) /
AppDimens.allDramasHorizontal * 2 -
AppDimens.allDramasGridGap * 2) /
3;
final itemExtent =
itemWidth / AppDimens.cardCoverAspectRatio +
@@ -85,14 +85,14 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
else
SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.feedPageHorizontal,
horizontal: AppDimens.allDramasHorizontal,
),
sliver: SliverGrid(
key: const Key('all-dramas-grid'),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: AppDimens.homeGridGap,
mainAxisSpacing: AppDimens.homeGridRowGap,
crossAxisSpacing: AppDimens.allDramasGridGap,
mainAxisSpacing: AppDimens.allDramasGridRowGap,
mainAxisExtent: itemExtent,
),
delegate: SliverChildBuilderDelegate(
@@ -131,9 +131,9 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
return Padding(
padding: const EdgeInsets.fromLTRB(
AppDimens.feedPageHorizontal,
AppDimens.spaceSm,
0,
AppDimens.feedPageHorizontal,
AppDimens.spaceMd,
AppDimens.allDramasFilterToGridGap,
),
child: Column(
key: const Key('all-dramas-filters'),
@@ -160,38 +160,7 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
onSelected: (value) => setState(() => _audio = value),
),
_genreRow(2, genres, l10n),
Row(
key: const Key('all-filter-row-3'),
children: [
Expanded(
child: Row(
children: [
_chip(
key: const Key('all-sort-hot'),
label: l10n.hot,
selected: !_newest,
onTap: () => setState(() => _newest = false),
),
const SizedBox(width: AppDimens.spaceSm),
_chip(
key: const Key('all-sort-new'),
label: l10n.newest,
selected: _newest,
onTap: () => setState(() => _newest = true),
),
],
),
),
IconButton(
key: const Key('all-dramas-filter-page'),
tooltip: l10n.filter,
onPressed: widget.onOpenFilter,
color: AppColors.textPrimary,
iconSize: AppDimens.tapTarget / 2,
icon: const Icon(Icons.filter_list_rounded),
),
],
),
_sortRow(l10n),
],
),
);
@@ -205,21 +174,26 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
}) {
return Padding(
key: Key('all-filter-row-$row'),
padding: const EdgeInsets.only(bottom: AppDimens.spaceSm),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (final option in options) ...[
_chip(
key: Key('all-filter-${option.id}'),
label: option.label,
selected: selected == option.id,
onTap: () => onSelected(option.id),
),
const SizedBox(width: AppDimens.spaceSm),
padding: const EdgeInsets.only(bottom: AppDimens.filterRowGap),
child: SizedBox(
height: AppDimens.filterChipHeight,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
for (var index = 0; index < options.length; index++) ...[
_chip(
key: Key('all-filter-${options[index].id}'),
label: options[index].label,
selected: selected == options[index].id,
onTap: () => onSelected(options[index].id),
),
if (index < options.length - 1)
const SizedBox(width: AppDimens.filterItemGap),
],
],
],
),
),
),
);
@@ -254,18 +228,93 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
borderRadius: BorderRadius.circular(AppDimens.filterChipRadius),
child: Container(
height: AppDimens.filterChipHeight,
padding: const EdgeInsets.symmetric(horizontal: AppDimens.spaceMd),
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.filterChipHorizontalPadding,
),
alignment: Alignment.center,
decoration: BoxDecoration(
color: selected ? AppColors.contentSurface : AppColors.transparent,
color: selected
? AppColors.filterChipSelected
: AppColors.transparent,
borderRadius: BorderRadius.circular(AppDimens.filterChipRadius),
),
child: Text(
label,
style: TextStyle(
color: selected ? AppColors.textPrimary : AppColors.textTertiary,
color: selected ? AppColors.textPrimary : AppColors.textTabInactive,
fontSize: AppDimens.filterChipFont,
fontWeight: selected ? FontWeight.w700 : FontWeight.w400,
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
height: 1,
),
),
),
);
}
Widget _sortRow(AppLocalizations l10n) {
return SizedBox(
key: const Key('all-filter-row-3'),
height: AppDimens.filterChipHeight,
child: Row(
children: [
_sortOption(
key: const Key('all-sort-hot'),
label: l10n.hot,
selected: !_newest,
onTap: () => setState(() => _newest = false),
),
const SizedBox(width: AppDimens.filterItemGap),
_sortOption(
key: const Key('all-sort-new'),
label: l10n.newest,
selected: _newest,
onTap: () => setState(() => _newest = true),
),
const Spacer(),
SizedBox.square(
dimension: AppDimens.allDramasFilterIcon,
child: IconButton(
key: const Key('all-dramas-filter-page'),
tooltip: l10n.filter,
padding: EdgeInsets.zero,
constraints: const BoxConstraints.tightFor(
width: AppDimens.allDramasFilterIcon,
height: AppDimens.allDramasFilterIcon,
),
onPressed: widget.onOpenFilter,
color: AppColors.textPrimary,
iconSize: AppDimens.allDramasFilterIcon,
icon: const Icon(Icons.filter_list_rounded),
),
),
],
),
);
}
Widget _sortOption({
required Key key,
required String label,
required bool selected,
required VoidCallback onTap,
}) {
return GestureDetector(
key: key,
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: SizedBox(
height: AppDimens.filterChipHeight,
child: Center(
child: Text(
label,
style: TextStyle(
color: selected
? AppColors.textPrimary
: AppColors.textTabInactive,
fontSize: AppDimens.filterChipFont,
fontWeight: selected ? FontWeight.w700 : FontWeight.w400,
height: 1,
),
),
),
),
@@ -273,7 +322,6 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
}
List<({String id, String label})> _availableGenres(AppLocalizations l10n) {
final source = widget.feedController.dramas;
final definitions = [
(id: 'reversal', label: l10n.reversalFilter, terms: _reversalTerms),
(id: 'uplifting', label: l10n.homeTagUplifting, terms: <String>[]),
@@ -282,11 +330,6 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
(id: 'angst', label: l10n.homeTagAngst, terms: <String>[]),
];
return definitions
.where(
(definition) => source.any(
(drama) => _matchesGenre(drama, definition.id, definition.terms),
),
)
.map((definition) => (id: definition.id, label: definition.label))
.toList(growable: false);
}
@@ -237,14 +237,49 @@ void main() {
await tester.tap(find.byKey(const Key('recommend-tab-all')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('all-dramas-grid')), findsOneWidget);
final grid = tester.widget<SliverGrid>(
find.byKey(const Key('all-dramas-grid')),
);
final delegate =
grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount;
expect(delegate.crossAxisCount, 3);
expect(delegate.crossAxisSpacing, AppDimens.allDramasGridGap);
expect(delegate.mainAxisSpacing, AppDimens.allDramasGridRowGap);
for (var row = 0; row < 4; row++) {
expect(find.byKey(Key('all-filter-row-$row')), findsOneWidget);
}
expect(find.byKey(const Key('all-filter-local')), findsOneWidget);
expect(find.byKey(const Key('all-filter-subtitles')), findsOneWidget);
for (final key in [
'all-filter-reversal',
'all-filter-uplifting',
'all-filter-modern',
'all-filter-sweet',
'all-filter-angst',
]) {
expect(find.byKey(Key(key)), findsOneWidget);
}
await tester.tap(find.byKey(const Key('all-filter-local')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('all-dramas-empty')), findsOneWidget);
expect(_textStyle(tester, 'Local').fontWeight, FontWeight.w600);
expect(_textStyle(tester, 'Local').color, AppColors.textPrimary);
final selectedChip = tester.widget<Container>(
find
.descendant(
of: find.byKey(const Key('all-filter-local')),
matching: find.byType(Container),
)
.first,
);
expect(
(selectedChip.decoration! as BoxDecoration).color,
AppColors.filterChipSelected,
);
expect(
tester.getSize(find.byKey(const Key('all-filter-local'))).height,
AppDimens.filterChipHeight,
);
await tester.tap(find.byKey(const Key('all-filter-all')).first);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('all-sort-new')));
@@ -265,6 +300,18 @@ void main() {
(chipContainer.decoration! as BoxDecoration).borderRadius,
BorderRadius.circular(AppDimens.filterChipRadius),
);
final filterButton = find.byKey(const Key('all-dramas-filter-page'));
expect(
tester.getSize(filterButton),
const Size.square(AppDimens.allDramasFilterIcon),
);
expect(
tester.getRect(filterButton).right,
closeTo(
tester.getRect(find.byKey(const Key('all-filter-row-3'))).right,
0.1,
),
);
await tester.tap(find.byKey(const Key('recommend-tab-playlist')));
await tester.pumpAndSettle();