Files
ReelShort/lib/pages/recommend/widgets/home_recommend_content.dart
T
Vicky c027acc91f feat(recommend): 新增推荐频道页并恢复首页沉浸式信息流
- tab1 推荐页改为接口分类频道:频道导航可横滑,选中频道自动滚动到可视区,内容区左右滑动切换分类
- 精选频道:顶部固定高度 banner 轮播图 + 接口 tag 筛选的上新短剧横滑 + 其余短剧双列网格;其他分类三列网格
- 分类内容支持懒加载、下拉刷新与上拉分页加载
- 卡片角标文案与背景色取自接口 tag/tagColor,短剧标题统一白色
- 恢复 tab0 为沉浸式全屏视频首页及其三 inner-tab 顶导(搜索/礼物入口)
- l10n 新增原创出品、立即观看、上新、编辑精选、全N集等多语言文案
2026-10-27 19:57:42 +08:00

253 lines
8.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Author: fruit.com.cn
import 'package:flutter/material.dart';
import '../../../core/data/models/drama.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/widgets/remote_image.dart';
import '../../../l10n/app_localizations.dart';
/// 设计稿(首页 1)推荐页内容区规格:
/// 海报卡 110×157、页边距 14、卡距 9、圆角 12。
const double _pageSide = 14;
const double _cardWidth = 110;
const double _cardCoverHeight = 157;
const double _cardGap = 9;
const double _cardRadius = 12;
/// 上新横滑卡板块。无板块标题,卡片左上角带「上新」角标。
/// 内层横向 ListView 会自行识别横向手势,不与外层纵向滚动冲突。
class HomeNewArrivalRow extends StatelessWidget {
const HomeNewArrivalRow({
super.key,
required this.dramas,
required this.onOpen,
});
final List<Drama> dramas;
final ValueChanged<Drama> onOpen;
@override
Widget build(BuildContext context) {
if (dramas.isEmpty) return const SizedBox.shrink();
return SizedBox(
key: const Key('home-new-arrival-row'),
height:
_cardCoverHeight + 8 + 20 + 2 + 16, // 封面 + 剧名行 + 集数行 + 余量
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: _pageSide),
itemCount: dramas.length,
separatorBuilder: (_, _) => const SizedBox(width: _cardGap),
itemBuilder: (context, index) => SizedBox(
width: _cardWidth,
child: HomePosterCard(
key: Key('home-new-arrival-${dramas[index].id}'),
drama: dramas[index],
badge: AppLocalizations.of(context)!.newArrivalBadge,
onTap: () => onOpen(dramas[index]),
),
),
),
);
}
}
/// 110×157 海报 + 卡下剧名/集数。
///
/// 封面左上角标签优先用接口返回的短剧标签(`drama.tag` + `drama.tagColor`);
/// 接口没有标签时,若传了 [badge](如「上新」)则退回显示该角标。
class HomePosterCard extends StatelessWidget {
const HomePosterCard({
super.key,
required this.drama,
required this.onTap,
this.badge,
});
final Drama drama;
final VoidCallback onTap;
final String? badge;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final lang = Localizations.localeOf(context).languageCode;
final tag = drama.localizedTag(lang);
final tagLabel = tag.isNotEmpty ? tag : (badge ?? '');
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(_cardRadius),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(_cardRadius),
child: AspectRatio(
aspectRatio: _cardWidth / _cardCoverHeight,
child: Stack(
fit: StackFit.expand,
children: [
RemoteImage(
drama.posterUrl,
fit: BoxFit.cover,
showFallbackIcon: true,
fallbackColor: AppColors.contentSurface,
),
if (tagLabel.isNotEmpty)
Positioned(
left: 8,
top: 8,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
decoration: BoxDecoration(
color: tag.isNotEmpty
? drama.tagRibbonColor(AppColors.brandRed)
: AppColors.brandRed,
borderRadius: BorderRadius.circular(6),
),
child: Text(
tagLabel,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: 11,
fontWeight: FontWeight.w600,
height: 1.1,
),
),
),
),
],
),
),
),
const SizedBox(height: 8),
Text(
drama.localizedTitle(lang),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.2,
),
),
const SizedBox(height: 2),
Text(
l10n.totalEpisodesShort(drama.episodeCount),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textTertiary,
fontSize: 12,
height: 1.2,
),
),
],
),
);
}
}
/// 编辑精选横幅:满宽背景图 + 左上小标签、大标题、副标题。
class HomeEditorPickBanner extends StatelessWidget {
const HomeEditorPickBanner({
super.key,
required this.drama,
required this.onOpen,
});
final Drama drama;
final ValueChanged<Drama> onOpen;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final lang = Localizations.localeOf(context).languageCode;
return Padding(
key: const Key('home-editor-pick'),
padding: const EdgeInsets.symmetric(horizontal: _pageSide),
child: SizedBox(
height: 72,
child: ClipRRect(
borderRadius: BorderRadius.circular(_cardRadius),
child: Stack(
fit: StackFit.expand,
children: [
RemoteImage(
drama.posterUrl,
fit: BoxFit.cover,
showFallbackIcon: true,
fallbackColor: AppColors.contentSurface,
),
const DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [Color(0xE6000000), Color(0x4D000000), Color(0x1A000000)],
),
),
),
Positioned.fill(
child: Material(
color: AppColors.transparent,
child: InkWell(onTap: () => onOpen(drama)),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 14),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.editorPick,
maxLines: 1,
style: TextStyle(
color: AppColors.textPrimary.withValues(alpha: 0.85),
fontSize: 11,
fontWeight: FontWeight.w500,
letterSpacing: 2,
),
),
const SizedBox(height: 4),
Text(
drama.localizedTitle(lang),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: 18,
fontWeight: FontWeight.w600,
height: 1.15,
),
),
const SizedBox(height: 2),
Text(
drama.localizedSynopsis(lang),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: AppColors.textPrimary.withValues(alpha: 0.8),
fontSize: 12,
),
),
],
),
),
],
),
),
),
);
}
}