From e16faa258ad1415575b71a3aeabef93a931d3ec4 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Thu, 22 Oct 2026 18:26:52 +0800 Subject: [PATCH] =?UTF-8?q?fix(home):=20=E4=BF=AE=E6=AD=A3=E5=B0=81?= =?UTF-8?q?=E9=9D=A2=E8=A7=92=E6=A0=87=E4=B8=8E=E9=A2=98=E6=9D=90=E6=A0=87?= =?UTF-8?q?=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/PROGRESS.md | 3 +- lib/core/theme/app_dimens.dart | 5 -- .../widgets/drama_cover_badge_ribbon.dart | 69 +++++++++++++++ lib/core/widgets/drama_genre_ribbon.dart | 88 ------------------- lib/pages/following/following_grid_card.dart | 26 ++++-- lib/pages/home/widgets/home_feed_card.dart | 74 +++++++++------- .../widgets/recommend_section_card.dart | 56 ++++++------ test/following/following_page_test.dart | 15 ++-- test/home/home_card_style_test.dart | 39 ++++---- test/recommend/recommend_sections_test.dart | 23 +++-- 10 files changed, 199 insertions(+), 199 deletions(-) create mode 100644 lib/core/widgets/drama_cover_badge_ribbon.dart delete mode 100644 lib/core/widgets/drama_genre_ribbon.dart diff --git a/docs/plans/PROGRESS.md b/docs/plans/PROGRESS.md index 3e34e83..3fa7245 100644 --- a/docs/plans/PROGRESS.md +++ b/docs/plans/PROGRESS.md @@ -1112,7 +1112,8 @@ $ flutter test ## 2026-10-22 首页布局对齐修正 - 一级分类改为页内切换 6 个列表,未选 16/w400 灰字、选中 20/w700 白字,无下划线;二级 chips 仅精选显示。 -- 四类短剧卡片移除底板/描边,封面统一 8 圆角,网格行距 10、封底到标题 6,题材移至右上角异形丝带。 +- 四类短剧卡片移除底板/描边,封面统一 8 圆角,网格行距 10、封底到标题 6;热剧/新剧/漫剧角标移至右上角异形丝带。 +- 标题下方恢复左对齐题材文字,使用 `|` 分隔、灰色小字,与封面状态角标保持独立。 - 首页搜索栏到一级、一级到二级、二级到网格间距为 8/8/10;非精选 tab 直接以 10 衔接网格。 - 播放量统一 spark 图标与 `w` 格式,整数无小数;首页、剧单、全部短剧、追剧共用。 - 全部短剧的地区、字幕、配音筛选 UI 已占位;以下接口待补:地区、字幕/配音筛选数据。 diff --git a/lib/core/theme/app_dimens.dart b/lib/core/theme/app_dimens.dart index 9f114d6..98533a3 100644 --- a/lib/core/theme/app_dimens.dart +++ b/lib/core/theme/app_dimens.dart @@ -92,13 +92,8 @@ abstract final class AppDimens { static const double cardGenreRibbonLeftInset = 8; static const double cardGenreRibbonMaxWidth = 86; static const double cardTagFont = 11; - static const double cardTagHeight = 18; - static const double cardTagRadius = 9; static const double cardTagHorizontalPadding = 6; static const double cardBadgeFont = 10; - static const double cardBadgeRadius = 3; - static const double cardBadgeHorizontalPadding = 6; - static const double cardBadgeVerticalPadding = 2; static const double cardCoverTitleGap = 6; static const double cardTitleTagGap = 6; static const double cardCoverAspectRatio = 5 / 6; diff --git a/lib/core/widgets/drama_cover_badge_ribbon.dart b/lib/core/widgets/drama_cover_badge_ribbon.dart new file mode 100644 index 0000000..95a5048 --- /dev/null +++ b/lib/core/widgets/drama_cover_badge_ribbon.dart @@ -0,0 +1,69 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; + +import '../theme/app_colors.dart'; +import '../theme/app_dimens.dart'; + +/// 封面右上角的斜边状态角标。 +class DramaCoverBadgeRibbon extends StatelessWidget { + const DramaCoverBadgeRibbon({ + super.key, + required this.label, + required this.color, + required this.keyPrefix, + }); + + final String label; + final Color color; + final String keyPrefix; + + @override + Widget build(BuildContext context) { + return ClipPath( + key: Key('drama-cover-badge-ribbon-$keyPrefix'), + clipper: const _SlantedRibbonClipper(), + child: Container( + constraints: const BoxConstraints( + maxWidth: AppDimens.cardGenreRibbonMaxWidth, + ), + height: AppDimens.cardGenreRibbonHeight, + padding: const EdgeInsets.only( + left: + AppDimens.cardTagHorizontalPadding + + AppDimens.cardGenreRibbonLeftInset, + right: AppDimens.cardTagHorizontalPadding, + ), + alignment: Alignment.center, + color: color, + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.textPrimary, + fontSize: AppDimens.cardBadgeFont, + fontWeight: FontWeight.w600, + ), + ), + ), + ); + } +} + +class _SlantedRibbonClipper extends CustomClipper { + const _SlantedRibbonClipper(); + + @override + Path getClip(Size size) { + return Path() + ..moveTo(AppDimens.cardGenreRibbonLeftInset, 0) + ..lineTo(size.width, 0) + ..lineTo(size.width, size.height) + ..lineTo(0, size.height) + ..close(); + } + + @override + bool shouldReclip(_SlantedRibbonClipper oldClipper) => false; +} diff --git a/lib/core/widgets/drama_genre_ribbon.dart b/lib/core/widgets/drama_genre_ribbon.dart deleted file mode 100644 index 93a42c8..0000000 --- a/lib/core/widgets/drama_genre_ribbon.dart +++ /dev/null @@ -1,88 +0,0 @@ -// Author: fruit.com.cn - -import 'package:flutter/material.dart'; - -import '../theme/app_colors.dart'; -import '../theme/app_dimens.dart'; - -const List _ribbonColors = [ - AppColors.badgeHot, - AppColors.badgeNew, - AppColors.badgeAnime, -]; - -/// 封面右上角的斜边题材丝带。 -class DramaGenreRibbon extends StatelessWidget { - const DramaGenreRibbon({ - super.key, - required this.tags, - required this.keyPrefix, - }); - - final List tags; - final String keyPrefix; - - @override - Widget build(BuildContext context) { - final visible = tags - .map((tag) => tag.trim()) - .where((tag) => tag.isNotEmpty) - .take(3) - .toList(growable: false); - if (visible.isEmpty) return const SizedBox.shrink(); - return Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - for (final (index, tag) in visible.indexed) ...[ - if (index > 0) const SizedBox(height: 2), - ClipPath( - key: Key('drama-genre-ribbon-$keyPrefix-$index'), - clipper: const _SlantedRibbonClipper(), - child: Container( - constraints: const BoxConstraints( - maxWidth: AppDimens.cardGenreRibbonMaxWidth, - ), - height: AppDimens.cardGenreRibbonHeight, - padding: const EdgeInsets.only( - left: - AppDimens.cardTagHorizontalPadding + - AppDimens.cardGenreRibbonLeftInset, - right: AppDimens.cardTagHorizontalPadding, - ), - alignment: Alignment.center, - color: _ribbonColors[index % _ribbonColors.length], - child: Text( - tag, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: AppColors.textPrimary, - fontSize: AppDimens.cardBadgeFont, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ], - ], - ); - } -} - -class _SlantedRibbonClipper extends CustomClipper { - const _SlantedRibbonClipper(); - - @override - Path getClip(Size size) { - return Path() - ..moveTo(AppDimens.cardGenreRibbonLeftInset, 0) - ..lineTo(size.width, 0) - ..lineTo(size.width, size.height) - ..lineTo(0, size.height) - ..close(); - } - - @override - bool shouldReclip(_SlantedRibbonClipper oldClipper) => false; -} diff --git a/lib/pages/following/following_grid_card.dart b/lib/pages/following/following_grid_card.dart index 7a7317e..0b78507 100644 --- a/lib/pages/following/following_grid_card.dart +++ b/lib/pages/following/following_grid_card.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import '../../core/formatting/count_format.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_dimens.dart'; -import '../../core/widgets/drama_genre_ribbon.dart'; import '../../core/widgets/remote_image.dart'; class FollowingGridCard extends StatelessWidget { @@ -95,12 +94,6 @@ class FollowingGridCard extends StatelessWidget { ), ), ), - if (!editing) - Positioned( - top: 0, - right: 0, - child: DramaGenreRibbon(tags: tags, keyPrefix: dramaId), - ), if (editing) Positioned( top: -6, @@ -135,6 +128,10 @@ class FollowingGridCard extends StatelessWidget { height: AppDimens.cardTitleLineHeight, ), ), + if (tags.isNotEmpty) ...[ + const SizedBox(height: AppDimens.cardTitleTagGap), + _tags(), + ], const SizedBox(height: AppDimens.spaceXs), Text( subtitle, @@ -164,4 +161,19 @@ class FollowingGridCard extends StatelessWidget { ), ); } + + Widget _tags() { + final visible = tags.take(3).toList(growable: false); + return Text( + visible.join(' | '), + key: Key('following-tags-$dramaId'), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.left, + style: const TextStyle( + color: AppColors.textTertiary, + fontSize: AppDimens.cardTagFont, + ), + ); + } } diff --git a/lib/pages/home/widgets/home_feed_card.dart b/lib/pages/home/widgets/home_feed_card.dart index b78f575..1f78dbb 100644 --- a/lib/pages/home/widgets/home_feed_card.dart +++ b/lib/pages/home/widgets/home_feed_card.dart @@ -6,7 +6,7 @@ import '../../../core/data/models/drama.dart'; import '../../../core/formatting/count_format.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; -import '../../../core/widgets/drama_genre_ribbon.dart'; +import '../../../core/widgets/drama_cover_badge_ribbon.dart'; import '../../../core/widgets/remote_image.dart'; import '../../../l10n/app_localizations.dart'; import 'home_drama_classification.dart'; @@ -63,6 +63,8 @@ class HomeFeedCard extends StatelessWidget { height: AppDimens.cardTitleLineHeight, ), ), + const SizedBox(height: AppDimens.cardTitleTagGap), + _tags(), ], ), ), @@ -92,15 +94,14 @@ class HomeFeedCard extends StatelessWidget { ), if (badge != null) Positioned( - top: AppDimens.spaceXs, - left: AppDimens.spaceXs, - child: _badgeChip(l10n, badge), + top: 0, + right: 0, + child: DramaCoverBadgeRibbon( + label: _badgeLabel(l10n, badge), + color: _badgeColor(badge), + keyPrefix: '${drama.id}-${badge.name}', + ), ), - Positioned( - top: 0, - right: 0, - child: DramaGenreRibbon(tags: _cardTags(), keyPrefix: drama.id), - ), Positioned( left: AppDimens.spaceXs, bottom: AppDimens.spaceXs, @@ -123,32 +124,20 @@ class HomeFeedCard extends StatelessWidget { return null; } - Widget _badgeChip(AppLocalizations l10n, HomeDramaBadge badge) { - final (label, color) = switch (badge) { - HomeDramaBadge.hot => (l10n.homeBadgeHot, AppColors.badgeHot), - HomeDramaBadge.newRelease => (l10n.homeBadgeNew, AppColors.badgeNew), - HomeDramaBadge.anime => (l10n.homeBadgeAnime, AppColors.badgeAnime), + String _badgeLabel(AppLocalizations l10n, HomeDramaBadge badge) { + return switch (badge) { + HomeDramaBadge.hot => l10n.homeBadgeHot, + HomeDramaBadge.newRelease => l10n.homeBadgeNew, + HomeDramaBadge.anime => l10n.homeBadgeAnime, + }; + } + + Color _badgeColor(HomeDramaBadge badge) { + return switch (badge) { + HomeDramaBadge.hot => AppColors.badgeHot, + HomeDramaBadge.newRelease => AppColors.badgeNew, + HomeDramaBadge.anime => AppColors.badgeAnime, }; - return Container( - key: Key('home-feed-badge-${badge.name}-${drama.id}'), - padding: const EdgeInsets.symmetric( - horizontal: AppDimens.cardBadgeHorizontalPadding, - vertical: AppDimens.cardBadgeVerticalPadding, - ), - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(AppDimens.cardBadgeRadius), - ), - child: Text( - label, - style: const TextStyle( - color: AppColors.navSelected, - fontSize: AppDimens.cardBadgeFont, - fontWeight: FontWeight.w700, - letterSpacing: 0, - ), - ), - ); } Widget _views() { @@ -182,6 +171,23 @@ class HomeFeedCard extends StatelessWidget { ); } + Widget _tags() { + final tags = _cardTags(); + if (tags.isEmpty) return const SizedBox.shrink(); + return Text( + tags.join(' | '), + key: Key('home-feed-tags-${drama.id}'), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.left, + style: const TextStyle( + color: AppColors.textTertiary, + fontSize: AppDimens.cardTagFont, + letterSpacing: 0, + ), + ); + } + List _cardTags() { final seen = {}; return [drama.genre, drama.category] diff --git a/lib/pages/recommend/widgets/recommend_section_card.dart b/lib/pages/recommend/widgets/recommend_section_card.dart index 5178976..fdaab54 100644 --- a/lib/pages/recommend/widgets/recommend_section_card.dart +++ b/lib/pages/recommend/widgets/recommend_section_card.dart @@ -6,7 +6,7 @@ import '../../../core/data/models/drama.dart'; import '../../../core/formatting/count_format.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; -import '../../../core/widgets/drama_genre_ribbon.dart'; +import '../../../core/widgets/drama_cover_badge_ribbon.dart'; import '../../../core/widgets/remote_image.dart'; import '../../../l10n/app_localizations.dart'; import '../../home/widgets/home_drama_classification.dart'; @@ -54,38 +54,14 @@ class RecommendSectionCard extends StatelessWidget { ), if (isHotDrama(drama)) Positioned( - top: AppDimens.spaceXs, - left: AppDimens.spaceXs, - child: Container( - key: Key('recommend-hot-$sectionKey-${drama.id}'), - padding: const EdgeInsets.symmetric( - horizontal: AppDimens.cardBadgeHorizontalPadding, - vertical: AppDimens.cardBadgeVerticalPadding, - ), - decoration: BoxDecoration( - color: AppColors.badgeHot, - borderRadius: BorderRadius.circular( - AppDimens.cardBadgeRadius, - ), - ), - child: Text( - l10n.homeBadgeHot, - style: const TextStyle( - color: AppColors.textPrimary, - fontSize: AppDimens.cardBadgeFont, - fontWeight: FontWeight.w700, - ), - ), + top: 0, + right: 0, + child: DramaCoverBadgeRibbon( + label: l10n.homeBadgeHot, + color: AppColors.badgeHot, + keyPrefix: '$sectionKey-${drama.id}-hot', ), ), - Positioned( - top: 0, - right: 0, - child: DramaGenreRibbon( - tags: _cardTags(), - keyPrefix: '$sectionKey-${drama.id}', - ), - ), ], ), ), @@ -123,6 +99,8 @@ class RecommendSectionCard extends StatelessWidget { height: AppDimens.cardTitleLineHeight, ), ), + const SizedBox(height: AppDimens.cardTitleTagGap), + _tags(), ], ), ), @@ -138,4 +116,20 @@ class RecommendSectionCard extends StatelessWidget { .take(3) .toList(growable: false); } + + Widget _tags() { + final tags = _cardTags(); + if (tags.isEmpty) return const SizedBox.shrink(); + return Text( + tags.join(' | '), + key: Key('recommend-tags-$sectionKey-${drama.id}'), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.left, + style: const TextStyle( + color: AppColors.textTertiary, + fontSize: AppDimens.cardTagFont, + ), + ); + } } diff --git a/test/following/following_page_test.dart b/test/following/following_page_test.dart index a8a130f..d7273dc 100644 --- a/test/following/following_page_test.dart +++ b/test/following/following_page_test.dart @@ -96,17 +96,14 @@ void main() { ), findsOneWidget, ); - final ribbon = find.byKey( - const Key('drama-genre-ribbon-the-signal-demo-0'), + final tags = tester.widget( + find.byKey(const Key('following-tags-the-signal-demo')), ); - expect(ribbon, findsOneWidget); - final ribbonText = tester.widget( - find.descendant(of: ribbon, matching: find.byType(Text)), - ); - expect(ribbonText.data, 'Suspense'); - expect(ribbonText.style?.fontSize, AppDimens.cardBadgeFont); - expect(ribbonText.style?.color, AppColors.textPrimary); + expect(tags.data, 'Suspense | Mystery'); + expect(tags.style?.fontSize, AppDimens.cardTagFont); + expect(tags.style?.color, AppColors.textTertiary); expect(tester.getTopLeft(title).dx, tester.getTopLeft(cover).dx); + expect(tester.getTopLeft(tags).dx, tester.getTopLeft(cover).dx); await tester.tap(find.byKey(const Key('following-edit'))); await tester.pumpAndSettle(); diff --git a/test/home/home_card_style_test.dart b/test/home/home_card_style_test.dart index 4a900dc..212e915 100644 --- a/test/home/home_card_style_test.dart +++ b/test/home/home_card_style_test.dart @@ -48,13 +48,10 @@ void main() { coverClip.borderRadius, BorderRadius.circular(AppDimens.cardCoverRadius), ); - final badge = find.byKey(const Key('home-feed-badge-anime-anime-card')); - expect(badge, findsOneWidget); - final badgeContainer = tester.widget(badge); - expect( - (badgeContainer.decoration! as BoxDecoration).borderRadius, - BorderRadius.circular(AppDimens.cardBadgeRadius), + final badge = find.byKey( + const Key('drama-cover-badge-ribbon-anime-card-anime'), ); + expect(badge, findsOneWidget); expect( tester .widget(find.descendant(of: badge, matching: find.byType(Text))) @@ -62,6 +59,14 @@ void main() { ?.fontSize, AppDimens.cardBadgeFont, ); + expect( + tester.getTopRight(badge).dx, + closeTo(tester.getTopRight(cover).dx, 1), + ); + expect( + tester.getTopRight(badge).dy, + closeTo(tester.getTopRight(cover).dy, 1), + ); expect(find.text('Anime'), findsOneWidget); expect(find.byKey(const Key('home-feed-views-anime-card')), findsOneWidget); final views = find.byKey(const Key('home-feed-views-anime-card')); @@ -85,15 +90,14 @@ void main() { expect(title.style?.fontSize, AppDimens.cardTitleFont); expect(title.style?.fontWeight, FontWeight.w500); expect(title.style?.height, AppDimens.cardTitleLineHeight); - final ribbon = find.byKey(const Key('drama-genre-ribbon-anime-card-0')); - expect(ribbon, findsOneWidget); - final ribbonText = tester.widget( - find.descendant(of: ribbon, matching: find.byType(Text)), + final tags = tester.widget( + find.byKey(const Key('home-feed-tags-anime-card')), ); - expect(ribbonText.data, 'Romance'); - expect(ribbonText.style?.fontSize, AppDimens.cardBadgeFont); - expect(ribbonText.style?.color, AppColors.textPrimary); + expect(tags.data, 'Romance | Urban'); + expect(tags.style?.fontSize, AppDimens.cardTagFont); + expect(tags.style?.color, AppColors.textTertiary); expect(tester.getTopLeft(title).dx, tester.getTopLeft(cover).dx); + expect(tester.getTopLeft(tags).dx, tester.getTopLeft(cover).dx); await tester.tap(card); expect(taps, 1); @@ -134,14 +138,17 @@ void main() { await tester.pumpAndSettle(); expect( - find.byKey(const Key('home-feed-badge-anime-anime')), + find.byKey(const Key('drama-cover-badge-ribbon-anime-anime')), findsOneWidget, ); expect( - find.byKey(const Key('home-feed-badge-newRelease-new')), + find.byKey(const Key('drama-cover-badge-ribbon-new-newRelease')), + findsOneWidget, + ); + expect( + find.byKey(const Key('drama-cover-badge-ribbon-hot-hot')), findsOneWidget, ); - expect(find.byKey(const Key('home-feed-badge-hot-hot')), findsOneWidget); }); } diff --git a/test/recommend/recommend_sections_test.dart b/test/recommend/recommend_sections_test.dart index d20b534..5aac69b 100644 --- a/test/recommend/recommend_sections_test.dart +++ b/test/recommend/recommend_sections_test.dart @@ -115,17 +115,24 @@ void main() { matching: find.byIcon(Icons.auto_awesome_rounded), ); expect(viewIcon, findsOneWidget); - final ribbon = find.byKey( - const Key('drama-genre-ribbon-hits-the-signal-demo-0'), + final badge = find.byKey( + const Key('drama-cover-badge-ribbon-hits-the-signal-demo-hot'), ); - expect(ribbon, findsOneWidget); - final ribbonText = tester.widget( - find.descendant(of: ribbon, matching: find.byType(Text)), + expect(badge, findsOneWidget); + final badgeText = tester.widget( + find.descendant(of: badge, matching: find.byType(Text)), ); - expect(ribbonText.data, 'Suspense'); - expect(ribbonText.style?.fontSize, AppDimens.cardBadgeFont); - expect(ribbonText.style?.color, AppColors.textPrimary); + expect(badgeText.data, 'Hit'); + expect(badgeText.style?.fontSize, AppDimens.cardBadgeFont); + expect(badgeText.style?.color, AppColors.textPrimary); + final tags = tester.widget( + find.byKey(const Key('recommend-tags-hits-the-signal-demo')), + ); + expect(tags.data, 'Suspense | Mystery'); + expect(tags.style?.fontSize, AppDimens.cardTagFont); + expect(tags.style?.color, AppColors.textTertiary); expect(tester.getTopLeft(cardTitle).dx, tester.getTopLeft(cover).dx); + expect(tester.getTopLeft(tags).dx, tester.getTopLeft(cover).dx); }); testWidgets('should_switch_inner_tabs_without_losing_grid_filter_state', (