diff --git a/lib/modules/gift/gift_page.dart b/lib/modules/gift/gift_page.dart index 49dab82..e8b443a 100644 --- a/lib/modules/gift/gift_page.dart +++ b/lib/modules/gift/gift_page.dart @@ -676,6 +676,7 @@ class _GiftPageState extends State with TickerProviderStateMixin { color: const Color(0xff09372E).withValues(alpha: 0.5), constraints: BoxConstraints(maxHeight: 430.w), child: Column( + mainAxisSize: MainAxisSize.min, children: [ SizedBox(height: 12.w), Row( @@ -782,7 +783,8 @@ class _GiftPageState extends State with TickerProviderStateMixin { SizedBox(width: 5.w), ], ), - Expanded( + SizedBox( + height: kGiftTabPageHeight.w, child: TabBarView( physics: NeverScrollableScrollPhysics(), controller: tabController, diff --git a/lib/modules/gift/gift_tab_page.dart b/lib/modules/gift/gift_tab_page.dart index 7dff45e..b0efb3c 100644 --- a/lib/modules/gift/gift_tab_page.dart +++ b/lib/modules/gift/gift_tab_page.dart @@ -18,6 +18,29 @@ enum _GiftGridPageStatus { idle, loading, ready } const Duration _kGiftPageSkeletonMinDuration = Duration(milliseconds: 420); const Duration _kGiftPageSkeletonMaxDuration = Duration(milliseconds: 900); +const int _kGiftGridColumnCount = 4; +const int _kGiftGridRowCount = 2; +const double _kGiftGridCardHeight = 76; +const double _kGiftGridVerticalPadding = 5; +const double _kGiftGridMainAxisSpacing = 2; +const double _kGiftGridCrossAxisSpacing = 8; +const double _kGiftCardVerticalPadding = 5; +const double _kGiftIconPriceSpacing = 1; +const double _kGiftPriceRowHeight = 14; +const double _kGiftGridTopGap = 5; +const double _kGiftIndicatorTopGap = 5; +const double _kGiftPageIndicatorHeight = 6.5; +const double _kGiftGridBottomPadding = 6; +const double _kGiftGridHeight = + _kGiftGridCardHeight * _kGiftGridRowCount + + _kGiftGridMainAxisSpacing * (_kGiftGridRowCount - 1) + + _kGiftGridVerticalPadding * 2; +const double kGiftTabPageHeight = + _kGiftGridTopGap + + _kGiftGridHeight + + _kGiftIndicatorTopGap + + _kGiftPageIndicatorHeight + + _kGiftGridBottomPadding; class GiftTabPage extends StatefulWidget { final String type; @@ -72,8 +95,9 @@ class _GiftTabPageState extends State )) : Column( children: [ - SizedBox(height: 23.w), - Expanded( + SizedBox(height: _kGiftGridTopGap.w), + SizedBox( + height: _kGiftGridHeight.w, child: PageView.builder( controller: _giftPageController, onPageChanged: (i) { @@ -95,16 +119,7 @@ class _GiftTabPageState extends State _GiftGridPageStatus.ready) { return _buildGiftPageSkeleton(); } - return GridView.builder( - shrinkWrap: true, - padding: EdgeInsets.symmetric(horizontal: 12.w), - physics: NeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - childAspectRatio: 0.75, - mainAxisSpacing: 8.w, - crossAxisSpacing: 8.w, - ), + return _buildGiftGrid( itemCount: size, itemBuilder: (BuildContext context, int index) { return _bagItem(gifts[(i * 8) + index], ref); @@ -114,8 +129,9 @@ class _GiftTabPageState extends State itemCount: (gifts.length / 8).ceil(), ), ), + SizedBox(height: _kGiftIndicatorTopGap.w), _indicator(ref), - SizedBox(height: 10.w), + SizedBox(height: _kGiftGridBottomPadding.w), ], ); }, @@ -206,18 +222,11 @@ class _GiftTabPageState extends State ), ), ), - GridView.builder( - shrinkWrap: true, - padding: EdgeInsets.symmetric(horizontal: 12.w), - physics: NeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - childAspectRatio: 0.75, - mainAxisSpacing: 8.w, - crossAxisSpacing: 8.w, + Positioned.fill( + child: _buildGiftGrid( + itemCount: 8, + itemBuilder: (context, index) => _buildGiftSkeletonCard(), ), - itemCount: 8, - itemBuilder: (context, index) => _buildGiftSkeletonCard(), ), ], ); @@ -226,42 +235,71 @@ class _GiftTabPageState extends State Widget _buildGiftSkeletonCard() { final baseColor = Colors.white.withValues(alpha: 0.12); return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.w), - color: Colors.white.withValues(alpha: 0.08), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(12.w)), + child: Padding( + padding: EdgeInsets.symmetric(vertical: _kGiftCardVerticalPadding.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: 48.w, + height: 48.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: baseColor, + ), + child: Center(child: SCRotatingDotsLoading(size: 24.w)), + ), + SizedBox(height: _kGiftIconPriceSpacing.w), + Container( + width: 52.w, + height: 8.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(999.w), + color: baseColor, + ), + ), + ], + ), ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 48.w, - height: 48.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.w), - color: baseColor, - ), - child: Center(child: SCRotatingDotsLoading(size: 24.w)), + ); + } + + Widget _buildGiftGrid({ + required int itemCount, + required IndexedWidgetBuilder itemBuilder, + }) { + return LayoutBuilder( + builder: (context, constraints) { + final horizontalPadding = 12.w; + final verticalPadding = _kGiftGridVerticalPadding.w; + final mainAxisSpacing = _kGiftGridMainAxisSpacing.w; + final crossAxisSpacing = _kGiftGridCrossAxisSpacing.w; + final itemHeight = _kGiftGridCardHeight.w; + final gridWidth = + constraints.maxWidth - + horizontalPadding * 2 - + crossAxisSpacing * (_kGiftGridColumnCount - 1); + final itemWidth = (gridWidth / _kGiftGridColumnCount).clamp( + 1.0, + double.infinity, + ); + return GridView.builder( + padding: EdgeInsets.symmetric( + horizontal: horizontalPadding, + vertical: verticalPadding, ), - SizedBox(height: 7.w), - Container( - width: 52.w, - height: 8.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(999.w), - color: baseColor, - ), + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: _kGiftGridColumnCount, + childAspectRatio: itemWidth / itemHeight, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, ), - SizedBox(height: 6.w), - Container( - width: 40.w, - height: 8.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(999.w), - color: baseColor, - ), - ), - ], - ), + itemCount: itemCount, + itemBuilder: itemBuilder, + ); + }, ); } @@ -297,40 +335,45 @@ class _GiftTabPageState extends State Widget _bagItem(SocialChatGiftRes gift, SCAppGeneralManager ref) { return gift.id == "-1000" ? GestureDetector( + behavior: HitTestBehavior.opaque, child: Stack( children: [ Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12.w), - color: Colors.white10, border: Border.all(color: Colors.transparent, width: 1.w), ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - _strategy.getGiftPageCustomizedRuleIcon(), - fit: BoxFit.cover, - width: 48.w, - height: 48.w, - ), - SizedBox(height: 5.w), - Container( - height: 23.w, - margin: EdgeInsets.symmetric(horizontal: 3.w), - alignment: Alignment.center, - child: text( - SCAppLocalizations.of(context)!.rulesUpload, - maxLines: 2, - fontSize: 10.sp, - letterSpacing: 0.1, - lineHeight: 1, - textAlign: TextAlign.center, - fontWeight: FontWeight.w600, - textColor: Colors.white, + child: Padding( + padding: EdgeInsets.symmetric( + vertical: _kGiftCardVerticalPadding.w, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Image.asset( + _strategy.getGiftPageCustomizedRuleIcon(), + fit: BoxFit.cover, + width: 48.w, + height: 48.w, ), - ), - ], + SizedBox(height: _kGiftIconPriceSpacing.w), + Container( + height: 12.w, + margin: EdgeInsets.symmetric(horizontal: 3.w), + alignment: Alignment.center, + child: text( + SCAppLocalizations.of(context)!.rulesUpload, + maxLines: 1, + fontSize: 10, + letterSpacing: 0.1, + lineHeight: 1, + textAlign: TextAlign.center, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + ), + ], + ), ), ), ], @@ -403,10 +446,10 @@ class _GiftTabPageState extends State }, ) : GestureDetector( + behavior: HitTestBehavior.opaque, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12.w), - color: Colors.white10, border: Border.all( color: checkedIndex == ref.giftByTab[widget.type]!.indexOf(gift) @@ -415,64 +458,61 @@ class _GiftTabPageState extends State width: 1.w, ), ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - netImage( - url: gift.giftPhoto ?? "", - fit: BoxFit.cover, - width: 48.w, - height: 48.w, - loadingWidget: _buildGiftCoverLoading(), - errorWidget: _buildGiftCoverNoData(), - ), - SizedBox(height: 5.w), - Container( - height: 23.w, - margin: EdgeInsets.symmetric(horizontal: 3.w), - alignment: Alignment.center, - child: text( - gift.giftName ?? "", - maxLines: 2, - fontSize: 10.sp, - letterSpacing: 0.1, - lineHeight: 1, - textAlign: TextAlign.center, - fontWeight: FontWeight.w600, - textColor: widget.isDark ? Colors.white : Colors.black, + child: Padding( + padding: EdgeInsets.symmetric( + vertical: _kGiftCardVerticalPadding.w, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + netImage( + url: gift.giftPhoto ?? "", + fit: BoxFit.cover, + width: 48.w, + height: 48.w, + loadingWidget: _buildGiftCoverLoading(), + errorWidget: _buildGiftCoverNoData(), ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: - _isBackpackTab - ? [ - text( - "x${_formatBackpackQuantity(gift)}", - fontSize: 10.sp, - fontWeight: FontWeight.w600, - textColor: - widget.isDark - ? SocialChatTheme.primaryLight - : SocialChatTheme.primaryColor, - ), - ] - : [ - Image.asset( - _strategy.getGiftPageGoldCoinIcon(), - width: 14.w, - height: 14.w, - ), - SizedBox(width: 3.w), - text( - "${gift.giftCandy}", - fontSize: 10.sp, - textColor: - widget.isDark ? Colors.white : Colors.black, - ), - ], - ), - ], + SizedBox(height: _kGiftIconPriceSpacing.w), + SizedBox( + height: _kGiftPriceRowHeight.w, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: + _isBackpackTab + ? [ + text( + "x${_formatBackpackQuantity(gift)}", + fontSize: 10, + lineHeight: 1, + fontWeight: FontWeight.w600, + textColor: + widget.isDark + ? SocialChatTheme.primaryLight + : SocialChatTheme.primaryColor, + ), + ] + : [ + Image.asset( + _strategy.getGiftPageGoldCoinIcon(), + width: 14.w, + height: 14.w, + ), + SizedBox(width: 3.w), + text( + "${gift.giftCandy}", + fontSize: 10, + lineHeight: 1, + textColor: + widget.isDark + ? Colors.white + : Colors.black, + ), + ], + ), + ), + ], + ), ), ), onTap: () { diff --git a/lib/modules/user/my_items/gift/bags_gift_page.dart b/lib/modules/user/my_items/gift/bags_gift_page.dart new file mode 100644 index 0000000..a2f9922 --- /dev/null +++ b/lib/modules/user/my_items/gift/bags_gift_page.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/business_logic/models/res/gift_backpack_res.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/sc_page_list.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/ui_kit/widgets/store/store_bag_page_helpers.dart'; + +const double _kBagsGiftGridAspectRatio = 0.86; + +///背包-礼物 +class BagsGiftPage extends SCPageList { + const BagsGiftPage({super.key}); + + @override + SCPageListState createState() => + _BagsGiftPageState(); +} + +class _BagsGiftPageState + extends SCPageListState { + SocialChatGiftBackpackRes? selectedGift; + + @override + void initState() { + super.initState(); + enablePullUp = false; + isGridView = true; + gridViewCount = 3; + padding = EdgeInsets.symmetric(horizontal: 6.w); + backgroundColor = Colors.transparent; + gridDelegate = SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: gridViewCount, + mainAxisSpacing: 12.w, + crossAxisSpacing: 12.w, + childAspectRatio: _kBagsGiftGridAspectRatio, + ); + loadData(1); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: + items.isEmpty && isLoading + ? const SCBagGridSkeleton() + : buildList(context), + ); + } + + @override + Widget buildItem(SocialChatGiftBackpackRes res) { + final gift = res.giftConfig; + final isSelected = selectedGift?.id == res.id; + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + selectedGift = res; + setState(() {}); + }, + child: Container( + decoration: BoxDecoration( + color: const Color(0xff18F2B1).withValues(alpha: 0.1), + border: Border.all( + color: + isSelected ? SocialChatTheme.primaryLight : Colors.transparent, + width: 1.w, + ), + borderRadius: BorderRadius.all(Radius.circular(8.w)), + ), + child: Stack( + alignment: Alignment.center, + children: [ + Column( + children: [ + SizedBox(height: 25.w), + netImage( + url: gift?.giftPhoto ?? "", + width: 55.w, + height: 55.w, + fit: BoxFit.contain, + ), + SizedBox(height: 8.w), + buildStoreBagItemTitle( + gift?.giftName ?? "", + textColor: Colors.white, + fontSize: 12, + ), + SizedBox(height: 6.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy + .getGiftPageGoldCoinIcon(), + width: 14.w, + height: 14.w, + ), + SizedBox(width: 3.w), + text( + _formatNumber(gift?.giftCandy), + fontSize: 10, + lineHeight: 1, + textColor: Colors.white, + ), + ], + ), + ], + ), + PositionedDirectional( + top: 0, + start: 0, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.w), + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: BorderRadiusDirectional.only( + topStart: Radius.circular(6.w), + topEnd: Radius.circular(12.w), + bottomEnd: Radius.circular(12.w), + ), + ), + child: text( + "x${_formatNumber(res.quantity)}", + fontSize: 10, + lineHeight: 1, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + ), + ), + ], + ), + ), + ); + } + + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + try { + final giftList = await SCChatRoomRepository().giftBackpack(); + onSuccess(giftList.where((item) => (item.quantity ?? 0) > 0).toList()); + } catch (e) { + if (onErr != null) { + onErr(); + } + } + } + + String _formatNumber(num? value) { + if (value == null) { + return "0"; + } + if (value % 1 == 0) { + return value.toInt().toString(); + } + return value.toString(); + } +} diff --git a/lib/modules/user/my_items/my_items_page.dart b/lib/modules/user/my_items/my_items_page.dart index 3a6a5d0..13abae2 100644 --- a/lib/modules/user/my_items/my_items_page.dart +++ b/lib/modules/user/my_items/my_items_page.dart @@ -1,142 +1,140 @@ -import 'dart:async'; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:yumi/modules/user/my_items/theme/bags_tab_theme_page.dart'; -import 'package:tancent_vap/utils/constant.dart'; -import 'package:tancent_vap/widgets/vap_view.dart'; - -import 'package:yumi/app_localizations.dart'; -import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; -import 'package:yumi/ui_kit/components/sc_compontent.dart'; -import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; -import 'package:yumi/app/routes/sc_fluro_navigator.dart'; -import 'package:yumi/shared/tools/sc_path_utils.dart'; -import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; -import 'package:yumi/ui_kit/widgets/headdress/headdress_widget.dart'; -import 'package:yumi/modules/store/store_route.dart'; -import 'package:yumi/modules/user/my_items/chatbox/bags_chatbox_page.dart'; -import 'package:yumi/modules/user/my_items/headdress/bags_headdress_page.dart'; -import 'package:yumi/modules/user/my_items/mountains/bags_mountains_page.dart'; - -import '../../../app/constants/sc_global_config.dart'; -import '../../../shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart'; - -class MyItemsPage extends StatefulWidget { - @override - _MyItemsPageState createState() => _MyItemsPageState(); -} - -class _MyItemsPageState extends State - with SingleTickerProviderStateMixin { - late TabController _tabController; - final List _pages = []; - final List _tabs = []; - - @override - void initState() { - super.initState(); - _pages.add(BagsHeaddressPage()); - _pages.add( - BagsMountainsPage(), - ); - _pages.add( - BagsTabThemePage(), - ); - _pages.add(BagsChatboxPage()); - _tabController = TabController(length: _pages.length, vsync: this); - _tabController.addListener(() { - }); - } - - @override - void dispose() { - super.dispose(); - } - - @override - Widget build(BuildContext context) { - _tabs.clear(); - _tabs.add(Tab(text: SCAppLocalizations.of(context)!.headdress)); - _tabs.add(Tab(text: SCAppLocalizations.of(context)!.mountains)); - _tabs.add(Tab(text: SCAppLocalizations.of(context)!.theme)); - _tabs.add(Tab(text: SCAppLocalizations.of(context)!.chatBox)); - return Stack( - children: [ - Image.asset( - SCGlobalConfig.businessLogicStrategy.getLanguagePageBackgroundImage(), - width: ScreenUtil().screenWidth, - height: ScreenUtil().screenHeight, - fit: BoxFit.fill, - ), - Scaffold( - backgroundColor: Colors.transparent, - resizeToAvoidBottomInset: false, - appBar: SocialChatStandardAppBar( - title: SCAppLocalizations.of(context)!.myItems, - actions: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - child: Container( - padding: EdgeInsets.all(5.w), - margin: EdgeInsetsDirectional.only(end: 10.w), - child: Image.asset( - "sc_images/store/sc_icon_bag_shop.png", - width: 20.w, - height: 20.w, - ), - ), - onTap: () { - SCNavigatorUtils.push(context, StoreRoute.list, replace: true); - }, - ), - ], - ), - body: SafeArea(top:false,child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TabBar( - tabAlignment: TabAlignment.center, - isScrollable: true, - labelColor: Colors.white, - unselectedLabelStyle: SCGlobalConfig.businessLogicStrategy - .getStorePageTabUnselectedLabelStyle() - .copyWith( - fontSize: - SCGlobalConfig.businessLogicStrategy - .getStorePageTabUnselectedLabelStyle() - .fontSize - ?.sp, - ), - // indicatorPadding: EdgeInsets.symmetric( - // vertical: 5.w, - // horizontal: 15.w, - // ), - indicator: SCFixedWidthTabIndicator( - width: 15.w, - color: - SCGlobalConfig.businessLogicStrategy - .getStorePageTabIndicatorColor(), - ), - dividerColor: - SCGlobalConfig.businessLogicStrategy - .getStorePageTabDividerColor(), - controller: _tabController, - tabs: _tabs, - ), - ], - ), - SizedBox(height: 5.w), - Expanded( - child: TabBarView(controller: _tabController, children: _pages), - ), - ], - ),), - ), - ], - ); - } -} +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/modules/store/store_route.dart'; +import 'package:yumi/modules/user/my_items/chatbox/bags_chatbox_page.dart'; +import 'package:yumi/modules/user/my_items/gift/bags_gift_page.dart'; +import 'package:yumi/modules/user/my_items/headdress/bags_headdress_page.dart'; +import 'package:yumi/modules/user/my_items/mountains/bags_mountains_page.dart'; + +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart'; + +class MyItemsPage extends StatefulWidget { + const MyItemsPage({super.key}); + + @override + State createState() => _MyItemsPageState(); +} + +class _MyItemsPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + final List _pages = []; + final List _tabs = []; + + @override + void initState() { + super.initState(); + _pages.add(BagsHeaddressPage()); + _pages.add(BagsMountainsPage()); + _pages.add(BagsGiftPage()); + _pages.add(BagsChatboxPage()); + _tabController = TabController(length: _pages.length, vsync: this); + _tabController.addListener(() {}); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + _tabs.clear(); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.headdress)); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.mountains)); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.gift)); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.chatBox)); + return Stack( + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy.getLanguagePageBackgroundImage(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.myItems, + actions: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + child: Container( + padding: EdgeInsets.all(5.w), + margin: EdgeInsetsDirectional.only(end: 10.w), + child: Image.asset( + "sc_images/store/sc_icon_bag_shop.png", + width: 20.w, + height: 20.w, + ), + ), + onTap: () { + SCNavigatorUtils.push( + context, + StoreRoute.list, + replace: true, + ); + }, + ), + ], + ), + body: SafeArea( + top: false, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TabBar( + tabAlignment: TabAlignment.center, + isScrollable: true, + labelColor: Colors.white, + unselectedLabelStyle: SCGlobalConfig.businessLogicStrategy + .getStorePageTabUnselectedLabelStyle() + .copyWith( + fontSize: + SCGlobalConfig.businessLogicStrategy + .getStorePageTabUnselectedLabelStyle() + .fontSize + ?.sp, + ), + // indicatorPadding: EdgeInsets.symmetric( + // vertical: 5.w, + // horizontal: 15.w, + // ), + indicator: SCFixedWidthTabIndicator( + width: 15.w, + color: + SCGlobalConfig.businessLogicStrategy + .getStorePageTabIndicatorColor(), + ), + dividerColor: + SCGlobalConfig.businessLogicStrategy + .getStorePageTabDividerColor(), + controller: _tabController, + tabs: _tabs, + ), + ], + ), + SizedBox(height: 5.w), + Expanded( + child: TabBarView( + controller: _tabController, + children: _pages, + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart index 5f657c6..3f4c603 100644 --- a/lib/ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart @@ -49,11 +49,11 @@ class _RoomRocketRewardDialogLoaderState void initState() { super.initState(); if (widget.initialPopupRecords.isNotEmpty) { - _popupRecords.addAll(widget.initialPopupRecords); + _appendUniqueRewardRecords(_popupRecords, widget.initialPopupRecords); _rewards = RoomRocketApiMapper.rewardItemsFromRecords(_popupRecords); } if (widget.initialRoomRecords.isNotEmpty) { - _roomRecords.addAll(widget.initialRoomRecords); + _appendUniqueRewardRecords(_roomRecords, widget.initialRoomRecords); } _winners = RoomRocketApiMapper.winnersFromRecords( _visibleRoomRewardRecords(), @@ -85,7 +85,7 @@ class _RoomRocketRewardDialogLoaderState ? widget.initialPopupRecords : await _loadPopupRecords(requestCursor) : const []; - if (reset && useInitialPopupRecords) { + if (useInitialPopupRecords) { _usedInitialPopupRecords = true; } final nextRoomRecords = await _loadRoomRewardRecords(requestCursor); @@ -94,25 +94,13 @@ class _RoomRocketRewardDialogLoaderState } setState(() { if (reset) { - _popupRecords - ..clear() - ..addAll(nextPopupRecords); - _roomRecords - ..clear() - ..addAll( - _mergeRewardRecords([ - ...widget.initialRoomRecords, - ...nextRoomRecords, - ]), - ); - } else { - final mergedRoomRecords = _mergeRewardRecords([ - ..._roomRecords, + _replaceUniqueRewardRecords(_popupRecords, nextPopupRecords); + _replaceUniqueRewardRecords(_roomRecords, [ + ...widget.initialRoomRecords, ...nextRoomRecords, ]); - _roomRecords - ..clear() - ..addAll(mergedRoomRecords); + } else { + _appendUniqueRewardRecords(_roomRecords, nextRoomRecords); } _rewards = RoomRocketApiMapper.rewardItemsFromRecords(_popupRecords); _winners = RoomRocketApiMapper.winnersFromRecords( @@ -204,20 +192,6 @@ class _RoomRocketRewardDialogLoaderState return matched.isEmpty ? _roomRecords : matched; } - List _mergeRewardRecords( - List records, - ) { - final result = []; - final seen = {}; - for (final record in records) { - final id = record.id.trim(); - if (id.isEmpty || seen.add(id)) { - result.add(record); - } - } - return result; - } - @override Widget build(BuildContext context) { return RoomRocketRewardDialog( @@ -234,6 +208,66 @@ class _RoomRocketRewardDialogLoaderState } } +void _replaceUniqueRewardRecords( + List target, + List records, +) { + target + ..clear() + ..addAll(debugUniqueRoomRocketRewardRecords(records)); +} + +void _appendUniqueRewardRecords( + List target, + List records, +) { + final seen = target.map(_roomRocketRewardRecordUniqueKey).toSet(); + for (final record in records) { + final key = _roomRocketRewardRecordUniqueKey(record); + if (!seen.add(key)) { + continue; + } + target.add(record); + } +} + +@visibleForTesting +List debugUniqueRoomRocketRewardRecords( + List records, +) { + final seen = {}; + final uniqueRecords = []; + for (final record in records) { + final key = _roomRocketRewardRecordUniqueKey(record); + if (!seen.add(key)) { + continue; + } + uniqueRecords.add(record); + } + return uniqueRecords; +} + +String _roomRocketRewardRecordUniqueKey(SCRoomRocketRewardRecordRes record) { + final id = record.id.trim(); + if (id.isNotEmpty) { + return 'id:$id'; + } + return [ + 'fallback', + record.launchNo.trim(), + record.roomId.trim(), + record.roundNo.toString(), + record.level.toString(), + record.userId.trim(), + record.rewardScene.trim().toUpperCase(), + record.rewardType.trim().toUpperCase(), + record.rewardItemId.trim(), + record.rewardName.trim(), + record.rewardAmount.toString(), + record.expireDays.toString(), + ].join('|'); +} + void _rewardDialogDebug(String message) { if (!kDebugMode) { return; diff --git a/test/room_rocket_reward_dialog_loader_test.dart b/test/room_rocket_reward_dialog_loader_test.dart new file mode 100644 index 0000000..3fb48f8 --- /dev/null +++ b/test/room_rocket_reward_dialog_loader_test.dart @@ -0,0 +1,55 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart'; +import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart'; + +void main() { + group('room rocket reward dialog loader', () { + test('deduplicates reward records by id', () { + final records = debugUniqueRoomRocketRewardRecords([ + _rewardRecord(id: 'record-1', rewardAmount: 5000), + _rewardRecord(id: 'record-1', rewardAmount: 5200), + _rewardRecord(id: 'record-2', rewardAmount: 5200), + ]); + + expect(records, hasLength(2)); + expect(records.map((item) => item.id), ['record-1', 'record-2']); + expect(records.first.rewardAmount, 5000); + }); + + test('deduplicates id-less reward records by reward identity', () { + final records = debugUniqueRoomRocketRewardRecords([ + _rewardRecord(id: '', rewardAmount: 5000), + _rewardRecord(id: '', rewardAmount: 5000), + _rewardRecord(id: '', rewardAmount: 5200), + ]); + + expect(records, hasLength(2)); + expect(records.map((item) => item.rewardAmount), [5000, 5200]); + }); + }); +} + +SCRoomRocketRewardRecordRes _rewardRecord({ + required String id, + required int rewardAmount, +}) { + return SCRoomRocketRewardRecordRes.fromJson({ + 'id': id, + 'launchNo': 'launch-1', + 'roomId': 'room-1', + 'dayKey': '2026-05-15', + 'roundNo': 1, + 'level': 2, + 'userId': 'user-1', + 'userNickname': 'User 1', + 'rewardScene': 'TOP1', + 'rewardType': 'GOLD', + 'rewardItemId': 'gold', + 'rewardName': 'Yumi', + 'rewardCover': '', + 'rewardAmount': rewardAmount, + 'expireDays': 0, + 'grantStatus': 'SUCCESS', + 'popupStatus': 'UNREAD', + }); +}