feat(recommend): 新增推荐频道页并恢复首页沉浸式信息流
- tab1 推荐页改为接口分类频道:频道导航可横滑,选中频道自动滚动到可视区,内容区左右滑动切换分类 - 精选频道:顶部固定高度 banner 轮播图 + 接口 tag 筛选的上新短剧横滑 + 其余短剧双列网格;其他分类三列网格 - 分类内容支持懒加载、下拉刷新与上拉分页加载 - 卡片角标文案与背景色取自接口 tag/tagColor,短剧标题统一白色 - 恢复 tab0 为沉浸式全屏视频首页及其三 inner-tab 顶导(搜索/礼物入口) - l10n 新增原创出品、立即观看、上新、编辑精选、全N集等多语言文案
This commit is contained in:
1 parent
a3f3481f4f
commit
c027acc91f
22 files changed
+2018
-318
No files matched your search
@@ -0,0 +1,252 @@
|
||||
// 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user