diff --git a/lib/modules/index/main_route.dart b/lib/modules/index/main_route.dart index 20b281a..afe4552 100644 --- a/lib/modules/index/main_route.dart +++ b/lib/modules/index/main_route.dart @@ -13,6 +13,7 @@ import 'package:yumi/modules/admin/editing/sc_editing_user_room_page.dart'; import 'package:yumi/modules/admin/search/sc_edit_room_search_admin_page.dart'; import 'package:yumi/modules/admin/search/sc_edit_user_search_admin_page.dart'; import 'package:yumi/modules/user/profile/person_detail_page.dart'; +import 'package:yumi/modules/user/profile/profile_gift_wall_detail_page.dart'; import 'package:yumi/modules/search/sc_search_page.dart'; import 'package:yumi/modules/media/image_preview_page.dart'; import 'package:yumi/modules/media/video_player_page.dart'; @@ -26,6 +27,7 @@ import '../user/edit/edit_user_info_page2.dart'; class SCMainRoute implements SCIRouterProvider { static String report = '/main/report'; static String person = '/main/person'; + static String giftWall = '/main/person/giftWall'; static String edit = '/main/person/edit'; static String follow = '/main/me/follow'; static String vistors = '/main/me/vistors'; @@ -63,6 +65,14 @@ class SCMainRoute implements SCIRouterProvider { ), ), ); + router.define( + giftWall, + handler: Handler( + handlerFunc: + (_, params) => + ProfileGiftWallDetailPage(tageId: params['tageId']!.first), + ), + ); router.define( mainSearch, handler: Handler(handlerFunc: (_, params) => SearchPage()), diff --git a/lib/modules/user/profile/person_detail_page.dart b/lib/modules/user/profile/person_detail_page.dart index d292b35..5cb8481 100644 --- a/lib/modules/user/profile/person_detail_page.dart +++ b/lib/modules/user/profile/person_detail_page.dart @@ -4,10 +4,7 @@ import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter/services.dart'; -import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:yumi/modules/user/profile/profile/sc_profile_page.dart'; -import 'package:yumi/modules/user/profile/props/sc_giftwall_page.dart'; import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; import 'package:yumi/ui_kit/components/text/sc_text.dart'; import 'package:yumi/shared/tools/sc_room_utils.dart'; @@ -24,18 +21,18 @@ import 'package:yumi/ui_kit/components/sc_tts.dart'; import 'package:yumi/app/routes/sc_routes.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_gift_repository_imp.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/services/audio/rtm_manager.dart'; import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/services/general/sc_app_general_manager.dart'; -import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; import '../../../app/constants/sc_screen.dart'; import '../../../shared/business_logic/models/res/sc_user_counter_res.dart'; import '../../../shared/business_logic/models/res/sc_user_identity_res.dart'; -import '../../../shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart'; import '../../../ui_kit/widgets/id/sc_special_id_badge.dart'; import '../../chat/chat_route.dart'; @@ -50,11 +47,11 @@ class PersonDetailPage extends StatefulWidget { _PersonDetailPageState createState() => _PersonDetailPageState(); } -class _PersonDetailPageState extends State - with SingleTickerProviderStateMixin { - late TabController _tabController; - final List _pages = []; - final List _tabs = []; +class _PersonDetailPageState extends State { + static const Color _profileBg = Color(0xff072121); + static const Color _cardBg = Color(0xff08251E); + static const Color _profileBorder = Color(0xffB2FBCC); + SCUserIdentityRes? userIdentity; bool isFollow = false; @@ -63,6 +60,7 @@ class _PersonDetailPageState extends State bool isBlacklist = false; Map counterMap = {}; + List giftWallList = []; // 添加滚动控制器 final ScrollController _scrollController = ScrollController(); @@ -72,19 +70,6 @@ class _PersonDetailPageState extends State void initState() { super.initState(); - // _pages.add(PersonDynamicListPage(false, widget.tageId)); - _pages.add(SCProfilePage(false, widget.tageId)); - _pages.add(SCGiftwallPage(widget.tageId)); - // _pages.add(RelationShipPage(false, widget.tageId)); - _tabController = TabController( - initialIndex: 0, - length: _pages.length, - vsync: this, - ); - _tabController.addListener(() { - setState(() {}); // 刷新UI - }); // 监听切换 - // 监听滚动 _scrollController.addListener(() { if (_scrollController.hasClients) { @@ -145,6 +130,7 @@ class _PersonDetailPageState extends State isBlacklistLoading = false; } userCounter(widget.tageId); + _loadGiftWall(); } void userCounter(String userId) async { @@ -158,13 +144,19 @@ class _PersonDetailPageState extends State setState(() {}); } - Widget _buildTab(int index, String text) { - return Tab(text: text); + void _loadGiftWall() { + SCGiftRepositoryImp() + .giftWall(widget.tageId) + .then((result) { + if (!mounted) return; + giftWallList = result; + setState(() {}); + }) + .catchError((e) {}); } @override void dispose() { - _tabController.dispose(); _scrollController.dispose(); super.dispose(); } @@ -399,12 +391,165 @@ class _PersonDetailPageState extends State ); } + Widget _buildHeaderBadgePlaceholder() { + return SizedBox( + height: 24.w, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [], + ), + ); + } + + Widget _buildHeaderIntroduction(SocialChatUserProfileManager ref) { + final intro = (ref.userProfile?.autograph ?? "").trim(); + return Container( + width: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 10.w), + child: Text.rich( + TextSpan( + children: [ + const TextSpan(text: "Introduction: "), + TextSpan(text: intro.isEmpty ? "--" : intro), + ], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + style: TextStyle( + color: Colors.white, + fontSize: 17.sp, + fontWeight: FontWeight.w400, + height: 1.28, + ), + ), + ); + } + + Widget _buildHeaderStats() { + return _buildGradientBorder( + width: 355.w, + height: 51.w, + radius: 8.w, + backgroundColor: _cardBg, + endAlpha: 0.42, + child: Row( + children: [ + _buildHeaderStatItem( + value: "${counterMap["INTERVIEW"]?.quantity ?? 0}", + label: SCAppLocalizations.of(context)!.vistors, + onTap: () { + if (widget.isMe == "true") { + SCNavigatorUtils.push(context, SCMainRoute.vistors); + } + }, + ), + _buildHeaderStatsDivider(), + _buildHeaderStatItem( + value: "${counterMap["SUBSCRIPTION"]?.quantity ?? 0}", + label: SCAppLocalizations.of(context)!.follow, + onTap: () { + if (widget.isMe == "true") { + SCNavigatorUtils.push(context, SCMainRoute.follow); + } + }, + ), + _buildHeaderStatsDivider(), + _buildHeaderStatItem( + value: "${counterMap["FANS"]?.quantity ?? 0}", + label: SCAppLocalizations.of(context)!.fans, + onTap: () { + if (widget.isMe == "true") { + SCNavigatorUtils.push(context, SCMainRoute.fans); + } + }, + ), + ], + ), + ); + } + + Widget _buildHeaderStatItem({ + required String value, + required String label, + required VoidCallback onTap, + }) { + return Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + value, + fontSize: 17.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + text(label, fontSize: 14.sp, textColor: Color(0xffB1B1B1)), + ], + ), + ), + ); + } + + Widget _buildHeaderStatsDivider() { + return Container( + height: 28.w, + width: 1.w, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color.fromRGBO(255, 255, 255, 0), + Colors.white, + Color.fromRGBO(255, 255, 255, 0), + ], + stops: [0, 0.4862, 1], + ), + ), + ); + } + + Widget _buildGradientBorder({ + required double width, + required double height, + required double radius, + required Widget child, + Color backgroundColor = _cardBg, + double endAlpha = 0, + }) { + final borderRadius = BorderRadius.circular(radius); + return Container( + width: width, + height: height, + decoration: BoxDecoration( + borderRadius: borderRadius, + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [_profileBorder, _profileBorder.withValues(alpha: endAlpha)], + ), + ), + padding: EdgeInsets.all(1.w), + child: Container( + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular((radius - 1.w).clamp(0, radius)), + ), + child: child, + ), + ); + } + Widget _buildProfileHeader( SocialChatUserProfileManager ref, List backgroundPhotos, ) { return SizedBox( - height: 450.w, + height: 520.w, child: Stack( children: [ SizedBox( @@ -450,7 +595,7 @@ class _PersonDetailPageState extends State left: 0, right: 0, bottom: 0, - child: Container(color: Color(0xff083b2f)), + child: Container(color: _profileBg), ), Positioned( top: 210.w, @@ -464,133 +609,13 @@ class _PersonDetailPageState extends State _buildHeaderName(ref), SizedBox(height: 5.w), _buildHeaderMeta(ref), - SizedBox(height: 5.w), - Consumer( - builder: (context, ref, child) { - return Container( - height: 72.w, - alignment: AlignmentDirectional.center, - width: ScreenUtil().screenWidth, - margin: EdgeInsets.symmetric(horizontal: 18.w), - padding: EdgeInsets.symmetric(horizontal: 35.w), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - text( - "${counterMap["INTERVIEW"]?.quantity ?? 0}", - fontSize: 17.sp, - textColor: Colors.white, - fontWeight: FontWeight.bold, - ), - text( - SCAppLocalizations.of(context)!.vistors, - fontSize: 14.sp, - textColor: Color(0xffB1B1B1), - ), - ], - ), - onTap: () { - if (widget.isMe == "true") { - SCNavigatorUtils.push( - context, - SCMainRoute.vistors, - ); - } - }, - ), - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Color(0xff333333).withOpacity(0.0), - Color(0xff333333), - Color(0xff333333).withOpacity(0.0), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - height: 25.w, - width: 1.w, - ), - GestureDetector( - onTap: () { - if (widget.isMe == "true") { - SCNavigatorUtils.push( - context, - SCMainRoute.follow, - ); - } - }, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - text( - "${counterMap["SUBSCRIPTION"]?.quantity ?? 0}", - fontSize: 17.sp, - textColor: Colors.white, - fontWeight: FontWeight.bold, - ), - text( - SCAppLocalizations.of(context)!.follow, - fontSize: 14.sp, - textColor: Color(0xffB1B1B1), - ), - ], - ), - ), - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Color(0xff333333).withOpacity(0.0), - Color(0xff333333), - Color(0xff333333).withOpacity(0.0), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - height: 25.w, - width: 1.w, - ), - GestureDetector( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - text( - "${counterMap["FANS"]?.quantity ?? 0}", - fontSize: 17.sp, - textColor: Colors.white, - fontWeight: FontWeight.bold, - ), - text( - SCAppLocalizations.of(context)!.fans, - fontSize: 14.sp, - textColor: Color(0xffB1B1B1), - ), - ], - ), - onTap: () { - if (widget.isMe == "true") { - SCNavigatorUtils.push( - context, - SCMainRoute.fans, - ); - } - }, - ), - ], - ), - ); - }, - ), + SizedBox(height: 4.w), + _buildHeaderBadgePlaceholder(), SizedBox(height: 6.w), + _buildHeaderIntroduction(ref), + SizedBox(height: 12.w), + _buildHeaderStats(), + SizedBox(height: 10.w), ], ), ), @@ -599,13 +624,353 @@ class _PersonDetailPageState extends State ); } + Widget _buildProfileColumnContent() { + return Container( + width: double.infinity, + color: _profileBg, + padding: EdgeInsets.fromLTRB(10.w, 4.w, 10.w, 220.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildProfileSectionTitle("Wall Of Honors"), + SizedBox(height: 12.w), + _buildWallGrid( + itemCount: 4, + itemBuilder: (_) => _buildHonorPlaceholderItem(), + ), + SizedBox(height: 18.w), + _buildProfileSectionTitle( + "Gift Wall", + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.giftWall}?tageId=${widget.tageId}", + ); + }, + ), + SizedBox(height: 12.w), + _buildWallGrid( + itemCount: giftWallList.isEmpty ? 4 : giftWallList.length, + itemBuilder: (index) { + final gift = + index < giftWallList.length ? giftWallList[index] : null; + return _buildGiftWallItem(gift); + }, + ), + ], + ), + ); + } + + Widget _buildProfileSectionTitle(String title, {VoidCallback? onTap}) { + final titleAsset = + title == "Wall Of Honors" + ? "sc_images/person/sc_icon_profile_wall_of_honors_title.png" + : "sc_images/person/sc_icon_profile_gift_wall_title.png"; + final titleHeight = 32.h; + final titleWidth = title == "Wall Of Honors" ? 98.w : 61.w; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: Row( + children: [ + Expanded( + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Image.asset( + titleAsset, + height: titleHeight, + width: titleWidth, + fit: BoxFit.contain, + errorBuilder: + (_, __, ___) => Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: _profileBorder, + fontSize: 19.sp, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w800, + ), + ), + ), + ), + ), + Icon(Icons.chevron_right_rounded, color: Colors.white, size: 28.w), + ], + ), + ); + } + + Widget _buildWallGrid({ + required int itemCount, + required Widget Function(int index) itemBuilder, + }) { + final spacing = + ((ScreenUtil().screenWidth - 20.w - 76.w * 4) / 3) + .clamp(8.w, 26.w) + .toDouble(); + return Wrap( + spacing: spacing, + runSpacing: 17.w, + children: List.generate(itemCount, itemBuilder), + ); + } + + Widget _buildHonorPlaceholderItem() { + 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, + ), + ), + ); + } + + Widget _buildGiftWallItem(SocialChatGiftRes? gift) { + return _buildWallItemFrame( + child: Stack( + children: [ + Positioned.fill( + child: Padding( + padding: EdgeInsets.all(8.w), + child: + gift == null + ? Opacity( + opacity: 0.28, + child: Image.asset( + "sc_images/room/sc_icon_room_music_empty.png", + fit: BoxFit.contain, + ), + ) + : netImage( + url: gift.giftPhoto ?? "", + width: double.infinity, + height: double.infinity, + fit: BoxFit.contain, + noDefaultImg: true, + ), + ), + ), + if (gift != null) + Positioned( + right: 5.w, + bottom: 4.w, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 1.w), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.35), + borderRadius: BorderRadius.circular(8.w), + ), + child: text( + "x${gift.quantity ?? 0}", + fontSize: 10.sp, + textColor: Colors.white, + ), + ), + ), + ], + ), + ); + } + + Widget _buildWallItemFrame({required Widget child}) { + return _buildGradientBorder( + width: 76.w, + height: 76.w, + radius: 8.w, + backgroundColor: _cardBg, + endAlpha: 0.62, + child: child, + ); + } + + Widget _buildBottomActions(SocialChatUserProfileManager ref) { + final hasRoom = (ref.userProfile?.inRoomId ?? "").isNotEmpty; + final safeBottom = MediaQuery.of(context).padding.bottom; + final bottomPadding = safeBottom + 32.w; + final actionRowBottom = bottomPadding; + final enterBottom = actionRowBottom + 36.w + 43.w; + final backgroundHeight = 93.w + safeBottom + 24.w; + final height = hasRoom ? enterBottom + 38.w : backgroundHeight; + + return SizedBox( + height: height, + child: Stack( + clipBehavior: Clip.none, + children: [ + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container(height: backgroundHeight, color: _profileBg), + ), + if (hasRoom) + Positioned( + left: 0, + right: 0, + bottom: enterBottom, + child: _buildEnterRoomPill(ref), + ), + Positioned( + left: 0, + right: 0, + bottom: actionRowBottom, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SCDebounceWidget( + debounceTime: const Duration(milliseconds: 800), + onTap: () { + SCAccountRepository().followUser(widget.tageId).then((v) { + if (!mounted) return; + isFollow = !isFollow; + setState(() {}); + }); + }, + child: _buildBottomButton( + width: 154.w, + icon: + isFollow + ? "sc_images/person/sc_icon_profile_following.png" + : "sc_images/person/sc_icon_person_follow.png", + label: + isFollow + ? SCAppLocalizations.of(context)!.following + : SCAppLocalizations.of(context)!.follow, + ), + ), + SizedBox(width: 20.w), + SCDebounceWidget( + debounceTime: const Duration(milliseconds: 800), + onTap: _openChat, + child: _buildBottomButton( + width: 165.w, + icon: "sc_images/person/sc_icon_person_tochat.png", + label: SCAppLocalizations.of(context)!.chat, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildBottomButton({ + required double width, + required String icon, + required String label, + }) { + return Container( + width: width, + height: 36.w, + decoration: BoxDecoration( + color: const Color(0xff0A342D), + borderRadius: BorderRadius.circular(22.w), + border: Border.all(color: const Color(0xff18F2B1), width: 1.w), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset(icon, width: 28.w, height: 28.w, fit: BoxFit.contain), + SizedBox(width: 6.w), + Flexible( + child: text( + label, + fontSize: 16.sp, + textColor: Colors.white, + fontWeight: FontWeight.w400, + maxLines: 1, + ), + ), + ], + ), + ); + } + + Widget _buildEnterRoomPill(SocialChatUserProfileManager ref) { + return Center( + child: Container( + width: 343.w, + height: 38.w, + decoration: BoxDecoration( + color: const Color(0xff0A342D), + borderRadius: BorderRadius.circular(22.w), + border: Border.all(color: const Color(0xff18F2B1), width: 1.w), + ), + child: Row( + children: [ + SizedBox(width: 5.w), + head(url: ref.userProfile?.userAvatar ?? "", width: 30.w), + SizedBox(width: 8.w), + Expanded( + child: socialchatNickNameText( + ref.userProfile?.userNickname ?? "", + maxWidth: 190.w, + fontSize: 16.sp, + textColor: Colors.white, + fontWeight: FontWeight.w400, + type: ref.userProfile?.getVIP()?.name ?? "", + needScroll: + (ref.userProfile?.userNickname?.characters.length ?? 0) > + 16, + ), + ), + SCDebounceWidget( + debounceTime: const Duration(milliseconds: 800), + onTap: () { + SCRoomUtils.goRoom(ref.userProfile?.inRoomId ?? "", context); + }, + child: Container( + width: 69.w, + height: 27.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: const Color(0xff18F2B1), + borderRadius: BorderRadius.circular(22.w), + ), + child: text( + SCAppLocalizations.of(context)!.enter, + fontSize: 16.sp, + textColor: Colors.black, + fontWeight: FontWeight.w500, + ), + ), + ), + SizedBox(width: 8.w), + ], + ), + ), + ); + } + + Future _openChat() async { + final conversation = V2TimConversation( + type: ConversationType.V2TIM_C2C, + userID: widget.tageId, + conversationID: '', + ); + final started = await Provider.of( + context, + listen: false, + ).startConversation(conversation); + if (!started || !mounted) return; + final json = jsonEncode(conversation.toJson()); + SCNavigatorUtils.push( + context, + "${SCChatRouter.chat}?conversation=${Uri.encodeComponent(json)}", + ); + } + @override Widget build(BuildContext context) { - _tabs.clear(); - // _tabs.add(_buildTab(0, SCAppLocalizations.of(context)!.dynamicT)); - _tabs.add(_buildTab(0, SCAppLocalizations.of(context)!.aboutMe)); - _tabs.add(_buildTab(1, SCAppLocalizations.of(context)!.giftwall)); - // _tabs.add(_buildTab(3, SCAppLocalizations.of(context)!.relationShip)); return Consumer( builder: (context, ref, child) { final bool isProfileLoading = ref.userProfile == null; @@ -646,9 +1011,7 @@ class _PersonDetailPageState extends State fit: BoxFit.cover, ), ), - Expanded( - child: Container(color: const Color(0xff083b2f)), - ), + Expanded(child: Container(color: _profileBg)), ], ), ), @@ -982,126 +1345,14 @@ class _PersonDetailPageState extends State ? Container() : (isProfileLoading || isBlacklistLoading ? const SizedBox.shrink() - : ExtendedNestedScrollView( + : SingleChildScrollView( controller: _scrollController, - onlyOneScrollInBody: true, - headerSliverBuilder: ( - BuildContext context, - bool innerBoxIsScrolled, - ) { - return [ - SliverToBoxAdapter( - child: _buildProfileHeader( - ref, - backgroundPhotos, - ), - ), - ]; - }, - body: Container( - padding: EdgeInsets.symmetric( - horizontal: 3.w, - ), - decoration: BoxDecoration( - gradient: - ref.userProfile?.userSex == 0 - ? LinearGradient( - colors: [ - Color(0xff083b2f), - Color(0xff083b2f), - ], - begin: - AlignmentDirectional.topStart, - end: - AlignmentDirectional - .bottomEnd, - ) - : LinearGradient( - colors: [ - Color(0xff083b2f), - Color(0xff083b2f), - ], - begin: - AlignmentDirectional.topStart, - end: - AlignmentDirectional - .bottomEnd, - ), - ), - child: Column( - children: [ - Row( - children: [ - SizedBox(width: 6.w), - SizedBox( - height: 35.w, - child: TabBar( - tabAlignment: TabAlignment.start, - indicator: - SCFixedWidthTabIndicator( - width: 20.w, - height: 4.w, - gradient: - ref - .userProfile - ?.userSex == - 0 - ? LinearGradient( - colors: [ - Color( - 0xffFFD800, - ), - Color( - 0xffFFD800, - ), - ], - ) - : LinearGradient( - colors: [ - Color( - 0xffFFD800, - ), - Color( - 0xffFFD800, - ), - ], - ), - ), - labelPadding: - EdgeInsets.symmetric( - horizontal: 12.w, - ), - labelColor: Colors.white, - isScrollable: true, - unselectedLabelColor: Color( - 0xffB1B1B1, - ), - labelStyle: TextStyle( - fontSize: 15.sp, - fontWeight: FontWeight.bold, - ), - unselectedLabelStyle: TextStyle( - fontSize: 13.sp, - ), - indicatorColor: - Colors.transparent, - dividerColor: Colors.transparent, - controller: _tabController, - tabs: _tabs, - ), - ), - SizedBox(width: 6.w), - ], - ), - Expanded( - child: TabBarView( - controller: _tabController, - children: _pages, - ), - ), - SizedBox(height: 25.w), - ], - ), + physics: const BouncingScrollPhysics(), + child: Column( + children: [ + _buildProfileHeader(ref, backgroundPhotos), + _buildProfileColumnContent(), + ], ), )), ), @@ -1117,187 +1368,7 @@ class _PersonDetailPageState extends State ? Container() : (isLoading ? Container() - : Container( - alignment: AlignmentDirectional.center, - width: ScreenUtil().screenWidth, - height: 75.w, - decoration: BoxDecoration( - color: Color(0xff18F2B1).withOpacity(0.1), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(10.w), - topRight: Radius.circular(10.w), - ), - ), - child: Column( - children: [ - SizedBox(height: 10.w), - Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - SCDebounceWidget( - debounceTime: Duration( - milliseconds: 800, - ), - child: SizedBox( - width: 110.w, - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Image.asset( - "sc_images/person/sc_icon_person_in_room.png", - width: 23.w, - ), - SizedBox(height: 5.w), - Container( - margin: EdgeInsets.only( - bottom: 0.w, - ), - child: text( - SCAppLocalizations.of( - context, - )!.inRoom, - textColor: - SocialChatTheme - .primaryLight, - fontWeight: - FontWeight.w600, - fontSize: 14.sp, - ), - ), - ], - ), - ), - onTap: () { - if ((ref - .userProfile - ?.inRoomId ?? - "") - .isNotEmpty) { - SCRoomUtils.goRoom( - ref.userProfile?.inRoomId ?? - "", - context, - ); - } - }, - ), - SizedBox(width: 10.w), - SCDebounceWidget( - debounceTime: Duration( - milliseconds: 800, - ), - child: Container( - width: 110.w, - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Image.asset( - "sc_images/person/sc_icon_person_tochat.png", - width: 23.w, - ), - SizedBox(height: 5.w), - Container( - margin: EdgeInsets.only( - bottom: 0.w, - ), - child: text( - SCAppLocalizations.of( - context, - )!.message, - textColor: - SocialChatTheme - .primaryLight, - fontWeight: - FontWeight.w600, - fontSize: 14.sp, - ), - ), - ], - ), - ), - onTap: () async { - var conversation = - V2TimConversation( - type: - ConversationType - .V2TIM_C2C, - userID: widget.tageId, - conversationID: '', - ); - var bool = await Provider.of< - RtmProvider - >( - context, - listen: false, - ).startConversation( - conversation, - ); - if (!bool) return; - var json = jsonEncode( - conversation.toJson(), - ); - SCNavigatorUtils.push( - context, - "${SCChatRouter.chat}?conversation=${Uri.encodeComponent(json)}", - ); - }, - ), - SizedBox(width: 10.w), - SCDebounceWidget( - debounceTime: Duration( - milliseconds: 800, - ), - child: Container( - width: 110.w, - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Image.asset( - "sc_images/person/sc_icon_person_follow.png", - width: 23.w, - ), - SizedBox(height: 5.w), - Container( - margin: EdgeInsets.only( - bottom: 0.w, - ), - child: text( - isFollow - ? SCAppLocalizations.of( - context, - )!.following - : SCAppLocalizations.of( - context, - )!.follow, - textColor: - SocialChatTheme - .primaryLight, - fontWeight: - FontWeight.w600, - fontSize: 14.sp, - ), - ), - ], - ), - ), - onTap: () { - SCAccountRepository() - .followUser(widget.tageId) - .then((v) { - isFollow = !isFollow; - setState(() {}); - }); - }, - ), - ], - ), - ], - ), - )), + : _buildBottomActions(ref)), ), ], ), diff --git a/lib/modules/user/profile/profile_gift_wall_detail_page.dart b/lib/modules/user/profile/profile_gift_wall_detail_page.dart new file mode 100644 index 0000000..4439cc8 --- /dev/null +++ b/lib/modules/user/profile/profile_gift_wall_detail_page.dart @@ -0,0 +1,495 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_gift_repository_imp.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; + +class ProfileGiftWallDetailPage extends StatefulWidget { + const ProfileGiftWallDetailPage({super.key, required this.tageId}); + + final String tageId; + + @override + State createState() => + _ProfileGiftWallDetailPageState(); +} + +class _ProfileGiftWallDetailPageState extends State { + static const Color _profileBg = Color(0xff072121); + static const Color _cardBg = Color(0xff08251E); + static const Color _profileBorder = Color(0xffB2FBCC); + + SocialChatUserProfile? _profile; + List _gifts = []; + bool _loadingGifts = true; + + @override + void initState() { + super.initState(); + _primeProfile(); + _loadProfile(); + _loadGiftWall(); + } + + void _primeProfile() { + final currentProfile = + Provider.of( + context, + listen: false, + ).userProfile; + if (currentProfile?.id == widget.tageId) { + _profile = currentProfile; + } + } + + void _loadProfile() { + SCAccountRepository() + .loadUserInfo(widget.tageId) + .then((profile) { + if (!mounted) return; + setState(() { + _profile = profile; + }); + }) + .catchError((_) {}); + } + + Future _loadGiftWall() async { + final appGeneralManager = Provider.of( + context, + listen: false, + ); + try { + final result = await SCGiftRepositoryImp().giftWall(widget.tageId); + final resolvedGifts = await _resolveGiftWallMeta( + result, + appGeneralManager, + ); + if (!mounted) return; + setState(() { + _gifts = resolvedGifts; + _loadingGifts = false; + }); + } catch (_) { + if (!mounted) return; + setState(() { + _loadingGifts = false; + }); + } + } + + Future> _resolveGiftWallMeta( + List gifts, + SCAppGeneralManager appGeneralManager, + ) async { + if (gifts.isEmpty) return gifts; + if (appGeneralManager.giftResList.isEmpty) { + await appGeneralManager.giftList(includeCustomized: false); + } + return gifts + .map((gift) => _mergeGiftMeta(gift, appGeneralManager)) + .toList(); + } + + SocialChatGiftRes _mergeGiftMeta( + SocialChatGiftRes gift, + SCAppGeneralManager appGeneralManager, + ) { + final meta = _findGiftMeta(gift, appGeneralManager); + if (meta == null) return gift; + return gift.copyWith( + giftName: _firstNonBlank([gift.giftName, meta.giftName]), + giftCandy: gift.giftCandy ?? meta.giftCandy, + giftIntegral: gift.giftIntegral ?? meta.giftIntegral, + giftPhoto: _firstNonBlank([gift.giftPhoto, meta.giftPhoto]), + giftSourceUrl: _firstNonBlank([gift.giftSourceUrl, meta.giftSourceUrl]), + giftCode: _firstNonBlank([gift.giftCode, meta.giftCode]), + standardId: _firstNonBlank([gift.standardId, meta.standardId]), + ); + } + + SocialChatGiftRes? _findGiftMeta( + SocialChatGiftRes gift, + SCAppGeneralManager appGeneralManager, + ) { + final idCandidates = [ + gift.standardId ?? "", + gift.id ?? "", + gift.giftCode ?? "", + ].where((id) => id.trim().isNotEmpty && id.trim() != "0"); + + for (final id in idCandidates) { + final meta = appGeneralManager.getGiftByIdOrStandardId(id); + if (meta != null) return meta; + } + + final giftPhoto = (gift.giftPhoto ?? "").trim(); + final giftCode = (gift.giftCode ?? "").trim(); + for (final meta in appGeneralManager.giftResList) { + if (giftPhoto.isNotEmpty && giftPhoto == (meta.giftPhoto ?? "").trim()) { + return meta; + } + if (giftCode.isNotEmpty && giftCode == (meta.giftCode ?? "").trim()) { + return meta; + } + } + return null; + } + + String? _firstNonBlank(Iterable values) { + for (final value in values) { + final text = value?.trim(); + if (text != null && text.isNotEmpty) { + return text; + } + } + return null; + } + + int get _receivedCount { + return _gifts.fold(0, (sum, gift) => sum + _giftQuantity(gift)); + } + + num get _totalValue { + return _gifts.fold( + 0, + (sum, gift) => sum + (_giftAmount(gift) * _giftQuantity(gift)), + ); + } + + int _giftQuantity(SocialChatGiftRes gift) { + return int.tryParse(gift.quantity ?? "") ?? 0; + } + + num _giftAmount(SocialChatGiftRes gift) { + return gift.giftCandy ?? gift.giftIntegral ?? 0; + } + + String _formatNumber(num value) { + if (value % 1 == 0) return value.toInt().toString(); + return value.toStringAsFixed(1); + } + + Widget _buildGradientBorder({ + double? width, + double? height, + required double radius, + required Widget child, + Color backgroundColor = _cardBg, + double endAlpha = 0.42, + }) { + final borderRadius = BorderRadius.circular(radius); + return Container( + width: width, + height: height, + decoration: BoxDecoration( + borderRadius: borderRadius, + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [_profileBorder, _profileBorder.withValues(alpha: endAlpha)], + ), + ), + padding: EdgeInsets.all(1.w), + child: Container( + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular((radius - 1.w).clamp(0, radius)), + ), + child: child, + ), + ); + } + + Widget _buildTopBar() { + return SizedBox( + width: double.infinity, + height: 54.w, + child: Stack( + alignment: Alignment.center, + children: [ + PositionedDirectional( + start: 8.w, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => SCNavigatorUtils.goBack(context), + child: SizedBox( + width: 52.w, + height: 52.w, + child: Icon( + Icons.keyboard_arrow_left_rounded, + color: Colors.white, + size: 38.w, + ), + ), + ), + ), + Text( + "Gift Wall", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 19.sp, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } + + Widget _buildProfileHeader() { + return Column( + children: [ + SizedBox(height: 12.w), + head( + url: _profile?.userAvatar ?? "", + width: 104.w, + headdress: _profile?.getHeaddress()?.sourceUrl, + ), + SizedBox(height: 10.w), + socialchatNickNameText( + _profile?.userNickname ?? "", + maxWidth: 300.w, + fontSize: 20, + textColor: Colors.white, + fontWeight: FontWeight.w400, + type: _profile?.getVIP()?.name ?? "", + needScroll: (_profile?.userNickname?.characters.length ?? 0) > 13, + ), + SizedBox(height: 18.w), + _buildSummaryCard(), + ], + ); + } + + Widget _buildSummaryCard() { + return _buildGradientBorder( + width: 355.w, + height: 51.w, + radius: 8.w, + backgroundColor: _cardBg, + endAlpha: 0.52, + child: Row( + children: [ + _buildSummaryItem( + value: _formatNumber(_receivedCount), + label: "Received", + ), + _buildSummaryDivider(), + _buildSummaryItem( + value: _formatNumber(_totalValue), + label: "Total Value", + showCoin: true, + ), + ], + ), + ); + } + + Widget _buildSummaryItem({ + required String value, + required String label, + bool showCoin = false, + }) { + return Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (showCoin) ...[_buildCoinIcon(14.w), SizedBox(width: 3.w)], + text( + value, + fontSize: 17, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + ], + ), + text(label, fontSize: 14, textColor: const Color(0xffB1B1B1)), + ], + ), + ); + } + + Widget _buildSummaryDivider() { + return Container( + height: 28.w, + width: 1.w, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color.fromRGBO(255, 255, 255, 0), + Colors.white, + Color.fromRGBO(255, 255, 255, 0), + ], + stops: [0, 0.4862, 1], + ), + ), + ); + } + + Widget _buildGiftGrid() { + if (_loadingGifts) { + return SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.only(top: 80.w), + child: Center( + child: SizedBox( + width: 24.w, + height: 24.w, + child: const CircularProgressIndicator( + strokeWidth: 2, + color: _profileBorder, + ), + ), + ), + ), + ); + } + + if (_gifts.isEmpty) { + return SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.only(top: 90.w), + child: Opacity( + opacity: 0.42, + child: Image.asset( + "sc_images/room/sc_icon_room_music_empty.png", + width: 72.w, + height: 72.w, + fit: BoxFit.contain, + ), + ), + ), + ); + } + + return SliverPadding( + padding: EdgeInsets.fromLTRB(10.w, 34.w, 10.w, 0), + sliver: SliverGrid( + delegate: SliverChildBuilderDelegate( + (context, index) => _buildGiftItem(_gifts[index]), + childCount: _gifts.length, + ), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisSpacing: 25.w, + crossAxisSpacing: 22.w, + childAspectRatio: 104 / 130, + ), + ), + ); + } + + Widget _buildGiftItem(SocialChatGiftRes gift) { + final quantity = _giftQuantity(gift); + final giftName = + (gift.giftName ?? "").trim().isEmpty ? "--" : gift.giftName!.trim(); + return _buildGradientBorder( + radius: 8.w, + backgroundColor: _cardBg, + endAlpha: 0.62, + child: Padding( + padding: EdgeInsets.fromLTRB(10.w, 10.w, 10.w, 8.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Center( + child: netImage( + url: gift.giftPhoto ?? "", + width: 82.w, + height: 74.w, + fit: BoxFit.contain, + noDefaultImg: true, + ), + ), + ), + SizedBox(height: 7.w), + text( + "$giftName*$quantity", + fontSize: 13, + textColor: Colors.white, + maxLines: 1, + ), + SizedBox(height: 5.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildCoinIcon(12.w), + SizedBox(width: 4.w), + Flexible( + child: text( + _formatNumber(_giftAmount(gift)), + fontSize: 11, + textColor: Colors.white, + maxLines: 1, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildCoinIcon(double size) { + return SizedBox( + width: size, + height: size, + child: ClipRect( + child: FittedBox( + fit: BoxFit.cover, + alignment: Alignment.bottomCenter, + child: Image.asset("sc_images/room/sc_icon_luckgift_coins_anim.webp"), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final bottomPadding = MediaQuery.of(context).padding.bottom + 22.w; + return Scaffold( + backgroundColor: _profileBg, + body: SafeArea( + bottom: false, + child: CustomScrollView( + physics: const BouncingScrollPhysics(), + slivers: [ + SliverToBoxAdapter( + child: Column(children: [_buildTopBar(), _buildProfileHeader()]), + ), + _buildGiftGrid(), + SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.only(top: 54.w, bottom: bottomPadding), + child: Center( + child: text( + "No Further Data Available~", + fontSize: 13, + textColor: const Color(0xffB1B1B1), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/shared/business_logic/models/res/gift_res.dart b/lib/shared/business_logic/models/res/gift_res.dart index a35d53d..2cef234 100644 --- a/lib/shared/business_logic/models/res/gift_res.dart +++ b/lib/shared/business_logic/models/res/gift_res.dart @@ -1,182 +1,250 @@ -/// id : 0 -/// giftCode : "" -/// giftPhoto : "" -/// giftSourceUrl : "" -/// giftCandy : 0.0 -/// giftIntegral : 0.0 -/// special : "" -/// type : "" -/// giftTab : "" -/// standardId : 0 -/// explanationGift : false -/// account : "" -/// userId : 0 -/// giftName : "" -/// expiredTime : 0 - -class SocialChatGiftRes { - SocialChatGiftRes({ - String? id, - String? giftCode, - String? giftPhoto, - String? giftSourceUrl, - num? giftCandy, - num? activityId, - num? giftIntegral, - String? special, - String? type, - String? giftTab, - String? standardId, - String? jumpUrl, - String? bannerUrl, - bool? explanationGift, - String? account, - String? userId, - String? giftName, - String? quantity, - num? expiredTime,}){ - _id = id; - _giftCode = giftCode; - _giftPhoto = giftPhoto; - _giftSourceUrl = giftSourceUrl; - _giftCandy = giftCandy; - _activityId = activityId; - _giftIntegral = giftIntegral; - _special = special; - _type = type; - _giftTab = giftTab; - _standardId = standardId; - _jumpUrl = jumpUrl; - _bannerUrl = bannerUrl; - _explanationGift = explanationGift; - _account = account; - _userId = userId; - _giftName = giftName; - _quantity = quantity; - _expiredTime = expiredTime; -} - - SocialChatGiftRes.fromJson(dynamic json) { - _id = json['id']; - _giftCode = json['giftCode']; - _giftPhoto = json['giftPhoto']; - _giftSourceUrl = json['giftSourceUrl']; - _giftCandy = json['giftCandy']; - _activityId = json['activityId']; - _giftIntegral = json['giftIntegral']; - _special = json['special']; - _type = json['type']; - _giftTab = json['giftTab']; - _standardId = json['standardId']; - _jumpUrl = json['jumpUrl']; - _bannerUrl = json['bannerUrl']; - _explanationGift = json['explanationGift']; - _account = json['account']; - _userId = json['userId']; - _giftName = json['giftName']; - _quantity = json['quantity']; - _expiredTime = json['expiredTime']; - } - String? _id; - String? _giftCode; - String? _giftPhoto; - String? _giftSourceUrl; - num? _giftCandy; - num? _activityId; - num? _giftIntegral; - String? _special; - String? _type; - String? _giftTab; - String? _standardId; - String? _jumpUrl; - String? _bannerUrl; - bool? _explanationGift; - String? _account; - String? _userId; - String? _giftName; - String? _quantity; - num? _expiredTime; -SocialChatGiftRes copyWith({ String? id, - String? giftCode, - String? giftPhoto, - String? giftSourceUrl, - num? giftCandy, - num? activityId, - num? giftIntegral, - String? special, - String? type, - String? giftTab, - String? standardId, - String? jumpUrl, - String? bannerUrl, - bool? explanationGift, - String? account, - String? userId, - String? giftName, - String? quantity, - num? expiredTime, -}) => SocialChatGiftRes( id: id ?? _id, - giftCode: giftCode ?? _giftCode, - giftPhoto: giftPhoto ?? _giftPhoto, - giftSourceUrl: giftSourceUrl ?? _giftSourceUrl, - giftCandy: giftCandy ?? _giftCandy, - activityId: activityId ?? _activityId, - giftIntegral: giftIntegral ?? _giftIntegral, - special: special ?? _special, - type: type ?? _type, - giftTab: giftTab ?? _giftTab, - standardId: standardId ?? _standardId, - jumpUrl: jumpUrl ?? _jumpUrl, - bannerUrl: bannerUrl ?? _bannerUrl, - explanationGift: explanationGift ?? _explanationGift, - account: account ?? _account, - userId: userId ?? _userId, - giftName: giftName ?? _giftName, - quantity: quantity ?? _quantity, - expiredTime: expiredTime ?? _expiredTime, -); - String? get id => _id; - String? get giftCode => _giftCode; - String? get giftPhoto => _giftPhoto; - String? get giftSourceUrl => _giftSourceUrl; - num? get giftCandy => _giftCandy; - num? get activityId => _activityId; - num? get giftIntegral => _giftIntegral; - String? get special => _special; - String? get type => _type; - String? get giftTab => _giftTab; - String? get standardId => _standardId; - String? get jumpUrl => _jumpUrl; - String? get bannerUrl => _bannerUrl; - bool? get explanationGift => _explanationGift; - String? get account => _account; - String? get userId => _userId; - String? get giftName => _giftName; - String? get quantity => _quantity; - num? get expiredTime => _expiredTime; - - Map toJson() { - final map = {}; - map['id'] = _id; - map['giftCode'] = _giftCode; - map['giftPhoto'] = _giftPhoto; - map['giftSourceUrl'] = _giftSourceUrl; - map['giftCandy'] = _giftCandy; - map['activityId'] = _activityId; - map['giftIntegral'] = _giftIntegral; - map['special'] = _special; - map['type'] = _type; - map['giftTab'] = _giftTab; - map['standardId'] = _standardId; - map['jumpUrl'] = _jumpUrl; - map['bannerUrl'] = _bannerUrl; - map['explanationGift'] = _explanationGift; - map['account'] = _account; - map['userId'] = _userId; - map['giftName'] = _giftName; - map['quantity'] = _quantity; - map['expiredTime'] = _expiredTime; - return map; - } - -} \ No newline at end of file +// id : 0 +// giftCode : "" +// giftPhoto : "" +// giftSourceUrl : "" +// giftCandy : 0.0 +// giftIntegral : 0.0 +// special : "" +// type : "" +// giftTab : "" +// standardId : 0 +// explanationGift : false +// account : "" +// userId : 0 +// giftName : "" +// expiredTime : 0 + +class SocialChatGiftRes { + SocialChatGiftRes({ + String? id, + String? giftCode, + String? giftPhoto, + String? giftSourceUrl, + num? giftCandy, + num? activityId, + num? giftIntegral, + String? special, + String? type, + String? giftTab, + String? standardId, + String? jumpUrl, + String? bannerUrl, + bool? explanationGift, + String? account, + String? userId, + String? giftName, + String? quantity, + num? expiredTime, + }) { + _id = id; + _giftCode = giftCode; + _giftPhoto = giftPhoto; + _giftSourceUrl = giftSourceUrl; + _giftCandy = giftCandy; + _activityId = activityId; + _giftIntegral = giftIntegral; + _special = special; + _type = type; + _giftTab = giftTab; + _standardId = standardId; + _jumpUrl = jumpUrl; + _bannerUrl = bannerUrl; + _explanationGift = explanationGift; + _account = account; + _userId = userId; + _giftName = giftName; + _quantity = quantity; + _expiredTime = expiredTime; + } + + SocialChatGiftRes.fromJson(dynamic json) { + final giftConfig = + _giftResMap(json['giftConfig']) ?? + _giftResMap(json['gift']) ?? + _giftResMap(json['giftInfo']) ?? + _giftResMap(json['giftVO']); + + _id = _giftResString(json['id'] ?? giftConfig?['id']); + _giftCode = _giftResString(json['giftCode'] ?? giftConfig?['giftCode']); + _giftPhoto = _giftResString( + json['giftPhoto'] ?? + json['giftCover'] ?? + json['cover'] ?? + giftConfig?['giftPhoto'] ?? + giftConfig?['giftCover'] ?? + giftConfig?['cover'], + ); + _giftSourceUrl = _giftResString( + json['giftSourceUrl'] ?? + json['sourceUrl'] ?? + giftConfig?['giftSourceUrl'] ?? + giftConfig?['sourceUrl'], + ); + _giftCandy = _giftResNum( + json['giftCandy'] ?? + json['giftAmount'] ?? + json['giftValue'] ?? + json['amount'] ?? + json['price'] ?? + json['value'] ?? + json['coins'] ?? + giftConfig?['giftCandy'] ?? + giftConfig?['giftAmount'] ?? + giftConfig?['giftValue'] ?? + giftConfig?['amount'] ?? + giftConfig?['price'] ?? + giftConfig?['value'] ?? + giftConfig?['coins'], + ); + _activityId = _giftResNum(json['activityId'] ?? giftConfig?['activityId']); + _giftIntegral = _giftResNum( + json['giftIntegral'] ?? + json['integral'] ?? + giftConfig?['giftIntegral'] ?? + giftConfig?['integral'], + ); + _special = _giftResString(json['special'] ?? giftConfig?['special']); + _type = _giftResString(json['type'] ?? giftConfig?['type']); + _giftTab = _giftResString(json['giftTab'] ?? giftConfig?['giftTab']); + _standardId = _giftResString( + json['standardId'] ?? giftConfig?['standardId'], + ); + _jumpUrl = _giftResString(json['jumpUrl'] ?? giftConfig?['jumpUrl']); + _bannerUrl = _giftResString(json['bannerUrl'] ?? giftConfig?['bannerUrl']); + _explanationGift = json['explanationGift']; + _account = _giftResString(json['account'] ?? giftConfig?['account']); + _userId = _giftResString(json['userId'] ?? giftConfig?['userId']); + _giftName = _giftResString( + json['giftName'] ?? + json['name'] ?? + giftConfig?['giftName'] ?? + giftConfig?['name'], + ); + _quantity = _giftResString( + json['quantity'] ?? json['giftQuantity'] ?? json['count'] ?? json['num'], + ); + _expiredTime = _giftResNum( + json['expiredTime'] ?? giftConfig?['expiredTime'], + ); + } + String? _id; + String? _giftCode; + String? _giftPhoto; + String? _giftSourceUrl; + num? _giftCandy; + num? _activityId; + num? _giftIntegral; + String? _special; + String? _type; + String? _giftTab; + String? _standardId; + String? _jumpUrl; + String? _bannerUrl; + bool? _explanationGift; + String? _account; + String? _userId; + String? _giftName; + String? _quantity; + num? _expiredTime; + SocialChatGiftRes copyWith({ + String? id, + String? giftCode, + String? giftPhoto, + String? giftSourceUrl, + num? giftCandy, + num? activityId, + num? giftIntegral, + String? special, + String? type, + String? giftTab, + String? standardId, + String? jumpUrl, + String? bannerUrl, + bool? explanationGift, + String? account, + String? userId, + String? giftName, + String? quantity, + num? expiredTime, + }) => SocialChatGiftRes( + id: id ?? _id, + giftCode: giftCode ?? _giftCode, + giftPhoto: giftPhoto ?? _giftPhoto, + giftSourceUrl: giftSourceUrl ?? _giftSourceUrl, + giftCandy: giftCandy ?? _giftCandy, + activityId: activityId ?? _activityId, + giftIntegral: giftIntegral ?? _giftIntegral, + special: special ?? _special, + type: type ?? _type, + giftTab: giftTab ?? _giftTab, + standardId: standardId ?? _standardId, + jumpUrl: jumpUrl ?? _jumpUrl, + bannerUrl: bannerUrl ?? _bannerUrl, + explanationGift: explanationGift ?? _explanationGift, + account: account ?? _account, + userId: userId ?? _userId, + giftName: giftName ?? _giftName, + quantity: quantity ?? _quantity, + expiredTime: expiredTime ?? _expiredTime, + ); + String? get id => _id; + String? get giftCode => _giftCode; + String? get giftPhoto => _giftPhoto; + String? get giftSourceUrl => _giftSourceUrl; + num? get giftCandy => _giftCandy; + num? get activityId => _activityId; + num? get giftIntegral => _giftIntegral; + String? get special => _special; + String? get type => _type; + String? get giftTab => _giftTab; + String? get standardId => _standardId; + String? get jumpUrl => _jumpUrl; + String? get bannerUrl => _bannerUrl; + bool? get explanationGift => _explanationGift; + String? get account => _account; + String? get userId => _userId; + String? get giftName => _giftName; + String? get quantity => _quantity; + num? get expiredTime => _expiredTime; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['giftCode'] = _giftCode; + map['giftPhoto'] = _giftPhoto; + map['giftSourceUrl'] = _giftSourceUrl; + map['giftCandy'] = _giftCandy; + map['activityId'] = _activityId; + map['giftIntegral'] = _giftIntegral; + map['special'] = _special; + map['type'] = _type; + map['giftTab'] = _giftTab; + map['standardId'] = _standardId; + map['jumpUrl'] = _jumpUrl; + map['bannerUrl'] = _bannerUrl; + map['explanationGift'] = _explanationGift; + map['account'] = _account; + map['userId'] = _userId; + map['giftName'] = _giftName; + map['quantity'] = _quantity; + map['expiredTime'] = _expiredTime; + return map; + } +} + +Map? _giftResMap(dynamic value) { + if (value is Map) return value; + if (value is Map) return Map.from(value); + return null; +} + +String? _giftResString(dynamic value) { + if (value == null) return null; + return value.toString(); +} + +num? _giftResNum(dynamic value) { + if (value == null) return null; + if (value is num) return value; + return num.tryParse(value.toString()); +} diff --git a/sc_images/person/sc_icon_profile_following.png b/sc_images/person/sc_icon_profile_following.png new file mode 100644 index 0000000..01fe763 Binary files /dev/null and b/sc_images/person/sc_icon_profile_following.png differ diff --git a/sc_images/person/sc_icon_profile_gift_wall_title.png b/sc_images/person/sc_icon_profile_gift_wall_title.png new file mode 100644 index 0000000..6b4ae74 Binary files /dev/null and b/sc_images/person/sc_icon_profile_gift_wall_title.png differ diff --git a/sc_images/person/sc_icon_profile_wall_of_honors_title.png b/sc_images/person/sc_icon_profile_wall_of_honors_title.png new file mode 100644 index 0000000..961ba28 Binary files /dev/null and b/sc_images/person/sc_icon_profile_wall_of_honors_title.png differ diff --git a/需求进度.md b/需求进度.md index b2225ce..372f720 100644 --- a/需求进度.md +++ b/需求进度.md @@ -9,7 +9,14 @@ - 第一步已去掉个人资料页加载阶段的骨架屏:`ref.userProfile` 或拉黑状态还没返回时,正文区域改为空占位,不再展示灰色骨架块;资料返回后仍按原来的 `Header + About/Giftwall Tab` 结构渲染。 - 第二步已将个人资料页上方默认背景图从旧的 `sc_images/person/sc_icon_my_head_bg_defalt.png` 切换为新素材 `sc_images/person/sc_icon_profile_card_default_bg.png`;用户已有可用 `backgroundPhotos` 时仍优先展示用户自定义背景。 - 第三步已调整头部资料区对齐:深绿资料区去掉顶部圆角改为矩形;头像/头像框移动到中间;用户名居中单独一行;ID、复制按钮、性别年龄和国旗收成同一行居中展示,复制按钮点击后仍写入剪贴板并提示 copied。 -- 验证进度:`dart format lib/modules/user/profile/person_detail_page.dart` 已执行;`flutter analyze --no-fatal-infos lib/modules/user/profile/person_detail_page.dart` 无 error/warning,仅保留该旧文件原有的 info 级代码风格提示。下一步再继续逐项对齐资料页 UI。 +- 第四步已继续对齐设计稿:ID 行下方新增一行徽章预留占位;用户 `autograph/bio` 已前移到徽章占位下方并按 `Introduction:` 展示;Visitor/Follow/Fans 统计块改为 `355*51`、圆角 `8` 的纵向渐隐绿色描边,中间分割线改为高度 `28` 的白色纵向渐隐线。 +- 第五步已把个人资料页大背景切到 `#072121`,Visitor/Follow/Fans 渐变卡内部保持 `#08251E`;下方 `About me / Gift wall` tab 结构已移除,改为设计图同款纵向 `Wall Of Honors` + `Gift Wall` section。`Wall Of Honors` 现用语音房 music 空态图做占位;两个 section 的 item 统一为 `76*76`、圆角 `8`、纵向渐隐绿色描边,`Gift Wall` 继续复用原礼物墙接口数据。 +- 第六步已继续对稿:`Wall Of Honors` 与 `Gift Wall` 标题改为图片资源渲染,新增 `sc_icon_profile_wall_of_honors_title.png` 与 `sc_icon_profile_gift_wall_title.png`;荣誉/礼物 item 的描边底部透明度提高,解决下边线不明显的问题;非本人资料页底部操作区改为设计稿样式,`Follow` 为 `104*36`、`Chat` 为 `215*36`,同为 `#0A342D` 背景、`#18F2B1` 1px 边框、22 圆角和 28 图标;在房间用户会额外显示悬浮 `343*38` 房间入口,左侧头像昵称,右侧 `69*27` Enter 按钮,和底部按钮行保持 43 间距。 +- 第七步已按 2026-05-07 最新反馈微调:重新覆盖 `Wall Of Honors / Gift Wall` 标题 PNG 风格;Visitor/Follow/Fans 统计卡底部描边透明度提高;荣誉/礼物 item 底部描边透明度继续提高;底部按钮区增加 `#072121`、高度 `93` 的背景;`Follow` 宽度增至 `154`,`Chat` 宽度缩至 `165`;Chat 图标改回旧图标 `sc_icon_person_tochat.png`;Following 状态新增并使用 `sc_icon_profile_following.png`。 +- 第八步已继续按 2026-05-07 最新反馈收口:确认 `Wall Of Honors / Gift Wall` 标题直接使用用户替换后的 PNG 素材,不再自绘标题;非本人资料页底部按钮整体上移并扩大底部安全留白,减少被系统 Home Indicator 遮挡;`Gift Wall` 标题点击已进入新的独立礼物墙页面,页面包含返回栏、头像头像框、昵称、Received/Total Value 统计卡、三列礼物列表和底部 `No Further Data Available~` 文案。 +- 第九步已修正 Gift Wall 详情页初版问题:顶部导航栏补齐满屏宽度,避免返回箭头挤进标题;头像缩小到 `104`,昵称字号改为正常单次缩放;金币展示改用现有单枚金币动效素材;礼物数据模型兼容 `giftConfig/name/amount/price/value/coins/giftQuantity` 等接口字段,避免礼物名和金额接口有值但页面显示 `-- / 0`。 +- 第十步已为 Gift Wall 详情页补齐礼物名称/金额解析链路:礼物墙接口返回后会再用全局 `/gift/list` 配置表按 `standardId / id / giftCode / giftPhoto` 做二次匹配,补齐缺失的 `giftName / giftCandy / giftIntegral / giftPhoto`,用于单个礼物卡片和 `Total Value` 统计。 +- 验证进度:`dart format lib/modules/user/profile/profile_gift_wall_detail_page.dart lib/shared/business_logic/models/res/gift_res.dart` 已执行;`flutter analyze --no-fatal-infos lib/modules/user/profile/profile_gift_wall_detail_page.dart lib/shared/business_logic/models/res/gift_res.dart lib/modules/user/profile/person_detail_page.dart lib/modules/index/main_route.dart` 无 error/warning,仅保留旧文件原有的 info 级代码风格提示。下一步再继续逐项对齐资料页 UI。 ## 本轮 Android 语言房音乐功能(进行中) - 已查阅桌面《Android音乐添加流程.md》,并产出可执行方案文档 `docs/android-room-music-execution-plan.md`,覆盖功能范围、模块拆分、Android 权限、声网混音链路、UI 流程、风险点和 GPT 5.5 预估开发工时。