From 3590cd0c55f3d6b5c0e0823e67b420b812224b3a Mon Sep 17 00:00:00 2001 From: Vicky <10212746+haifeng-jiang@user.noreply.gitee.com> Date: Thu, 29 Oct 2026 15:44:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(recommend):=20=E4=B8=8A=E6=96=B0=E6=9D=BF?= =?UTF-8?q?=E5=9D=97=E6=94=B9=E6=8C=89=E5=90=8E=E7=AB=AF=E8=AF=AD=E4=B9=89?= =?UTF-8?q?=E7=A0=81=E5=88=A4=E6=96=AD=EF=BC=8C=E9=A1=B6=E9=83=A8=E8=BD=AE?= =?UTF-8?q?=E6=92=AD=E6=94=B9=E4=B8=BA=E6=97=A0=E7=BC=9D=E5=BE=AA=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drama 新增 tagCode,推荐页只看 NEW_ARRIVAL 这个码决定上新横滑, 不再依赖可变的标签文案;分类网格高度改为复用卡片的统一常量。 头部轮播改用虚拟无限页码,首尾相接且能反向滑,自动播放不再倒滑回第一张。 --- lib/core/data/models/drama.dart | 21 ++++- .../recommend/recommend_discover_page.dart | 76 +++++++---------- .../recommend/widgets/home_hero_carousel.dart | 62 ++++++++++---- test/recommend/home_hero_carousel_test.dart | 83 +++++++++++++++++++ test/recommend/recommend_sections_test.dart | 66 +++++++++++++-- 5 files changed, 235 insertions(+), 73 deletions(-) create mode 100644 test/recommend/home_hero_carousel_test.dart diff --git a/lib/core/data/models/drama.dart b/lib/core/data/models/drama.dart index c02e6dd..8dc16b9 100644 --- a/lib/core/data/models/drama.dart +++ b/lib/core/data/models/drama.dart @@ -102,6 +102,7 @@ class Drama { this.synopsisLocales = const {}, this.tag = '', this.tagLocales = const {}, + this.tagCode = '', this.tagColor = '', this.category = '', this.categoryLocales = const {}, @@ -127,6 +128,9 @@ class Drama { final Map synopsisLocales; final String tag; final Map tagLocales; + + /// 标签语义码(如 `NEW_ARRIVAL`)。文案会随语言变,语义判断只认这个码。 + final String tagCode; final String tagColor; final String posterUrl; final List episodes; @@ -172,6 +176,7 @@ class Drama { synopsisLocales: text.synopsisLocales, tag: text.tag, tagLocales: text.tagLocales, + tagCode: (json['tagCode'] as String? ?? '').trim(), tagColor: json['tagColor'] as String? ?? '', posterUrl: json['cover'] as String? ?? '', episodes: preview is Map @@ -204,6 +209,7 @@ class Drama { synopsisLocales: text.synopsisLocales, tag: text.tag, tagLocales: text.tagLocales, + tagCode: (json['tagCode'] as String? ?? '').trim(), tagColor: json['tagColor'] as String? ?? '', posterUrl: json['cover'] as String? ?? '', category: text.category, @@ -315,7 +321,10 @@ class _DramaText { final category = _firstText(categoryLocales, json['category'], types); return _DramaText( title: _firstText(titleLocales, json['title']), - synopsis: _firstText(synopsisLocales, json['synopsis'] ?? json['description']), + synopsis: _firstText( + synopsisLocales, + json['synopsis'] ?? json['description'], + ), titleLocales: titleLocales, synopsisLocales: synopsisLocales, tag: _firstText(tagLocales, json['tag']), @@ -328,7 +337,11 @@ class _DramaText { } } -String _firstText(Map locales, Object? fallback, [List>? types]) { +String _firstText( + Map locales, + Object? fallback, [ + List>? types, +]) { final chinese = locales['zh'] ?? ''; if (chinese.isNotEmpty) return chinese; if (types != null && types.isNotEmpty) { @@ -339,7 +352,9 @@ String _firstText(Map locales, Object? fallback, [List> types, Object? fallback) { - final names = types.map((item) => item['zh'] ?? '').where((value) => value.isNotEmpty); + final names = types + .map((item) => item['zh'] ?? '') + .where((value) => value.isNotEmpty); final joined = names.join(' / '); if (joined.isNotEmpty) return joined; return fallback is String ? fallback : ''; diff --git a/lib/pages/recommend/recommend_discover_page.dart b/lib/pages/recommend/recommend_discover_page.dart index e9ebe34..ce9a17f 100644 --- a/lib/pages/recommend/recommend_discover_page.dart +++ b/lib/pages/recommend/recommend_discover_page.dart @@ -138,8 +138,7 @@ class _RecommendDiscoverPageState extends State } } - Future _openDrama(Drama drama) => - playDramaFromCatalog(context, drama); + Future _openDrama(Drama drama) => playDramaFromCatalog(context, drama); @override Widget build(BuildContext context) { @@ -163,9 +162,7 @@ class _RecommendDiscoverPageState extends State Positioned.fill( child: categoryList.isEmpty ? const Center( - child: CircularProgressIndicator( - color: AppColors.brandRed, - ), + child: CircularProgressIndicator(color: AppColors.brandRed), ) : PageView.builder( key: const Key('recommend-channel-pager'), @@ -184,7 +181,8 @@ class _RecommendDiscoverPageState extends State carouselActive: widget.active && index == selectedIndex, mediaTop: mediaTop, onRequest: _ensureCategory, - onRefresh: () => _refreshCategory(category.id, index == 0), + onRefresh: () => + _refreshCategory(category.id, index == 0), onOpenDrama: _openDrama, ); }, @@ -294,11 +292,7 @@ class _ChannelFeedState extends State<_ChannelFeed> List newArrivals = const []; List rest = dramas; if (widget.isFeatured && dramas.isNotEmpty) { - final lang = Localizations.localeOf(context).languageCode; - final badge = AppLocalizations.of(context)!.newArrivalBadge; - newArrivals = dramas - .where((drama) => _isNewArrivalTag(drama, lang, badge)) - .toList(growable: false); + newArrivals = dramas.where(isNewArrivalTag).toList(growable: false); if (newArrivals.isNotEmpty) { final newIds = newArrivals.map((drama) => drama.id).toSet(); rest = dramas @@ -326,7 +320,9 @@ class _ChannelFeedState extends State<_ChannelFeed> hasScrollBody: false, child: Padding( padding: EdgeInsets.only( - top: showCarousel ? 0 : widget.mediaTop + RecommendChannelNav.height, + top: showCarousel + ? 0 + : widget.mediaTop + RecommendChannelNav.height, ), child: Center(child: _placeholder(slot.loading)), ), @@ -334,12 +330,11 @@ class _ChannelFeedState extends State<_ChannelFeed> ); } else if (widget.isFeatured) { // 无轮播图时内容需避开顶部悬浮导航。 - final contentTop = - showCarousel ? 24.0 : widget.mediaTop + RecommendChannelNav.height + 16; + final contentTop = showCarousel + ? 24.0 + : widget.mediaTop + RecommendChannelNav.height + 16; if (newArrivals.isNotEmpty) { - sliverList.add( - SliverToBoxAdapter(child: SizedBox(height: contentTop)), - ); + sliverList.add(SliverToBoxAdapter(child: SizedBox(height: contentTop))); sliverList.add( SliverToBoxAdapter( child: HomeNewArrivalRow( @@ -348,9 +343,7 @@ class _ChannelFeedState extends State<_ChannelFeed> ), ), ); - sliverList.add( - const SliverToBoxAdapter(child: SizedBox(height: 28)), - ); + sliverList.add(const SliverToBoxAdapter(child: SizedBox(height: 28))); } sliverList.add( _gridSliver( @@ -363,7 +356,9 @@ class _ChannelFeedState extends State<_ChannelFeed> sliverList.add( _gridSliver( dramas, - topPadding: showCarousel ? 20 : widget.mediaTop + RecommendChannelNav.height + 16, + topPadding: showCarousel + ? 20 + : widget.mediaTop + RecommendChannelNav.height + 16, crossAxisCount: 3, ), ); @@ -427,7 +422,6 @@ class _ChannelFeedState extends State<_ChannelFeed> const pageSide = 14.0; const cardGap = 9.0; const rowGap = 16.0; - const cardBodyExtent = 44.0; // 封面下间距 + 剧名行 + 集数行 // 两列时卡更大,卡间距略放宽。 final spacing = crossAxisCount == 2 ? 12.0 : cardGap; return SliverPadding( @@ -437,7 +431,7 @@ class _ChannelFeedState extends State<_ChannelFeed> final itemWidth = (constraints.crossAxisExtent - spacing * (crossAxisCount - 1)) / crossAxisCount; - final itemExtent = itemWidth * 157 / 110 + cardBodyExtent; + final itemExtent = itemWidth * 157 / 110 + homePosterCardBodyExtent; return SliverGrid( key: Key('recommend-category-grid-${widget.categoryId}'), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( @@ -446,17 +440,14 @@ class _ChannelFeedState extends State<_ChannelFeed> mainAxisSpacing: rowGap, mainAxisExtent: itemExtent, ), - delegate: SliverChildBuilderDelegate( - (context, index) { - final drama = dramas[index]; - return HomePosterCard( - key: Key('recommend-category-card-${drama.id}'), - drama: drama, - onTap: () => widget.onOpenDrama(drama), - ); - }, - childCount: dramas.length, - ), + delegate: SliverChildBuilderDelegate((context, index) { + final drama = dramas[index]; + return HomePosterCard( + key: Key('recommend-category-card-${drama.id}'), + drama: drama, + onTap: () => widget.onOpenDrama(drama), + ); + }, childCount: dramas.length), ); }, ), @@ -464,14 +455,11 @@ class _ChannelFeedState extends State<_ChannelFeed> } } -/// 判定接口下发的短剧标签是否为「上新」。 +/// 「上新」标签语义码,对应后台 content_tag.code。 +const String newArrivalTagCode = 'NEW_ARRIVAL'; + +/// 判定短剧是否属于「上新」板块。 /// -/// 完全以后端返回的 `tag` / `tagLocales`(经 [Drama.localizedTag] 本地化) -/// 为准;兼容「上新 / NEW / BARU」三种文案,并兼容「本周上新」这类带前后缀的写法。 -bool _isNewArrivalTag(Drama drama, String lang, String localizedBadge) { - final tag = drama.localizedTag(lang).trim(); - if (tag.isEmpty) return false; - if (tag == localizedBadge) return true; - final lower = tag.toLowerCase(); - return tag.contains('上新') || lower == 'new' || lower == 'baru'; -} +/// 只看语义码:码由后台标签管理配置,文案怎么改、切什么语言都不影响。 +bool isNewArrivalTag(Drama drama) => + drama.tagCode.trim().toUpperCase() == newArrivalTagCode; diff --git a/lib/pages/recommend/widgets/home_hero_carousel.dart b/lib/pages/recommend/widgets/home_hero_carousel.dart index a8bfb6e..538226b 100644 --- a/lib/pages/recommend/widgets/home_hero_carousel.dart +++ b/lib/pages/recommend/widgets/home_hero_carousel.dart @@ -46,15 +46,25 @@ class _HomeHeroCarouselState extends State { 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 { 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 { void _syncTimer() { _timer?.cancel(); _timer = null; - final canAdvance = mounted && + final canAdvance = + mounted && widget.active && _tickerEnabled && !_pressed && @@ -102,12 +116,16 @@ class _HomeHeroCarouselState extends State { 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 { 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), ], diff --git a/test/recommend/home_hero_carousel_test.dart b/test/recommend/home_hero_carousel_test.dart new file mode 100644 index 0000000..f5b3672 --- /dev/null +++ b/test/recommend/home_hero_carousel_test.dart @@ -0,0 +1,83 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fruitshorts/core/data/models/home_feed.dart'; +import 'package:fruitshorts/core/theme/app_colors.dart'; +import 'package:fruitshorts/l10n/app_localizations.dart'; +import 'package:fruitshorts/pages/recommend/widgets/home_hero_carousel.dart'; + +const List _banners = [ + FeedBanner(id: '1', imageUrl: 'https://cdn.test/a.png'), + FeedBanner(id: '2', imageUrl: 'https://cdn.test/b.png'), +]; + +Widget _harness(List banners) { + return MaterialApp( + locale: const Locale('en'), + supportedLocales: AppLocalizations.supportedLocales, + localizationsDelegates: AppLocalizations.localizationsDelegates, + home: Scaffold( + body: HomeHeroCarousel( + banners: banners, + dramaFor: (_) => null, + onWatch: (_) {}, + ), + ), + ); +} + +Color? _dotColor(WidgetTester tester, int index) { + final decoration = + tester + .widget(find.byKey(Key('home-hero-dot-$index'))) + .decoration + as BoxDecoration?; + return decoration?.color; +} + +void main() { + testWidgets('should_keep_looping_backwards_from_the_first_banner', ( + tester, + ) async { + await tester.pumpWidget(_harness(_banners)); + await tester.pumpAndSettle(); + + expect(_dotColor(tester, 0), AppColors.brandRed); + + // 从第一张往右滑:无缝轮播应该接到最后一张,而不是被边界挡住。 + await tester.fling(find.byType(PageView), const Offset(700, 0), 1200); + await tester.pumpAndSettle(); + + expect(_dotColor(tester, 1), AppColors.brandRed); + expect(_dotColor(tester, 0), isNot(AppColors.brandRed)); + }); + + testWidgets('should_wrap_to_first_banner_after_last_auto_advance', ( + tester, + ) async { + await tester.pumpWidget(_harness(_banners)); + await tester.pumpAndSettle(); + + // 自动播两次:第 2 张之后应回到第 1 张,且页码继续往前走不回头。 + await tester.pump(const Duration(milliseconds: 4600)); + await tester.pumpAndSettle(); + expect(_dotColor(tester, 1), AppColors.brandRed); + + await tester.pump(const Duration(milliseconds: 4600)); + await tester.pumpAndSettle(); + expect(_dotColor(tester, 0), AppColors.brandRed); + }); + + testWidgets('should_not_loop_when_only_one_banner', (tester) async { + await tester.pumpWidget( + _harness(const [FeedBanner(id: '1', imageUrl: 'https://cdn.test/a.png')]), + ); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('home-hero-dot-0')), findsNothing); + await tester.pump(const Duration(milliseconds: 6000)); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + }); +} diff --git a/test/recommend/recommend_sections_test.dart b/test/recommend/recommend_sections_test.dart index 995d6be..00fb4e8 100644 --- a/test/recommend/recommend_sections_test.dart +++ b/test/recommend/recommend_sections_test.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/app.dart'; +import 'package:fruitshorts/core/data/models/drama.dart'; import 'package:fruitshorts/core/data/repositories/favorite_repository.dart'; import 'package:fruitshorts/core/data/repositories/playback_repository.dart'; import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart'; @@ -10,6 +11,7 @@ import 'package:fruitshorts/core/theme/app_colors.dart'; import 'package:fruitshorts/core/theme/app_dimens.dart'; import 'package:fruitshorts/core/widgets/app_bottom_nav.dart'; import 'package:fruitshorts/pages/player/player_view.dart'; +import 'package:fruitshorts/pages/recommend/recommend_discover_page.dart'; import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; @@ -34,7 +36,10 @@ void main() { expect(find.byKey(const Key('recommend-tab-recommend')), findsOneWidget); expect(find.byKey(const Key('recommend-tab-playlist')), findsOneWidget); expect(find.byKey(const Key('recommend-tab-all')), findsOneWidget); - expect(find.byKey(const Key('recommend-feed-player'), skipOffstage: false), findsOneWidget); + expect( + find.byKey(const Key('recommend-feed-player'), skipOffstage: false), + findsOneWidget, + ); expect(find.byKey(const Key('recommend-top-gradient')), findsOneWidget); final recommendText = tester.widget( find.descendant( @@ -65,7 +70,9 @@ void main() { find.byKey(const Key('recommend-top-nav')), ); // 右侧依次是搜索和金色礼物盒(钱包入口),礼物盒贴右边距 8。 - final searchRect = tester.getRect(find.byKey(const Key('recommend-search'))); + final searchRect = tester.getRect( + find.byKey(const Key('recommend-search')), + ); final giftRect = tester.getRect(find.byKey(const Key('recommend-gift'))); expect(giftRect.right, closeTo(topNavRect.right - 8, 0.1)); expect(searchRect.right, lessThanOrEqualTo(giftRect.left)); @@ -78,7 +85,12 @@ void main() { tester .getRect(find.byKey(const Key('recommend-top-nav'))) .overlaps( - tester.getRect(find.byKey(const Key('recommend-feed-player'), skipOffstage: false)), + tester.getRect( + find.byKey( + const Key('recommend-feed-player'), + skipOffstage: false, + ), + ), ), isTrue, ); @@ -387,7 +399,10 @@ void main() { tester.getRect(find.byKey(const Key('all-dramas-filters'))).top, closeTo(fixedFilterTop, 0.1), ); - expect(find.byKey(const Key('all-filter-classification-all')), findsOneWidget); + expect( + find.byKey(const Key('all-filter-classification-all')), + findsOneWidget, + ); expect(find.byKey(const Key('all-filter-category-all')), findsOneWidget); expect(find.byKey(const Key('all-filter-tag-all')), findsOneWidget); expect( @@ -407,7 +422,10 @@ void main() { await tester.tap(find.byKey(const Key('recommend-tab-playlist'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('recommend-section-hits')), findsOneWidget); - expect(find.byKey(const Key('recommend-feed-player'), skipOffstage: false), findsOneWidget); + expect( + find.byKey(const Key('recommend-feed-player'), skipOffstage: false), + findsOneWidget, + ); expect(find.byType(AppBottomNav), findsOneWidget); await tester.ensureVisible(find.byKey(const Key('recommend-tab-all'))); @@ -430,7 +448,10 @@ void main() { await tester.drag(pages, const Offset(-320, 0)); await tester.pumpAndSettle(); expect(find.byKey(const Key('recommend-section-hits')), findsOneWidget); - expect(find.byKey(const Key('recommend-feed-player'), skipOffstage: false), findsOneWidget); + expect( + find.byKey(const Key('recommend-feed-player'), skipOffstage: false), + findsOneWidget, + ); expect( tester .widget( @@ -454,7 +475,10 @@ void main() { await tester.drag(pages, const Offset(640, 0)); await tester.pumpAndSettle(); - expect(find.byKey(const Key('recommend-feed-player'), skipOffstage: false), findsOneWidget); + expect( + find.byKey(const Key('recommend-feed-player'), skipOffstage: false), + findsOneWidget, + ); expect(find.byKey(const Key('recommend-tab-recommend')), findsOneWidget); _drainOverflows(tester); }); @@ -497,7 +521,9 @@ void main() { await _pumpRecommend(tester); final gesture = await tester.startGesture( - tester.getCenter(find.byKey(const Key('recommend-feed-player'), skipOffstage: false)), + tester.getCenter( + find.byKey(const Key('recommend-feed-player'), skipOffstage: false), + ), ); await gesture.moveBy(const Offset(0, 80)); await tester.pump(); @@ -505,7 +531,10 @@ void main() { final feedScroll = tester.state( find .descendant( - of: find.byKey(const Key('recommend-feed-player'), skipOffstage: false), + of: find.byKey( + const Key('recommend-feed-player'), + skipOffstage: false, + ), matching: find.byType(Scrollable), ) .first, @@ -544,6 +573,25 @@ void main() { expect(search.focusNode?.hasFocus, isTrue); _drainOverflows(tester); }); + + test('should_judge_new_arrival_only_by_tag_code', () { + Drama tagged({String tagCode = '', String tag = ''}) => Drama( + id: 'tagged', + title: 'Tagged', + synopsis: 'synopsis', + posterUrl: '', + tag: tag, + tagCode: tagCode, + ); + + expect(isNewArrivalTag(tagged(tagCode: 'NEW_ARRIVAL')), isTrue); + expect(isNewArrivalTag(tagged(tagCode: ' new_arrival ')), isTrue); + // 其它语义码(以及没有码的标签)都不算上新,文案不参与判断。 + expect(isNewArrivalTag(tagged(tagCode: 'HOT')), isFalse); + expect(isNewArrivalTag(tagged(tagCode: 'NEW_RELEASE')), isFalse); + expect(isNewArrivalTag(tagged(tag: '上新')), isFalse); + expect(isNewArrivalTag(tagged()), isFalse); + }); } /// 测试字体(等宽方块字形)比真实字体宽/高,卡片固定高度会报 2~4px