diff --git a/lib/modules/room/online/room_online_page.dart b/lib/modules/room/online/room_online_page.dart index 0c51636..0c989b6 100644 --- a/lib/modules/room/online/room_online_page.dart +++ b/lib/modules/room/online/room_online_page.dart @@ -14,6 +14,7 @@ import '../../../ui_kit/components/sc_tts.dart'; import '../../../ui_kit/components/text/sc_text.dart'; import '../../../ui_kit/widgets/id/sc_special_id_badge.dart'; import '../../../shared/tools/sc_lk_dialog_util.dart'; +import '../../../ui_kit/widgets/badge/sc_user_badge_strip.dart'; import '../../../ui_kit/widgets/room/room_user_info_card.dart'; ///房间用户在线列表 @@ -107,84 +108,105 @@ class _RoomOnlinePageState _openUserCard(userInfo); }, ), - SizedBox(width: 3.w), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - msgRoleTag(userInfo.roles ?? "", width: 20.w, height: 20.w), - SizedBox(width: 3.w), - socialchatNickNameText( - textColor: Colors.black, - maxWidth: 200.w, - userInfo.userNickname ?? "", - fontSize: 14.sp, - type: userInfo.getVIP()?.name ?? "", - needScroll: - (userInfo.userNickname?.characters.length ?? 0) > 16, - ), - getVIPBadge( - userInfo.getVIP()?.name, - width: 45.w, - height: 25.w, - ), - // ListView.separated( - // scrollDirection: Axis.horizontal, - // shrinkWrap: true, - // itemCount: userInfo.wearBadge?.length ?? 0, - // itemBuilder: (context, index) { - // return netImage( - // width: 25.w, - // height: 25.w, - // url: userInfo.wearBadge?[index].selectUrl ?? "", - // ); - // }, - // separatorBuilder: (BuildContext context, int index) { - // return SizedBox(width: 5.w); - // }, - // ), - ], - ), - SizedBox(height: 3.w), - GestureDetector( - child: Container( - padding: EdgeInsets.symmetric(vertical: 3.w), - child: SCSpecialIdBadge( - idText: userInfo.getID(), - showAnimated: userInfo.hasSpecialId(), - assetPath: SCSpecialIdAssets.userId, - animationWidth: 62.w, - animationHeight: 24.w, - showTextBesideAnimated: true, - animatedTextSpacing: 0, - showAnimatedGradientText: - userInfo.shouldShowColoredSpecialIdText(), - animationTextStyle: TextStyle( - color: Colors.black, - fontSize: 12.sp, - fontWeight: FontWeight.bold, + SizedBox(width: 8.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + msgRoleTag( + userInfo.roles ?? "", + width: 20.w, + height: 20.w, ), - normalTextStyle: TextStyle( - color: Colors.black, - fontSize: 12.sp, - fontWeight: FontWeight.bold, + SizedBox(width: 3.w), + socialchatNickNameText( + textColor: Colors.black, + maxWidth: 132.w, + userInfo.userNickname ?? "", + fontSize: 14.sp, + type: userInfo.getVIP()?.name ?? "", + needScroll: + (userInfo.userNickname?.characters.length ?? 0) > + 16, ), - showCopyIcon: true, - showCopyIconWhenAnimated: true, - copyIconSize: 12.w, - copyIconSpacing: 5.w, - animationFit: BoxFit.contain, - ), + getVIPBadge( + userInfo.vipLevel ?? userInfo.getVIP()?.name, + width: 45.w, + height: 25.w, + ), + SizedBox(width: 3.w), + getWealthLevel( + userInfo.wealthLevel ?? 0, + width: 42.w, + height: 20.w, + fontSize: 9.sp, + ), + SizedBox(width: 2.w), + getUserLevel( + userInfo.charmLevel ?? 0, + width: 42.w, + height: 20.w, + fontSize: 9.sp, + ), + ], ), - onTap: () { - Clipboard.setData(ClipboardData(text: userInfo.getID())); - SCTts.show( - SCAppLocalizations.of(context)!.copiedToClipboard, - ); - }, - ), - ], + SizedBox(height: 3.w), + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(vertical: 3.w), + child: SCSpecialIdBadge( + idText: userInfo.getID(), + showAnimated: userInfo.hasSpecialId(), + assetPath: SCSpecialIdAssets.userId, + animationWidth: 62.w, + animationHeight: 24.w, + showTextBesideAnimated: true, + animatedTextSpacing: 0, + showAnimatedGradientText: + userInfo.shouldShowColoredSpecialIdText(), + animationTextStyle: TextStyle( + color: Colors.black, + fontSize: 12.sp, + fontWeight: FontWeight.bold, + ), + normalTextStyle: TextStyle( + color: Colors.black, + fontSize: 12.sp, + fontWeight: FontWeight.bold, + ), + showCopyIcon: true, + showCopyIconWhenAnimated: true, + copyIconSize: 12.w, + copyIconSpacing: 5.w, + animationFit: BoxFit.contain, + ), + ), + onTap: () { + Clipboard.setData(ClipboardData(text: userInfo.getID())); + SCTts.show( + SCAppLocalizations.of(context)!.copiedToClipboard, + ); + }, + ), + SizedBox(height: 2.w), + SCUserBadgeStrip( + userId: userInfo.id, + fallbackBadges: + userInfo.wearBadge + ?.where((item) => item.use ?? false) + .toList() ?? + const [], + height: 23.w, + badgeHeight: 21.w, + longBadgeWidth: 51.w, + spacing: 4.5.w, + maxBadges: 8, + reserveSpace: true, + ), + ], + ), ), ], ), diff --git a/lib/modules/user/profile/person_detail_page.dart b/lib/modules/user/profile/person_detail_page.dart index efb8533..2539854 100644 --- a/lib/modules/user/profile/person_detail_page.dart +++ b/lib/modules/user/profile/person_detail_page.dart @@ -65,6 +65,7 @@ class _PersonDetailPageState extends State { Map counterMap = {}; List giftWallList = []; + List honorWallBadges = []; // 添加滚动控制器 final ScrollController _scrollController = ScrollController(); @@ -134,6 +135,7 @@ class _PersonDetailPageState extends State { isBlacklistLoading = false; } userCounter(widget.tageId); + _loadHonorWall(); _loadGiftWall(); } @@ -159,6 +161,26 @@ class _PersonDetailPageState extends State { .catchError((e) {}); } + void _loadHonorWall() { + final userId = widget.tageId.trim(); + if (userId.isEmpty) { + return; + } + final currentUserId = + AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? ""; + final future = + currentUserId.isNotEmpty && currentUserId == userId + ? SCAccountRepository().currentUserBadges() + : SCAccountRepository().otherUserBadges(userId); + future + .then((result) { + if (!mounted) return; + honorWallBadges = result.honorWallBadges; + setState(() {}); + }) + .catchError((e) {}); + } + @override void dispose() { _scrollController.dispose(); @@ -658,8 +680,19 @@ class _PersonDetailPageState extends State { ), SizedBox(height: 12.w), _buildWallGrid( - itemCount: _wallPreviewItemCount, - itemBuilder: (_) => _buildHonorPlaceholderItem(), + itemCount: + honorWallBadges.isEmpty + ? _wallPreviewItemCount + : honorWallBadges.length + .clamp(0, _wallPreviewItemCount) + .toInt(), + itemBuilder: (index) { + final badge = + index < honorWallBadges.length + ? honorWallBadges[index] + : null; + return _buildHonorWallItem(badge); + }, ), SizedBox(height: 18.w), _buildProfileSectionTitle( @@ -744,20 +777,34 @@ class _PersonDetailPageState extends State { ); } - Widget _buildHonorPlaceholderItem() { + Widget _buildHonorWallItem(WearBadge? badge) { return _buildWallItemFrame( - child: Opacity( - opacity: 0.45, - child: Image.asset( - "sc_images/room/sc_icon_room_music_empty.png", - width: 38.w, - height: 38.w, - fit: BoxFit.contain, - ), - ), + child: + badge == null + ? Opacity( + opacity: 0.45, + child: Image.asset( + "sc_images/room/sc_icon_room_music_empty.png", + width: 38.w, + height: 38.w, + fit: BoxFit.contain, + ), + ) + : Padding( + padding: EdgeInsets.all(8.w), + child: netImage( + url: _badgeUrl(badge), + width: double.infinity, + height: double.infinity, + fit: BoxFit.contain, + noDefaultImg: true, + ), + ), ); } + String _badgeUrl(WearBadge badge) => (badge.selectUrl ?? "").trim(); + Widget _buildGiftWallItem(SocialChatGiftRes? gift) { return _buildWallItemFrame( child: Stack( diff --git a/lib/modules/user/profile/profile_wall_of_honors_page.dart b/lib/modules/user/profile/profile_wall_of_honors_page.dart index 8f34016..e2cae40 100644 --- a/lib/modules/user/profile/profile_wall_of_honors_page.dart +++ b/lib/modules/user/profile/profile_wall_of_honors_page.dart @@ -20,21 +20,13 @@ class ProfileWallOfHonorsPage extends StatefulWidget { class _ProfileWallOfHonorsPageState extends State { static const Color _profileBg = Color(0xff072121); - static const Color _mutedText = Color(0xff8F9A98); - int _selectedTabIndex = 0; bool _loading = true; SCUserBadgeRes _badges = SCUserBadgeRes.empty(); - List get _honorBadges => - _badges.displayBadges.where(_isHonorBadge).toList(); + List get _wallBadges => _badges.honorWallBadges; - List get _eventBadges => - _badges.displayBadges.where(_isEventBadge).toList(); - - int get _honorCount => _honorBadges.length; - int get _eventCount => _eventBadges.length; - int get _totalCount => _honorCount + _eventCount; + int get _totalCount => _wallBadges.length; @override void initState() { @@ -110,54 +102,6 @@ class _ProfileWallOfHonorsPageState extends State { ); } - Widget _buildTabs() { - return SizedBox( - width: double.infinity, - height: 82.w, - child: Row( - children: [ - Expanded(child: _buildTab(index: 0, label: "Honors($_honorCount)")), - Expanded(child: _buildTab(index: 1, label: "Event($_eventCount)")), - ], - ), - ); - } - - Widget _buildTab({required int index, required String label}) { - final selected = _selectedTabIndex == index; - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (_selectedTabIndex == index) return; - setState(() { - _selectedTabIndex = index; - }); - }, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - text( - label, - fontSize: 20, - textColor: selected ? Colors.white : _mutedText, - fontWeight: FontWeight.w400, - maxLines: 1, - ), - SizedBox(height: 9.w), - AnimatedContainer( - duration: const Duration(milliseconds: 160), - width: selected ? 28.w : 0, - height: 4.w, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(4.w), - ), - ), - ], - ), - ); - } - Widget _buildEmptyState() { return SliverFillRemaining( hasScrollBody: false, @@ -194,7 +138,7 @@ class _ProfileWallOfHonorsPageState extends State { } Widget _buildBadgeGrid() { - final list = _selectedTabIndex == 0 ? _honorBadges : _eventBadges; + final list = _wallBadges; if (list.isEmpty) { return _buildEmptyState(); } @@ -253,29 +197,6 @@ class _ProfileWallOfHonorsPageState extends State { ); } - bool _isHonorBadge(WearBadge badge) { - final sourceType = badge.sourceType?.trim().toUpperCase() ?? ""; - final type = badge.type?.trim().toUpperCase() ?? ""; - return sourceType == "ACHIEVEMENT" || - sourceType == "VIP" || - (sourceType.isEmpty && - (type == "LONG" || type == "ACHIEVEMENT" || type == "VIP")); - } - - bool _isEventBadge(WearBadge badge) { - final sourceType = badge.sourceType?.trim().toUpperCase() ?? ""; - final type = badge.type?.trim().toUpperCase() ?? ""; - if (sourceType == "ACTIVITY") { - return true; - } - if (sourceType.isNotEmpty) { - return false; - } - return type == "ACTIVITY" || - type == "SHORT" || - (type != "LONG" && type != "VIP" && type != "ACHIEVEMENT"); - } - String _badgeUrl(WearBadge badge) { return (badge.selectUrl ?? "").trim(); } @@ -289,9 +210,7 @@ class _ProfileWallOfHonorsPageState extends State { child: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ - SliverToBoxAdapter( - child: Column(children: [_buildTopBar(), _buildTabs()]), - ), + SliverToBoxAdapter(child: _buildTopBar()), _loading ? _buildLoadingState() : _buildBadgeGrid(), ], ), diff --git a/lib/shared/business_logic/models/res/login_res.dart b/lib/shared/business_logic/models/res/login_res.dart index 6be89f2..a0ae1c6 100644 --- a/lib/shared/business_logic/models/res/login_res.dart +++ b/lib/shared/business_logic/models/res/login_res.dart @@ -851,6 +851,8 @@ class WearBadge { String? sourceType, String? type, String? userId, + int? updateTime, + String? updatedAt, bool? use, }) { _animationUrl = animationUrl; @@ -866,6 +868,8 @@ class WearBadge { _sourceType = sourceType; _type = type; _userId = userId; + _updateTime = updateTime; + _updatedAt = updatedAt; } WearBadge.fromJson(dynamic json) { @@ -889,6 +893,10 @@ class WearBadge { _stringValue(json['sourceType']) ?? _sourceTypeFromType(rawType); _type = rawType; _userId = _stringValue(json['userId']); + _updateTime = _intValue( + json['updateTime'] ?? json['updatedAt'] ?? json['createTime'], + ); + _updatedAt = _stringValue(json['updatedAt']); } String? _animationUrl; @@ -904,6 +912,8 @@ class WearBadge { String? _sourceType; String? _type; String? _userId; + int? _updateTime; + String? _updatedAt; WearBadge copyWith({ String? animationUrl, @@ -919,6 +929,8 @@ class WearBadge { String? sourceType, String? type, String? userId, + int? updateTime, + String? updatedAt, }) => WearBadge( animationUrl: animationUrl ?? _animationUrl, badgeKey: badgeKey ?? _badgeKey, @@ -933,6 +945,8 @@ class WearBadge { sourceType: sourceType ?? _sourceType, type: type ?? _type, userId: userId ?? _userId, + updateTime: updateTime ?? _updateTime, + updatedAt: updatedAt ?? _updatedAt, ); String? get animationUrl => _animationUrl; @@ -961,6 +975,10 @@ class WearBadge { String? get userId => _userId; + int? get updateTime => _updateTime; + + String? get updatedAt => _updatedAt; + Map toJson() { final map = {}; map['animationUrl'] = _animationUrl; @@ -976,6 +994,8 @@ class WearBadge { map['sourceType'] = _sourceType; map['type'] = _type; map['userId'] = _userId; + map['updateTime'] = _updateTime; + map['updatedAt'] = _updatedAt; return map; } diff --git a/lib/shared/business_logic/models/res/room_user_card_res.dart b/lib/shared/business_logic/models/res/room_user_card_res.dart index a9108f1..3210917 100644 --- a/lib/shared/business_logic/models/res/room_user_card_res.dart +++ b/lib/shared/business_logic/models/res/room_user_card_res.dart @@ -12,19 +12,20 @@ import 'package:yumi/shared/business_logic/models/res/login_res.dart'; /// userLevel : {"charmLevel":0,"extValues":{"":{}},"wealthLevel":0} /// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":"0","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]} -class RoomUserCardRes { - RoomUserCardRes({ - bool? agent, - bool? anchor, - bool? follow, - bool? friend, - String? ktvIntegral, - String? roomRole, - String? supperVip, - List? useBadge, - List? useProps, - UserLevel? userLevel, - SocialChatUserProfile? userProfile,}){ +class RoomUserCardRes { + RoomUserCardRes({ + bool? agent, + bool? anchor, + bool? follow, + bool? friend, + String? ktvIntegral, + String? roomRole, + String? supperVip, + List? useBadge, + List? useProps, + UserLevel? userLevel, + SocialChatUserProfile? userProfile, + }) { _agent = agent; _anchor = anchor; _follow = follow; @@ -35,12 +36,8 @@ class RoomUserCardRes { _useBadge = useBadge; _useProps = useProps; _userLevel = userLevel; - final normalizedVipLevel = supperVip?.trim(); - _userProfile = - normalizedVipLevel == null || normalizedVipLevel.isEmpty - ? userProfile - : userProfile?.copyWith(vipLevel: normalizedVipLevel); -} + _userProfile = _profileWithCardHints(userProfile, supperVip, userLevel); + } RoomUserCardRes.fromJson(dynamic json) { _agent = json['agent']; @@ -49,7 +46,7 @@ class RoomUserCardRes { _friend = json['friend']; _ktvIntegral = json['ktvIntegral']; _roomRole = json['roomRole']; - _supperVip = socialChatVipLevelHintFromJson(json); + _supperVip = socialChatVipLevelHintFromJson(json); if (json['useBadge'] != null) { _useBadge = []; json['useBadge'].forEach((v) { @@ -62,15 +59,15 @@ class RoomUserCardRes { _useProps?.add(UseProps.fromJson(v)); }); } - _userLevel = json['userLevel'] != null ? UserLevel.fromJson(json['userLevel']) : null; - _userProfile = - json['userProfile'] != null - ? socialChatUserProfileFromJsonWithVipHint(json) - : null; - final normalizedVipLevel = _supperVip?.trim(); - if (normalizedVipLevel != null && normalizedVipLevel.isNotEmpty) { - _userProfile = _userProfile?.copyWith(vipLevel: normalizedVipLevel); - } + _userLevel = + json['userLevel'] != null + ? UserLevel.fromJson(json['userLevel']) + : null; + _userProfile = + json['userProfile'] != null + ? socialChatUserProfileFromJsonWithVipHint(json) + : null; + _userProfile = _profileWithCardHints(_userProfile, _supperVip, _userLevel); } bool? _agent; bool? _anchor; @@ -83,29 +80,31 @@ class RoomUserCardRes { List? _useProps; UserLevel? _userLevel; SocialChatUserProfile? _userProfile; -RoomUserCardRes copyWith({ bool? agent, - bool? anchor, - bool? follow, - bool? friend, - String? ktvIntegral, - String? roomRole, - String? supperVip, - List? useBadge, - List? useProps, - UserLevel? userLevel, - SocialChatUserProfile? userProfile, -}) => RoomUserCardRes( agent: agent ?? _agent, - anchor: anchor ?? _anchor, - follow: follow ?? _follow, - friend: friend ?? _friend, - ktvIntegral: ktvIntegral ?? _ktvIntegral, - roomRole: roomRole ?? _roomRole, - supperVip: supperVip ?? _supperVip, - useBadge: useBadge ?? _useBadge, - useProps: useProps ?? _useProps, - userLevel: userLevel ?? _userLevel, - userProfile: userProfile ?? _userProfile, -); + RoomUserCardRes copyWith({ + bool? agent, + bool? anchor, + bool? follow, + bool? friend, + String? ktvIntegral, + String? roomRole, + String? supperVip, + List? useBadge, + List? useProps, + UserLevel? userLevel, + SocialChatUserProfile? userProfile, + }) => RoomUserCardRes( + agent: agent ?? _agent, + anchor: anchor ?? _anchor, + follow: follow ?? _follow, + friend: friend ?? _friend, + ktvIntegral: ktvIntegral ?? _ktvIntegral, + roomRole: roomRole ?? _roomRole, + supperVip: supperVip ?? _supperVip, + useBadge: useBadge ?? _useBadge, + useProps: useProps ?? _useProps, + userLevel: userLevel ?? _userLevel, + userProfile: userProfile ?? _userProfile, + ); bool? get agent => _agent; bool? get anchor => _anchor; bool? get follow => _follow; @@ -140,9 +139,30 @@ RoomUserCardRes copyWith({ bool? agent, map['userProfile'] = _userProfile?.toJson(); } return map; - } - -} + } +} + +SocialChatUserProfile? _profileWithCardHints( + SocialChatUserProfile? profile, + String? supperVip, + UserLevel? userLevel, +) { + if (profile == null) { + return null; + } + var result = profile; + final normalizedVipLevel = supperVip?.trim(); + if (normalizedVipLevel != null && normalizedVipLevel.isNotEmpty) { + result = result.copyWith(vipLevel: normalizedVipLevel); + } + if (userLevel?.wealthLevel != null || userLevel?.charmLevel != null) { + result = result.copyWith( + wealthLevel: userLevel?.wealthLevel ?? result.wealthLevel, + charmLevel: userLevel?.charmLevel ?? result.charmLevel, + ); + } + return result; +} /// account : "" /// accountStatus : "" @@ -179,19 +199,20 @@ RoomUserCardRes copyWith({ bool? agent, /// type : "" /// userId : 0 -class WearBadge { - WearBadge({ - String? animationUrl, - String? badgeKey, - num? badgeLevel, - String? badgeName, - num? expireTime, - num? id, - num? milestone, - String? notSelectUrl, - String? selectUrl, - String? type, - num? userId,}){ +class WearBadge { + WearBadge({ + String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + num? expireTime, + num? id, + num? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + num? userId, + }) { _animationUrl = animationUrl; _badgeKey = badgeKey; _badgeLevel = badgeLevel; @@ -200,10 +221,10 @@ class WearBadge { _id = id; _milestone = milestone; _notSelectUrl = notSelectUrl; - _selectUrl = selectUrl; - _type = type; - _userId = userId; -} + _selectUrl = selectUrl; + _type = type; + _userId = userId; + } WearBadge.fromJson(dynamic json) { _animationUrl = json['animationUrl']; @@ -229,29 +250,31 @@ class WearBadge { String? _selectUrl; String? _type; num? _userId; -WearBadge copyWith({ String? animationUrl, - String? badgeKey, - num? badgeLevel, - String? badgeName, - num? expireTime, - num? id, - num? milestone, - String? notSelectUrl, - String? selectUrl, - String? type, - num? userId, -}) => WearBadge( animationUrl: animationUrl ?? _animationUrl, - badgeKey: badgeKey ?? _badgeKey, - badgeLevel: badgeLevel ?? _badgeLevel, - badgeName: badgeName ?? _badgeName, - expireTime: expireTime ?? _expireTime, - id: id ?? _id, - milestone: milestone ?? _milestone, - notSelectUrl: notSelectUrl ?? _notSelectUrl, - selectUrl: selectUrl ?? _selectUrl, - type: type ?? _type, - userId: userId ?? _userId, -); + WearBadge copyWith({ + String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + num? expireTime, + num? id, + num? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + num? userId, + }) => WearBadge( + animationUrl: animationUrl ?? _animationUrl, + badgeKey: badgeKey ?? _badgeKey, + badgeLevel: badgeLevel ?? _badgeLevel, + badgeName: badgeName ?? _badgeName, + expireTime: expireTime ?? _expireTime, + id: id ?? _id, + milestone: milestone ?? _milestone, + notSelectUrl: notSelectUrl ?? _notSelectUrl, + selectUrl: selectUrl ?? _selectUrl, + type: type ?? _type, + userId: userId ?? _userId, + ); String? get animationUrl => _animationUrl; String? get badgeKey => _badgeKey; num? get badgeLevel => _badgeLevel; @@ -278,17 +301,14 @@ WearBadge copyWith({ String? animationUrl, map['type'] = _type; map['userId'] = _userId; return map; - } - -} - -class UserLevel { - UserLevel({ - num? charmLevel, - num? wealthLevel,}){ - _charmLevel = charmLevel; - _wealthLevel = wealthLevel; -} + } +} + +class UserLevel { + UserLevel({num? charmLevel, num? wealthLevel}) { + _charmLevel = charmLevel; + _wealthLevel = wealthLevel; + } UserLevel.fromJson(dynamic json) { _charmLevel = json['charmLevel']; @@ -296,11 +316,10 @@ class UserLevel { } num? _charmLevel; num? _wealthLevel; -UserLevel copyWith({ num? charmLevel, - num? wealthLevel, -}) => UserLevel( charmLevel: charmLevel ?? _charmLevel, - wealthLevel: wealthLevel ?? _wealthLevel, -); + UserLevel copyWith({num? charmLevel, num? wealthLevel}) => UserLevel( + charmLevel: charmLevel ?? _charmLevel, + wealthLevel: wealthLevel ?? _wealthLevel, + ); num? get charmLevel => _charmLevel; num? get wealthLevel => _wealthLevel; @@ -309,23 +328,23 @@ UserLevel copyWith({ num? charmLevel, map['charmLevel'] = _charmLevel; map['wealthLevel'] = _wealthLevel; return map; - } - -} - -class UseBadge { - UseBadge({ - String? animationUrl, - String? badgeKey, - num? badgeLevel, - String? badgeName, - num? expireTime, - String? id, - String? milestone, - String? notSelectUrl, - String? selectUrl, - String? type, - String? userId,}){ + } +} + +class UseBadge { + UseBadge({ + String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + num? expireTime, + String? id, + String? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + String? userId, + }) { _animationUrl = animationUrl; _badgeKey = badgeKey; _badgeLevel = badgeLevel; @@ -334,10 +353,10 @@ class UseBadge { _id = id; _milestone = milestone; _notSelectUrl = notSelectUrl; - _selectUrl = selectUrl; - _type = type; - _userId = userId; -} + _selectUrl = selectUrl; + _type = type; + _userId = userId; + } UseBadge.fromJson(dynamic json) { _animationUrl = json['animationUrl']; @@ -363,29 +382,31 @@ class UseBadge { String? _selectUrl; String? _type; String? _userId; -UseBadge copyWith({ String? animationUrl, - String? badgeKey, - num? badgeLevel, - String? badgeName, - num? expireTime, - String? id, - String? milestone, - String? notSelectUrl, - String? selectUrl, - String? type, - String? userId, -}) => UseBadge( animationUrl: animationUrl ?? _animationUrl, - badgeKey: badgeKey ?? _badgeKey, - badgeLevel: badgeLevel ?? _badgeLevel, - badgeName: badgeName ?? _badgeName, - expireTime: expireTime ?? _expireTime, - id: id ?? _id, - milestone: milestone ?? _milestone, - notSelectUrl: notSelectUrl ?? _notSelectUrl, - selectUrl: selectUrl ?? _selectUrl, - type: type ?? _type, - userId: userId ?? _userId, -); + UseBadge copyWith({ + String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + num? expireTime, + String? id, + String? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + String? userId, + }) => UseBadge( + animationUrl: animationUrl ?? _animationUrl, + badgeKey: badgeKey ?? _badgeKey, + badgeLevel: badgeLevel ?? _badgeLevel, + badgeName: badgeName ?? _badgeName, + expireTime: expireTime ?? _expireTime, + id: id ?? _id, + milestone: milestone ?? _milestone, + notSelectUrl: notSelectUrl ?? _notSelectUrl, + selectUrl: selectUrl ?? _selectUrl, + type: type ?? _type, + userId: userId ?? _userId, + ); String? get animationUrl => _animationUrl; String? get badgeKey => _badgeKey; num? get badgeLevel => _badgeLevel; @@ -412,6 +433,5 @@ UseBadge copyWith({ String? animationUrl, map['type'] = _type; map['userId'] = _userId; return map; - } - -} + } +} diff --git a/lib/shared/business_logic/models/res/sc_user_badge_res.dart b/lib/shared/business_logic/models/res/sc_user_badge_res.dart index 904fe23..4fbbca3 100644 --- a/lib/shared/business_logic/models/res/sc_user_badge_res.dart +++ b/lib/shared/business_logic/models/res/sc_user_badge_res.dart @@ -39,6 +39,17 @@ class SCUserBadgeRes { return badges; } + List get honorWallBadges { + final source = + shortBadges.isNotEmpty + ? shortBadges + : displayBadges.where(_isHonorWallBadge).toList(); + final result = + source.where((badge) => _badgeUrl(badge).isNotEmpty).toList(); + result.sort((a, b) => _badgeSortValue(b).compareTo(_badgeSortValue(a))); + return result; + } + static dynamic _bodyFrom(dynamic json) { if (json is Map) { final body = json['body']; @@ -92,6 +103,31 @@ class SCUserBadgeRes { type == "ACHIEVEMENT"; } + static bool _isHonorWallBadge(WearBadge badge) { + final sourceType = badge.sourceType?.trim().toUpperCase() ?? ""; + final type = badge.type?.trim().toUpperCase() ?? ""; + if (type == "LONG" || type == "VIP" || sourceType == "VIP") { + return false; + } + if (sourceType.isNotEmpty) { + return sourceType == "ACTIVITY" || sourceType == "ACHIEVEMENT"; + } + return type == "SHORT" || + type == "ACTIVITY" || + type == "ACHIEVEMENT" || + type == "HONOR_ACTIVITY" || + type == "HONOR_ADMIN"; + } + + static int _badgeSortValue(WearBadge badge) { + return badge.updateTime ?? + badge.expireTime ?? + DateTime.tryParse( + (badge.updatedAt ?? "").replaceFirst(" ", "T"), + )?.millisecondsSinceEpoch ?? + 0; + } + static String? _stringValue(dynamic value) { final text = value?.toString().trim(); return text == null || text.isEmpty ? null : text; diff --git a/lib/ui_kit/components/sc_compontent.dart b/lib/ui_kit/components/sc_compontent.dart index 5bffa13..e97585c 100644 --- a/lib/ui_kit/components/sc_compontent.dart +++ b/lib/ui_kit/components/sc_compontent.dart @@ -471,7 +471,43 @@ _buildPhoneInput( ///vip标识 getVIPBadge(String? type, {double? width, double? height}) { - return Container(); + final level = _vipBadgeLevel(type); + if (level <= 0) { + return const SizedBox.shrink(); + } + final assetLevel = level.clamp(1, 5).toInt(); + return Image.asset( + "sc_images/vip/sc_vip_badge_$assetLevel.png", + width: width, + height: height, + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => const SizedBox.shrink(), + ); +} + +int _vipBadgeLevel(String? type) { + final text = type?.trim() ?? ""; + if (text.isEmpty) { + return 0; + } + final plain = int.tryParse(text); + if ((plain ?? 0) > 0) { + return plain!; + } + final vipMatch = RegExp( + r'(?:S?VIP|NOBLE[_\s-]*VIP)[^\d]*([1-9]\d*)', + caseSensitive: false, + ).firstMatch(text); + if (vipMatch != null) { + return int.tryParse(vipMatch.group(1) ?? "") ?? 0; + } + final trailingMatch = RegExp(r'([1-9]\d*)$').firstMatch(text); + if (trailingMatch != null) { + return int.tryParse(trailingMatch.group(1) ?? "") ?? 0; + } + return RegExp(r'S?VIP|NOBLE[_\s-]*VIP', caseSensitive: false).hasMatch(text) + ? 1 + : 0; } ///用户等级 diff --git a/lib/ui_kit/widgets/props/sc_data_card_resource_view.dart b/lib/ui_kit/widgets/props/sc_data_card_resource_view.dart new file mode 100644 index 0000000..fb95204 --- /dev/null +++ b/lib/ui_kit/widgets/props/sc_data_card_resource_view.dart @@ -0,0 +1,280 @@ +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart'; +import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart'; + +class SCDataCardResourceView extends StatelessWidget { + const SCDataCardResourceView({ + super.key, + this.resource, + this.sourceUrl, + this.coverUrl, + this.width, + this.height, + this.fit = BoxFit.cover, + this.loop = true, + this.fallback, + }); + + final PropsResources? resource; + final String? sourceUrl; + final String? coverUrl; + final double? width; + final double? height; + final BoxFit fit; + final bool loop; + final Widget? fallback; + + static bool isSupported(String? url) { + return _isSvga(url) || _isPag(url) || _isVideo(url) || _isImage(url); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final resolvedWidth = _resolvedSize(width, constraints.maxWidth); + final resolvedHeight = _resolvedSize(height, constraints.maxHeight); + final cover = _firstNonBlank([coverUrl, resource?.cover]); + final source = _firstNonBlank([sourceUrl, resource?.sourceUrl]); + final resourceUrl = source.isNotEmpty ? source : cover; + final fallbackView = + fallback ?? + _buildImage( + cover, + width: resolvedWidth, + height: resolvedHeight, + fit: fit, + ); + + if (resourceUrl.isEmpty) { + return fallbackView; + } + if (_isSvga(resourceUrl)) { + return SCNetworkSvgaWidget( + resource: resourceUrl, + width: resolvedWidth, + height: resolvedHeight, + loop: loop, + fit: fit, + fallback: fallbackView, + ); + } + if (_isPag(resourceUrl)) { + return RoomRocketPagPreview( + resource: resourceUrl, + width: resolvedWidth, + height: resolvedHeight, + loop: loop, + fit: fit, + underlayBuilder: (_) => fallbackView, + defaultBuilder: (_) => fallbackView, + ); + } + if (_isVideo(resourceUrl)) { + return _LoopingDataCardVideo( + url: resourceUrl, + width: resolvedWidth, + height: resolvedHeight, + fit: fit, + loop: loop, + fallback: fallbackView, + ); + } + return _buildImage( + resourceUrl, + width: resolvedWidth, + height: resolvedHeight, + fit: fit, + fallback: fallbackView, + ); + }, + ); + } + + static double? _resolvedSize(double? fixed, double constrained) { + if (fixed != null && fixed.isFinite && fixed > 0) { + return fixed; + } + if (constrained.isFinite && constrained > 0) { + return constrained; + } + return null; + } + + static Widget _buildImage( + String url, { + double? width, + double? height, + required BoxFit fit, + Widget? fallback, + }) { + if (url.trim().isEmpty) { + return fallback ?? SizedBox(width: width, height: height); + } + return netImage( + url: url.trim(), + width: width, + height: height, + fit: fit, + noDefaultImg: fallback != null, + loadingWidget: fallback, + errorWidget: fallback, + ); + } + + static String _firstNonBlank(Iterable values) { + for (final value in values) { + final text = value?.trim() ?? ''; + if (text.isNotEmpty) { + return text; + } + } + return ''; + } + + static bool _hasExtension(String? url, List extensions) { + final lower = url?.trim().toLowerCase() ?? ''; + if (lower.isEmpty) { + return false; + } + return extensions.any( + (extension) => + lower.endsWith(extension) || + lower.contains('$extension?') || + lower.contains('$extension&'), + ); + } + + static bool _isSvga(String? url) => _hasExtension(url, const ['.svga']); + + static bool _isPag(String? url) => _hasExtension(url, const ['.pag']); + + static bool _isVideo(String? url) => + _hasExtension(url, const ['.mp4', '.mov', '.m4v']); + + static bool _isImage(String? url) => _hasExtension(url, const [ + '.png', + '.jpg', + '.jpeg', + '.webp', + '.gif', + '.bmp', + ]); +} + +class _LoopingDataCardVideo extends StatefulWidget { + const _LoopingDataCardVideo({ + required this.url, + this.width, + this.height, + required this.fit, + required this.loop, + required this.fallback, + }); + + final String url; + final double? width; + final double? height; + final BoxFit fit; + final bool loop; + final Widget fallback; + + @override + State<_LoopingDataCardVideo> createState() => _LoopingDataCardVideoState(); +} + +class _LoopingDataCardVideoState extends State<_LoopingDataCardVideo> { + VideoPlayerController? _controller; + bool _ready = false; + bool _failed = false; + + @override + void initState() { + super.initState(); + _load(); + } + + @override + void didUpdateWidget(covariant _LoopingDataCardVideo oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.url != widget.url || oldWidget.loop != widget.loop) { + _disposeController(); + _load(); + } + } + + Future _load() async { + final url = widget.url.trim(); + if (url.isEmpty) { + setState(() => _failed = true); + return; + } + setState(() { + _ready = false; + _failed = false; + }); + final controller = VideoPlayerController.networkUrl(Uri.parse(url)); + _controller = controller; + try { + await controller.setLooping(widget.loop); + await controller.setVolume(0); + await controller.initialize(); + if (!mounted || _controller != controller) { + await controller.dispose(); + return; + } + setState(() => _ready = true); + await controller.play(); + } catch (_) { + if (!mounted || _controller != controller) { + return; + } + setState(() { + _ready = false; + _failed = true; + }); + } + } + + void _disposeController() { + final controller = _controller; + _controller = null; + if (controller != null) { + controller.dispose(); + } + } + + @override + void dispose() { + _disposeController(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final controller = _controller; + if (_failed || !_ready || controller == null) { + return widget.fallback; + } + final videoSize = controller.value.size; + final child = + videoSize.width > 0 && videoSize.height > 0 + ? FittedBox( + fit: widget.fit, + child: SizedBox( + width: videoSize.width, + height: videoSize.height, + child: VideoPlayer(controller), + ), + ) + : VideoPlayer(controller); + return SizedBox( + width: widget.width, + height: widget.height, + child: ClipRect(child: child), + ); + } +} diff --git a/lib/ui_kit/widgets/room/room_msg_item.dart b/lib/ui_kit/widgets/room/room_msg_item.dart index f321286..26ed376 100644 --- a/lib/ui_kit/widgets/room/room_msg_item.dart +++ b/lib/ui_kit/widgets/room/room_msg_item.dart @@ -1296,10 +1296,10 @@ class _MsgItemState extends State { child: SCUserBadgeStrip( userId: userId, fallbackBadges: fallbackBadges, - height: 16.w, - badgeHeight: 14.w, - longBadgeWidth: 34.w, - spacing: 3.w, + height: 23.w, + badgeHeight: 21.w, + longBadgeWidth: 51.w, + spacing: 4.5.w, maxBadges: 8, reserveSpace: true, ), @@ -1841,10 +1841,10 @@ class _MsgItemState extends State { ], ), ), - SizedBox(width: 3.w), + SizedBox(width: 8.w), Expanded( child: SizedBox( - height: 48.w, + height: 50.w, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, @@ -1887,10 +1887,10 @@ class _MsgItemState extends State { ], ), ), - SizedBox(width: 3.w), + SizedBox(width: 8.w), Expanded( child: SizedBox( - height: 48.w, + height: 50.w, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/ui_kit/widgets/room/room_user_info_card.dart b/lib/ui_kit/widgets/room/room_user_info_card.dart index 64bdf23..bdea8dd 100644 --- a/lib/ui_kit/widgets/room/room_user_info_card.dart +++ b/lib/ui_kit/widgets/room/room_user_info_card.dart @@ -21,6 +21,7 @@ import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/data_sources/models/enum/sc_props_type.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; import 'package:yumi/shared/business_logic/models/res/login_res.dart'; import 'package:yumi/shared/business_logic/models/res/room_user_card_res.dart' @@ -30,6 +31,7 @@ import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/modules/gift/gift_page.dart'; import 'package:yumi/ui_kit/widgets/id/sc_special_id_badge.dart'; +import 'package:yumi/ui_kit/widgets/props/sc_data_card_resource_view.dart'; import '../../../shared/data_sources/models/enum/sc_room_roles_type.dart'; import '../../../shared/business_logic/models/res/sc_user_identity_res.dart'; @@ -83,7 +85,7 @@ class _RoomUserInfoCardState extends State { @override Widget build(BuildContext context) { - final sheetHeight = MediaQuery.of(context).size.height * 0.4; + final sheetHeight = MediaQuery.of(context).size.height * 0.6; return Consumer( builder: (context, ref, child) { @@ -151,6 +153,7 @@ class _RoomUserInfoCardState extends State { }) { final bottomPadding = MediaQuery.of(context).padding.bottom; final profile = ref.userCardInfo?.userProfile; + final dataCard = _activeDataCard(ref.userCardInfo); final canShowReport = currentUserId != widget.userId; final canShowSetting = canShowReport && @@ -162,86 +165,84 @@ class _RoomUserInfoCardState extends State { height: sheetHeight, child: ClipRRect( borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)), - child: DecoratedBox( - decoration: _sheetDecoration(), - child: Stack( - children: [ - Positioned.fill( - child: SingleChildScrollView( - physics: const ClampingScrollPhysics(), - padding: EdgeInsets.fromLTRB( - 18.w, - 16.w, - 18.w, - bottomPadding + 10.w, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _buildProfileHeader(context, profile, ref), - SizedBox(height: 6.w), - _buildInfoChips(context, profile), - SizedBox(height: 9.w), - _buildBadgeStrip( - profile?.id ?? widget.userId, - ref, - profile, - ), - SizedBox(height: 10.w), - _buildActions( - context: context, - ref: ref, - rtcProvider: rtcProvider, - isSelf: isSelf, - canLeaveMic: canLeaveMic, - currentMicIndex: currentMicIndex, - ), - ], - ), + child: Stack( + children: [ + Positioned.fill(child: _buildSheetBackground(dataCard)), + Positioned.fill( + child: SingleChildScrollView( + physics: const ClampingScrollPhysics(), + padding: EdgeInsets.fromLTRB( + 18.w, + 16.w, + 18.w, + bottomPadding + 10.w, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildProfileHeader(context, profile, ref), + SizedBox(height: 6.w), + _buildInfoChips(context, profile), + SizedBox(height: 9.w), + _buildBadgeStrip( + profile?.id ?? widget.userId, + ref, + profile, + ), + SizedBox(height: 10.w), + _buildActions( + context: context, + ref: ref, + rtcProvider: rtcProvider, + isSelf: isSelf, + canLeaveMic: canLeaveMic, + currentMicIndex: currentMicIndex, + ), + ], ), ), - if (canShowReport) - Positioned( - top: 14.w, - right: 16.w, - child: _buildHeaderIconButton( - iconAsset: "sc_images/room/sc_icon_user_card_report.png", - onTap: () { - final route = - "${SCMainRoute.report}?type=user&tageId=${widget.userId}"; - Navigator.of(context).pop(); - SCNavigatorUtils.push( - navigatorKey.currentState?.context ?? context, - route, - replace: false, - ); - }, - ), + ), + if (canShowReport) + Positioned( + top: 14.w, + right: 16.w, + child: _buildHeaderIconButton( + iconAsset: "sc_images/room/sc_icon_user_card_report.png", + onTap: () { + final route = + "${SCMainRoute.report}?type=user&tageId=${widget.userId}"; + Navigator.of(context).pop(); + SCNavigatorUtils.push( + navigatorKey.currentState?.context ?? context, + route, + replace: false, + ); + }, ), - if (canShowSetting) - Positioned( - top: 14.w, - right: 52.w, - child: _buildHeaderIconButton( - iconAsset: - "sc_images/room/sc_icon_room_user_card_setting.png", - onTap: () { - if (userProvider?.userCardInfo == null) { - return; - } - Navigator.of(context).pop(); - showBottomInBottomDialog( - navigatorKey.currentState?.context ?? context, - RoomUserCardSetting( - roomId: roomId, - userCardInfo: userProvider?.userCardInfo?.copyWith(), - ), - ); - }, - ), + ), + if (canShowSetting) + Positioned( + top: 14.w, + right: 52.w, + child: _buildHeaderIconButton( + iconAsset: + "sc_images/room/sc_icon_room_user_card_setting.png", + onTap: () { + if (userProvider?.userCardInfo == null) { + return; + } + Navigator.of(context).pop(); + showBottomInBottomDialog( + navigatorKey.currentState?.context ?? context, + RoomUserCardSetting( + roomId: roomId, + userCardInfo: userProvider?.userCardInfo?.copyWith(), + ), + ); + }, ), - ], - ), + ), + ], ), ), ); @@ -262,6 +263,57 @@ class _RoomUserInfoCardState extends State { ); } + Widget _buildSheetBackground(PropsResources? dataCard) { + if (dataCard == null) { + return DecoratedBox(decoration: _sheetDecoration()); + } + return Stack( + fit: StackFit.expand, + children: [ + SCDataCardResourceView(resource: dataCard, fit: BoxFit.cover), + DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.white.withValues(alpha: 0.08), + Colors.white.withValues(alpha: 0.22), + Colors.white.withValues(alpha: 0.58), + ], + ), + ), + ), + ], + ); + } + + PropsResources? _activeDataCard(room_card.RoomUserCardRes? cardInfo) { + return _activeDataCardFromUseProps(cardInfo?.userProfile?.useProps) ?? + _activeDataCardFromUseProps(cardInfo?.useProps); + } + + PropsResources? _activeDataCardFromUseProps(List? useProps) { + final now = DateTime.now().millisecondsSinceEpoch; + for (final item in useProps ?? const []) { + final resource = item.propsResources; + if (resource?.type != SCPropsType.DATA_CARD.name) { + continue; + } + final expireText = item.expireTime?.trim() ?? ""; + final expireTime = int.tryParse(expireText); + if (expireText.isNotEmpty && (expireTime ?? 0) <= now) { + continue; + } + final source = resource?.sourceUrl?.trim() ?? ""; + final cover = resource?.cover?.trim() ?? ""; + if (source.isNotEmpty || cover.isNotEmpty) { + return resource; + } + } + return null; + } + Widget _buildProfileHeader( BuildContext context, SocialChatUserProfile? profile, @@ -290,7 +342,7 @@ class _RoomUserInfoCardState extends State { ), SizedBox(width: 4.w), socialchatNickNameText( - maxWidth: 190.w, + maxWidth: 116.w, profile?.userNickname ?? "", fontSize: 18.sp, textColor: Colors.black, @@ -298,6 +350,30 @@ class _RoomUserInfoCardState extends State { type: "", needScroll: (profile?.userNickname?.characters.length ?? 0) > 12, ), + SizedBox(width: 3.w), + getVIPBadge( + profile?.vipLevel ?? profile?.getVIP()?.name, + width: 45.w, + height: 25.w, + ), + SizedBox(width: 3.w), + getWealthLevel( + profile?.wealthLevel ?? + ref.userCardInfo?.userLevel?.wealthLevel ?? + 0, + width: 42.w, + height: 20.w, + fontSize: 9.sp, + ), + SizedBox(width: 2.w), + getUserLevel( + profile?.charmLevel ?? + ref.userCardInfo?.userLevel?.charmLevel ?? + 0, + width: 42.w, + height: 20.w, + fontSize: 9.sp, + ), ], ), SizedBox(height: 2.w), @@ -422,10 +498,10 @@ class _RoomUserInfoCardState extends State { return SCUserBadgeStrip( userId: userId, fallbackBadges: _roomCardFallbackBadges(ref, profile), - height: 55.w, - badgeHeight: 23.w, - longBadgeWidth: 58.w, - spacing: 8.w, + height: 82.w, + badgeHeight: 34.5.w, + longBadgeWidth: 87.w, + spacing: 10.w, maxBadges: 12, wrap: true, reserveSpace: true, diff --git a/test/sc_user_badge_res_test.dart b/test/sc_user_badge_res_test.dart new file mode 100644 index 0000000..59b40b5 --- /dev/null +++ b/test/sc_user_badge_res_test.dart @@ -0,0 +1,82 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_user_badge_res.dart'; + +void main() { + group('SCUserBadgeRes honor wall badges', () { + test('uses short badges only and sorts newest first', () { + final result = SCUserBadgeRes.fromJson({ + 'longBadges': [ + { + 'id': 'vip', + 'type': 'LONG', + 'sourceType': 'VIP', + 'selectUrl': 'https://example.com/vip.png', + 'updateTime': 3000, + }, + ], + 'shortBadges': [ + { + 'id': 'old', + 'type': 'SHORT', + 'sourceType': 'ACTIVITY', + 'selectUrl': 'https://example.com/old.png', + 'updateTime': 1000, + }, + { + 'id': 'new', + 'type': 'SHORT', + 'sourceType': 'ACHIEVEMENT', + 'selectUrl': 'https://example.com/new.png', + 'updateTime': 2000, + }, + ], + }); + + expect(result.displayBadges.map((badge) => badge.id), [ + 'vip', + 'old', + 'new', + ]); + expect(result.honorWallBadges.map((badge) => badge.id), ['new', 'old']); + }); + + test('filters long and vip badges from legacy mixed badges', () { + final result = SCUserBadgeRes.fromJson({ + 'badges': [ + { + 'id': 'long', + 'type': 'LONG', + 'selectUrl': 'https://example.com/long.png', + 'updateTime': 3000, + }, + { + 'id': 'vip', + 'type': 'VIP', + 'sourceType': 'VIP', + 'selectUrl': 'https://example.com/vip.png', + 'updateTime': 2500, + }, + { + 'id': 'activity', + 'type': 'ACTIVITY', + 'sourceType': 'ACTIVITY', + 'selectUrl': 'https://example.com/activity.png', + 'updateTime': 1000, + }, + { + 'id': 'achievement', + 'type': 'ACHIEVEMENT', + 'sourceType': 'ACHIEVEMENT', + 'selectUrl': 'https://example.com/achievement.png', + 'updateTime': 2000, + }, + ], + }); + + expect(result.honorWallBadges.map((badge) => badge.id), [ + 'achievement', + 'activity', + ]); + }); + }); +}