feat(recommend): 上新板块改按后端语义码判断,顶部轮播改为无缝循环
flutter-ci / test (push) Failing after 30m39s

Drama 新增 tagCode,推荐页只看 NEW_ARRIVAL 这个码决定上新横滑,
不再依赖可变的标签文案;分类网格高度改为复用卡片的统一常量。
头部轮播改用虚拟无限页码,首尾相接且能反向滑,自动播放不再倒滑回第一张。
This commit is contained in:
Vicky authored and trae-solo-agent committed 2026-10-29 15:44:33 +08:00
1 parent c311985601
commit 3590cd0c55
5 files changed
+235 -73

No files matched your search

@@ -46,15 +46,25 @@ class _HomeHeroCarouselState extends State<HomeHeroCarousel> {
static const double _dotGap = 9;
static const double _ctaHeight = 38;
final PageController _controller = PageController();
/// 无限轮播:PageView 用虚拟大页码,取模映射到真实 banner,首尾天然相接。
static const int _basePage = 1000;
late final PageController _controller;
Timer? _timer;
int _page = 0;
int _index = 0;
bool _pressed = false;
bool _tickerEnabled = true;
/// 起始页码取 banner 数量的大倍数,向左右滑都不会到头。
static int _initialPageOf(int bannerCount) =>
bannerCount > 1 ? bannerCount * _basePage : 0;
@override
void initState() {
super.initState();
_page = _initialPageOf(widget.banners.length);
_controller = PageController(initialPage: _page);
WidgetsBinding.instance.addPostFrameCallback((_) => _syncTimer());
}
@@ -72,8 +82,11 @@ class _HomeHeroCarouselState extends State<HomeHeroCarousel> {
void didUpdateWidget(covariant HomeHeroCarousel oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.banners.length != widget.banners.length) {
if (_index >= widget.banners.length) _index = 0;
_syncTimer();
// 首屏 banner 是异步到的:列表从空变有之后要把页码挪到虚拟起点,
// 否则左滑第一下就到头了。
_page = _initialPageOf(widget.banners.length);
_index = 0;
_jumpToBasePage();
}
if (oldWidget.banners.length != widget.banners.length ||
oldWidget.active != widget.active) {
@@ -91,7 +104,8 @@ class _HomeHeroCarouselState extends State<HomeHeroCarousel> {
void _syncTimer() {
_timer?.cancel();
_timer = null;
final canAdvance = mounted &&
final canAdvance =
mounted &&
widget.active &&
_tickerEnabled &&
!_pressed &&
@@ -102,12 +116,16 @@ class _HomeHeroCarouselState extends State<HomeHeroCarousel> {
void _advance() {
if (!_controller.hasClients || widget.banners.length < 2) return;
final next = (_index + 1) % widget.banners.length;
_controller.animateToPage(
next,
duration: _pageDuration,
curve: Curves.easeOutCubic,
);
// 虚拟页码无限,直接翻下一页即可,不用在末尾跳回第 0 页。
_controller.nextPage(duration: _pageDuration, curve: Curves.easeOutCubic);
}
/// 把 PageView 静默挪到虚拟起点:视觉上是同一张图,不会闪。
void _jumpToBasePage() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || !_controller.hasClients) return;
if (_controller.page?.round() != _page) _controller.jumpToPage(_page);
});
}
@override
@@ -134,9 +152,17 @@ class _HomeHeroCarouselState extends State<HomeHeroCarousel> {
children: [
PageView.builder(
controller: _controller,
itemCount: widget.banners.length,
onPageChanged: (value) => setState(() => _index = value),
itemBuilder: (context, index) => _bannerPage(context, index),
// 数量大于 1 时用无限页码做无缝循环,取模映射到真实 banner。
itemCount: widget.banners.length > 1
? null
: widget.banners.length,
onPageChanged: (value) => setState(() {
_page = value;
final count = widget.banners.length;
_index = count == 0 ? 0 : value % count;
}),
itemBuilder: (context, page) =>
_bannerPage(context, page % widget.banners.length),
),
Positioned(left: 0, right: 0, bottom: 14, child: _dots(context)),
],
@@ -288,16 +314,18 @@ class _HeroCopy extends StatelessWidget {
color: AppColors.textPrimary.withValues(alpha: 0.95),
fontSize: 15,
fontWeight: FontWeight.w400,
shadows: const [
Shadow(color: Color(0x80000000), blurRadius: 8),
],
shadows: const [Shadow(color: Color(0x80000000), blurRadius: 8)],
),
),
const SizedBox(height: 20),
Row(
mainAxisSize: MainAxisSize.min,
children: [
_WatchButton(height: ctaHeight, label: l10n.watchNowAction, onTap: onWatch),
_WatchButton(
height: ctaHeight,
label: l10n.watchNowAction,
onTap: onWatch,
),
const SizedBox(width: 12),
_FavoriteCircle(size: ctaHeight, drama: drama),
],