fix(home): 调整首页分类与卡片标签样式

This commit is contained in:
Zhou Weidong committed 2026-10-22 18:22:05 +08:00
1 parent 7446431511
commit 4f6116896f
14 files changed
+251 -117

No files matched your search

+8
View File
@@ -7,3 +7,11 @@ String formatInteractionCount(int count) {
if (count < 10000) return '$count';
return '${(count / 10000).toStringAsFixed(1)}万';
}
/// 播放量展示:以 w 为单位,整数不保留小数,非整数保留一位。
String formatPlayCount(int count) {
if (count < 10000) return '$count';
final value = count / 10000;
if (value == value.roundToDouble()) return '${value.toInt()}w';
return '${value.toStringAsFixed(1)}w';
}
+7 -2
View File
@@ -81,11 +81,16 @@ abstract final class AppDimens {
static const double homeGridGap = 6;
static const double homeGridRowGap = 18;
static const double homeCardRadius = 6;
static const double cardCoverRadius = 8;
static const double homeCardPadding = 8;
static const double cardTitleFont = 15;
static const double cardTitleLineHeight = 1.3;
static const double cardViewsFont = 11;
static const double cardViewsIcon = 11;
static const double cardGenreRibbonHeight = 20;
static const double cardGenreRibbonRadius = 10;
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;
@@ -98,8 +103,8 @@ abstract final class AppDimens {
static const double cardTitleTagGap = 6;
static const double cardCoverAspectRatio = 5 / 6;
static const double homeCardBodyExtent = 84;
static const double homePrimaryTabFont = 15;
static const double homePrimaryTabSelectedFont = 18;
static const double homePrimaryTabFont = 16;
static const double homePrimaryTabSelectedFont = 20;
static const double homePrimaryRowHeight = 30;
static const Duration homePrimaryTabAnimationDuration = Duration(
milliseconds: 180,
+88
View File
@@ -0,0 +1,88 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_dimens.dart';
const List<Color> _ribbonColors = [
AppColors.badgeHot,
AppColors.badgeNew,
AppColors.badgeAnime,
];
/// 封面右上角的斜边题材丝带。
class DramaGenreRibbon extends StatelessWidget {
const DramaGenreRibbon({
super.key,
required this.tags,
required this.keyPrefix,
});
final List<String> 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<Path> {
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;
}
+11 -23
View File
@@ -5,6 +5,7 @@ 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 {
@@ -37,7 +38,7 @@ class FollowingGridCard extends StatelessWidget {
Widget build(BuildContext context) {
return InkWell(
key: Key('following-card-$dramaId'),
borderRadius: BorderRadius.circular(AppDimens.homeCardRadius),
borderRadius: BorderRadius.circular(AppDimens.cardCoverRadius),
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -52,7 +53,7 @@ class FollowingGridCard extends StatelessWidget {
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(
AppDimens.homeCardRadius,
AppDimens.cardCoverRadius,
),
child: RemoteImage(
posterUrl,
@@ -78,12 +79,12 @@ class FollowingGridCard extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.play_arrow_rounded,
Icons.auto_awesome_rounded,
color: AppColors.textPrimary,
size: AppDimens.cardViewsIcon,
),
Text(
formatInteractionCount(viewCount),
formatPlayCount(viewCount),
key: Key('following-views-$dramaId'),
style: const TextStyle(
color: AppColors.textPrimary,
@@ -94,6 +95,12 @@ class FollowingGridCard extends StatelessWidget {
),
),
),
if (!editing)
Positioned(
top: 0,
right: 0,
child: DramaGenreRibbon(tags: tags, keyPrefix: dramaId),
),
if (editing)
Positioned(
top: -6,
@@ -128,10 +135,6 @@ class FollowingGridCard extends StatelessWidget {
height: AppDimens.cardTitleLineHeight,
),
),
if (tags.isNotEmpty) ...[
const SizedBox(height: AppDimens.cardTitleTagGap),
_tags(),
],
const SizedBox(height: AppDimens.spaceXs),
Text(
subtitle,
@@ -161,19 +164,4 @@ 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,
),
);
}
}
+1 -1
View File
@@ -339,7 +339,7 @@ List<String> _dramaTags(Drama? drama) {
.expand((value) => value.split(_tagSplitter))
.map((value) => value.trim())
.where((value) => value.isNotEmpty && seen.add(value))
.take(2)
.take(3)
.toList(growable: false);
}
+36 -45
View File
@@ -6,6 +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/remote_image.dart';
import '../../../l10n/app_localizations.dart';
import 'home_drama_classification.dart';
@@ -33,7 +34,7 @@ class HomeFeedCard extends StatelessWidget {
key: Key('home-feed-card-${drama.id}'),
color: AppColors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(AppDimens.homeCardRadius),
borderRadius: BorderRadius.circular(AppDimens.cardCoverRadius),
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -62,8 +63,6 @@ class HomeFeedCard extends StatelessWidget {
height: AppDimens.cardTitleLineHeight,
),
),
const SizedBox(height: AppDimens.cardTitleTagGap),
_tags(),
],
),
),
@@ -76,30 +75,39 @@ class HomeFeedCard extends StatelessWidget {
Widget _cover(AppLocalizations l10n) {
final badge = _badge();
return AspectRatio(
aspectRatio: AppDimens.cardCoverAspectRatio,
child: Stack(
fit: StackFit.expand,
children: [
RemoteImage(
drama.posterUrl,
key: Key('home-feed-cover-${drama.id}'),
fit: BoxFit.cover,
showFallbackIcon: true,
fallbackColor: AppColors.contentSurface,
),
if (badge != null)
Positioned(
top: AppDimens.spaceXs,
left: AppDimens.spaceXs,
child: _badgeChip(l10n, badge),
return ClipRRect(
key: Key('home-feed-cover-clip-${drama.id}'),
borderRadius: BorderRadius.circular(AppDimens.cardCoverRadius),
child: AspectRatio(
aspectRatio: AppDimens.cardCoverAspectRatio,
child: Stack(
fit: StackFit.expand,
children: [
RemoteImage(
drama.posterUrl,
key: Key('home-feed-cover-${drama.id}'),
fit: BoxFit.cover,
showFallbackIcon: true,
fallbackColor: AppColors.contentSurface,
),
Positioned(
left: AppDimens.spaceXs,
bottom: AppDimens.spaceXs,
child: _views(),
),
],
if (badge != null)
Positioned(
top: AppDimens.spaceXs,
left: AppDimens.spaceXs,
child: _badgeChip(l10n, badge),
),
Positioned(
top: 0,
right: 0,
child: DramaGenreRibbon(tags: _cardTags(), keyPrefix: drama.id),
),
Positioned(
left: AppDimens.spaceXs,
bottom: AppDimens.spaceXs,
child: _views(),
),
],
),
),
);
}
@@ -155,13 +163,13 @@ class HomeFeedCard extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.play_arrow_rounded,
Icons.auto_awesome_rounded,
size: AppDimens.cardViewsIcon,
color: AppColors.navSelected,
),
const SizedBox(width: 1),
Text(
formatInteractionCount(drama.viewCount),
formatPlayCount(drama.viewCount),
style: const TextStyle(
color: AppColors.navSelected,
fontSize: AppDimens.cardViewsFont,
@@ -174,23 +182,6 @@ 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<String> _cardTags() {
final seen = <String>{};
return [drama.genre, drama.category]
+5 -1
View File
@@ -78,7 +78,11 @@ class HomeGridHeader extends StatelessWidget {
key: const Key('home-grid-header'),
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [_searchRow(l10n), _primaryRow(l10n), _tagRow(l10n)],
children: [
_searchRow(l10n),
_primaryRow(l10n),
if (selectedPrimary == HomePrimaryTab.featured) _tagRow(l10n),
],
);
}
@@ -6,6 +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/remote_image.dart';
import '../../../l10n/app_localizations.dart';
import '../../home/widgets/home_drama_classification.dart';
@@ -31,7 +32,7 @@ class RecommendSectionCard extends StatelessWidget {
width: AppDimens.recommendCardWidth,
child: InkWell(
key: Key('recommend-card-$sectionKey-${drama.id}'),
borderRadius: BorderRadius.circular(AppDimens.homeCardRadius),
borderRadius: BorderRadius.circular(AppDimens.cardCoverRadius),
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -39,7 +40,7 @@ class RecommendSectionCard extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(AppDimens.homeCardRadius),
borderRadius: BorderRadius.circular(AppDimens.cardCoverRadius),
child: AspectRatio(
aspectRatio: AppDimens.cardCoverAspectRatio,
child: Stack(
@@ -77,6 +78,14 @@ class RecommendSectionCard extends StatelessWidget {
),
),
),
Positioned(
top: 0,
right: 0,
child: DramaGenreRibbon(
tags: _cardTags(),
keyPrefix: '$sectionKey-${drama.id}',
),
),
],
),
),
@@ -88,12 +97,12 @@ class RecommendSectionCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start,
children: [
const Icon(
Icons.play_arrow_rounded,
Icons.auto_awesome_rounded,
color: AppColors.textPrimary,
size: AppDimens.cardViewsIcon,
),
Text(
formatInteractionCount(drama.viewCount),
formatPlayCount(drama.viewCount),
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: AppDimens.cardViewsFont,
@@ -114,31 +123,12 @@ class RecommendSectionCard extends StatelessWidget {
height: AppDimens.cardTitleLineHeight,
),
),
if (_cardTags().isNotEmpty) ...[
const SizedBox(height: AppDimens.cardTitleTagGap),
_tags(),
],
],
),
),
);
}
Widget _tags() {
final tags = _cardTags();
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,
),
);
}
List<String> _cardTags() {
final seen = <String>{};
return [drama.genre, drama.category]