yumi-flutter/lib/modules/user/me_page2.dart

1269 lines
39 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
import 'package:yumi/app_localizations.dart';
import 'package:yumi/modules/index/main_route.dart';
import 'package:yumi/modules/store/store_route.dart';
import 'package:yumi/modules/user/settings/settings_route.dart';
import 'package:yumi/modules/user/vip/vip_route.dart';
import 'package:yumi/modules/wallet/wallet_route.dart';
import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/services/general/sc_app_general_manager.dart';
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_vip_res.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart';
import 'package:yumi/shared/tools/sc_lk_event_bus.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
import 'package:yumi/ui_kit/widgets/id/sc_special_id_badge.dart';
import 'package:yumi/ui_kit/widgets/sc_home_shell_background.dart';
class MePage2 extends StatefulWidget {
const MePage2({super.key, this.currentIndexListenable, this.tabIndex = 3});
final ValueListenable<int>? currentIndexListenable;
final int tabIndex;
@override
State<MePage2> createState() => _MePage2State();
}
class _MePage2State extends State<MePage2> with WidgetsBindingObserver {
static const Duration _counterStaleDuration = Duration(seconds: 30);
static const String _cpActivityUrl =
'https://h5.global-interaction.com/activity/cp-space/yumi.html';
static const String _inviteActivityUrl =
'https://h5.haiyihy.com/app-invite/index.html';
Map<String, SCUserCounterRes> _counterMap = {};
SCVipStatusRes? _vipStatus;
bool _isVipStatusLoading = false;
bool _isCounterLoading = false;
bool _counterRefreshQueued = false;
bool _counterReloadAfterCurrent = false;
DateTime? _lastCounterLoadedAt;
StreamSubscription<SCUserRelationChangedEvent>? _relationChangedSubscription;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
widget.currentIndexListenable?.addListener(_handleTabVisibilityChanged);
_relationChangedSubscription = eventBus
.on<SCUserRelationChangedEvent>()
.listen(_handleUserRelationChanged);
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadPageData();
});
}
@override
void didUpdateWidget(covariant MePage2 oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.currentIndexListenable == widget.currentIndexListenable) {
return;
}
oldWidget.currentIndexListenable?.removeListener(
_handleTabVisibilityChanged,
);
widget.currentIndexListenable?.addListener(_handleTabVisibilityChanged);
}
@override
void dispose() {
widget.currentIndexListenable?.removeListener(_handleTabVisibilityChanged);
_relationChangedSubscription?.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed && _isCurrentTabVisible) {
_refreshCounterIfNeeded();
}
}
void _loadPageData() {
final profileManager = Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
);
profileManager.fetchUserProfileData();
profileManager.getUserIdentity();
profileManager.balance();
_loadCounter();
_loadVipStatus();
}
bool get _isCurrentTabVisible {
final currentIndex = widget.currentIndexListenable?.value;
return currentIndex == null || currentIndex == widget.tabIndex;
}
bool get _isCounterStale {
final lastLoadedAt = _lastCounterLoadedAt;
if (lastLoadedAt == null) {
return true;
}
return DateTime.now().difference(lastLoadedAt) >= _counterStaleDuration;
}
void _handleTabVisibilityChanged() {
if (!_isCurrentTabVisible) {
return;
}
_refreshCounterIfNeeded();
}
void _refreshCounterIfNeeded() {
if (_counterRefreshQueued || _isCounterStale) {
unawaited(_loadCounter(force: true));
}
}
void _handleUserRelationChanged(SCUserRelationChangedEvent event) {
final currentUserId =
AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? "";
if (currentUserId.isEmpty) {
return;
}
final actorUserId = event.actorUserId.trim();
final targetUserId = event.targetUserId.trim();
final affectsCurrentUser =
actorUserId == currentUserId || targetUserId == currentUserId;
if (!affectsCurrentUser) {
return;
}
final delta = event.isFollowing ? 1 : -1;
if (mounted) {
setState(() {
if (actorUserId == currentUserId) {
_applyCounterDelta("SUBSCRIPTION", delta);
}
if (targetUserId == currentUserId) {
_applyCounterDelta("FANS", delta);
}
});
}
_counterRefreshQueued = true;
if (_isCurrentTabVisible) {
unawaited(_loadCounter(force: true));
}
}
void _applyCounterDelta(String type, int delta) {
final currentCounter = _counterMap[type];
final currentQuantity = int.tryParse(currentCounter?.quantity ?? "0") ?? 0;
final nextQuantity = (currentQuantity + delta).clamp(0, 1 << 31);
_counterMap = {
..._counterMap,
type: (currentCounter ?? SCUserCounterRes(counterType: type)).copyWith(
quantity: nextQuantity.toString(),
),
};
}
Future<void> _loadCounter({bool force = false}) async {
if (_isCounterLoading) {
if (force || _counterRefreshQueued) {
_counterReloadAfterCurrent = true;
}
return;
}
if (!force && !_counterRefreshQueued && !_isCounterStale) return;
final userId = AccountStorage().getCurrentUser()?.userProfile?.id ?? "";
if (userId.isEmpty) return;
_isCounterLoading = true;
try {
final userCounterList = await SCAccountRepository().userCounter(userId);
if (!mounted) return;
setState(() {
_counterMap = Map.fromEntries(
userCounterList.map(
(counter) => MapEntry(counter.counterType ?? "", counter),
),
);
_lastCounterLoadedAt = DateTime.now();
_counterRefreshQueued = false;
});
} catch (_) {
} finally {
_isCounterLoading = false;
if (_counterReloadAfterCurrent && mounted) {
_counterReloadAfterCurrent = false;
unawaited(_loadCounter(force: true));
}
}
}
Future<void> _loadVipStatus() async {
if (mounted) {
setState(() => _isVipStatusLoading = true);
}
try {
final status = await _loadVipEntryStatus();
if (!mounted) return;
_syncCurrentProfileVipStatus(status);
setState(() {
_vipStatus = status;
_isVipStatusLoading = false;
});
} catch (error) {
if (!mounted) return;
setState(() => _isVipStatusLoading = false);
}
}
Future<SCVipStatusRes> _loadVipEntryStatus() async {
final repository = SCVipRepositoryImp();
try {
final home = await repository.vipHome();
final state = home.state;
if (state != null && state.levelInt > 0) {
return state;
}
} catch (error) {}
return repository.vipStatus();
}
void _syncCurrentProfileVipStatus(SCVipStatusRes status) {
final profileManager = Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
);
final profile = profileManager.currentUserProfile;
if (profile == null) return;
final nextVipLevel =
status.active == false || status.levelInt <= 0
? '0'
: status.levelInt.toString();
final nextProfile = socialChatProfileWithAvatarFrameResource(
profile.copyWith(vipLevel: nextVipLevel),
_avatarFramePropsFromVipStatus(status),
expireTime: _expireTimeFromVipStatus(status),
);
profileManager.syncCurrentUserProfile(nextProfile);
}
PropsResources? _avatarFramePropsFromVipStatus(SCVipStatusRes status) {
if (!status.isActive) {
return null;
}
final resource = status.avatarFrame;
return socialChatAvatarFrameProps(
id: resource?.resourceId,
name: resource?.name,
sourceUrl: resource?.sourceResourceUrl,
cover: resource?.coverUrl ?? resource?.cover,
code: "VIP_AVATAR_FRAME",
);
}
String? _expireTimeFromVipStatus(SCVipStatusRes status) {
final expireAt = status.expireAt?.trim();
if (expireAt == null || expireAt.isEmpty) {
return null;
}
final normalized =
expireAt.contains("T") ? expireAt : expireAt.replaceFirst(" ", "T");
return DateTime.tryParse(normalized)?.millisecondsSinceEpoch.toString();
}
void _refreshAfterVipReturn(SocialChatUserProfileManager profileManager) {
profileManager.fetchUserProfileData(loadGuardCount: false);
profileManager.balance();
_loadCounter();
_loadVipStatus();
}
void _openInviteActivity() {
final inviteActivityUrl = _appendToken(_inviteActivityUrl);
if (inviteActivityUrl.isEmpty) return;
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(inviteActivityUrl)}&showTitle=false",
);
}
void _openCpActivity() {
final cpActivityUrl = _cpActivityUrl.trim();
if (cpActivityUrl.isEmpty) {
if (kDebugMode) {
debugPrint('[MePage2] cp activity url is empty');
}
return;
}
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(cpActivityUrl)}&showTitle=false",
);
}
String _appendToken(String url) {
final token = AccountStorage().getToken();
if (token.isEmpty) return url;
final uri = Uri.tryParse(url);
if (uri == null) return url;
final queryParameters = Map<String, String>.from(uri.queryParameters);
queryParameters['token'] = token;
return uri.replace(queryParameters: queryParameters).toString();
}
@override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: [
const Positioned.fill(child: SCHomeShellBackground()),
SafeArea(
top: true,
bottom: false,
child: Consumer<SocialChatUserProfileManager>(
builder: (context, profileManager, child) {
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: EdgeInsets.fromLTRB(10.w, 10.w, 10.w, 24.w),
child: Column(
children: [
Consumer<SCAppGeneralManager>(
builder: (context, generalManager, child) {
final profile = profileManager.currentUserProfile;
final country =
generalManager.findCountryByName(
profile?.countryName ?? '',
) ??
generalManager.findCountryByCode(
profile?.countryCode ?? '',
);
final flagUrl = country?.nationalFlag?.trim() ?? '';
return _buildProfileSection(
profileManager,
flagUrl: flagUrl,
);
},
),
SizedBox(height: 12.w),
_buildStatsCard(),
SizedBox(height: 14.w),
_buildProfileActionPanel(profileManager),
SizedBox(height: 18.w),
_buildMenuGridCard([
..._identityMenuItems(profileManager),
_MenuRowData(
title: SCAppLocalizations.of(context)!.settings,
assetIcon: 'sc_images/index/sc_icon_me_settings.svg',
onTap: () {
SCNavigatorUtils.push(
context,
SettingsRoute.settings,
replace: false,
);
},
),
_MenuRowData(
title: SCAppLocalizations.of(context)!.language,
assetIcon: 'sc_images/index/sc_icon_me_language.svg',
onTap: () {
SCNavigatorUtils.push(context, SCMainRoute.language);
},
),
]),
],
),
);
},
),
),
],
);
}
Widget _buildProfileSection(
SocialChatUserProfileManager profileManager, {
required String flagUrl,
}) {
final profile = profileManager.currentUserProfile;
final avatarUrl = profile?.userAvatar?.trim() ?? '';
return SCDebounceWidget(
onTap: () {
SCNavigatorUtils.push(
context,
'${SCMainRoute.person}?isMe=true&tageId=${profile?.id ?? ""}',
replace: false,
);
},
child: Row(
children: [
head(
url: avatarUrl,
width: 72.w,
height: 72.w,
shape: BoxShape.circle,
headdress:
avatarUrl.isEmpty ? null : profile?.getHeaddress()?.sourceUrl,
headdressCover:
avatarUrl.isEmpty ? null : profile?.getHeaddress()?.cover,
showDefault: true,
),
SizedBox(width: 24.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (flagUrl.isNotEmpty) ...[
netImage(
url: flagUrl,
width: 20.w,
height: 14.w,
fit: BoxFit.cover,
noDefaultImg: true,
),
SizedBox(width: 5.w),
],
Flexible(
child: Text(
profile?.userNickname ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 15.sp,
fontWeight: FontWeight.w600,
height: 1.4,
),
),
),
if (profile?.userSex != null) ...[
SizedBox(width: 4.w),
Container(
width: 18.w,
height: 16.w,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
image: AssetImage(
profile?.userSex == 0
? 'sc_images/login/sc_icon_sex_woman_bg.png'
: 'sc_images/login/sc_icon_sex_man_bg.png',
),
fit: BoxFit.cover,
),
),
child: xb(profile?.userSex, height: 10.w, color: null),
),
],
],
),
SizedBox(height: 6.w),
Padding(
padding: EdgeInsetsDirectional.only(start: 3.w),
child: SCSpecialIdBadge(
idText: profile?.getID() ?? '',
showAnimated: profile?.hasSpecialId() ?? false,
assetPath: SCSpecialIdAssets.userIdLarge,
animationWidth: 80.w,
animationHeight: 32.w,
showTextBesideAnimated: true,
animatedTextSpacing: 0,
showAnimatedGradientText:
profile?.shouldShowColoredSpecialIdText() ?? false,
animationTextStyle: TextStyle(
color: Colors.white,
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
normalTextStyle: TextStyle(
color: Colors.white,
fontSize: 12.sp,
fontWeight: FontWeight.w400,
),
animationFit: BoxFit.contain,
),
),
],
),
),
],
),
);
}
Widget _buildStatsCard() {
return Row(
children: [
_buildStatItem(
value: _counterText('INTERVIEW'),
label: SCAppLocalizations.of(context)!.vistors,
onTap: () => SCNavigatorUtils.push(context, SCMainRoute.vistors),
),
_buildStatDivider(),
_buildStatItem(
value: _counterText('SUBSCRIPTION'),
label: SCAppLocalizations.of(context)!.following,
onTap: () => SCNavigatorUtils.push(context, SCMainRoute.follow),
),
_buildStatDivider(),
_buildStatItem(
value: _counterText('FANS'),
label: SCAppLocalizations.of(context)!.friends,
onTap: () => SCNavigatorUtils.push(context, SCMainRoute.fans),
),
],
);
}
Widget _buildProfileActionPanel(SocialChatUserProfileManager profileManager) {
return SizedBox(
width: double.infinity,
height: 260.w,
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(child: _buildActionPanelBackground()),
PositionedDirectional(
start: 0,
end: 0,
top: 0,
height: 76.w,
child: SCDebounceWidget(
onTap: _openInviteActivity,
child: const SizedBox.expand(),
),
),
PositionedDirectional(
start: 11.w,
end: 11.w,
top: 82.w,
child: _buildWalletVipRow(profileManager),
),
PositionedDirectional(
start: 18.w,
end: 18.w,
top: 161.w,
child: _buildEntryRow(),
),
],
),
);
}
Widget _buildActionPanelBackground() {
return Image.asset(
'sc_images/index/sc_me_action_panel_bg.png',
fit: BoxFit.fill,
errorBuilder: (context, error, stackTrace) {
return Padding(
padding: EdgeInsets.only(top: 52.w),
child: SvgPicture.asset(
'sc_images/index/sc_me_action_panel_bg.svg',
fit: BoxFit.fill,
),
);
},
);
}
Widget _buildEntryRow() {
return Row(
children: [
_buildEntryItem(
title: SCAppLocalizations.of(context)!.store,
iconPath: 'sc_images/index/sc_me_entry_store.png',
onTap: () => SCNavigatorUtils.push(context, StoreRoute.list),
),
_buildEntryItem(
title: SCAppLocalizations.of(context)!.bag,
iconPath: 'sc_images/index/sc_me_entry_bag.png',
onTap: () => SCNavigatorUtils.push(context, StoreRoute.bags),
),
_buildEntryItem(
title: SCAppLocalizations.of(context)!.level,
iconPath: 'sc_images/index/sc_me_entry_level.png',
onTap: () => SCNavigatorUtils.push(context, SCMainRoute.levelList),
),
_buildEntryItem(
title: 'CP',
iconPath: 'sc_images/index/sc_me_entry_cp.png',
iconWidth: 46,
iconHeight: 50,
onTap: _openCpActivity,
),
],
);
}
Widget _buildWalletVipRow(SocialChatUserProfileManager profileManager) {
return Row(
children: [
Expanded(child: _buildVipEntryCard(profileManager)),
SizedBox(width: 10.w),
Expanded(child: _buildWalletCard()),
],
);
}
Widget _buildWalletCard() {
return SCDebounceWidget(
onTap: () => SCNavigatorUtils.push(context, WalletRoute.recharge),
child: Container(
height: 68.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.w),
border: Border.all(color: scHomeShellGoldColor, width: 1.w),
image: const DecorationImage(
image: AssetImage('sc_images/index/sc_icon_wallet_bg.png'),
fit: BoxFit.fill,
),
),
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
SCAppLocalizations.of(context)!.wallet,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w800,
fontStyle: FontStyle.italic,
color: Colors.white,
height: 1,
),
),
SizedBox(height: 2.w),
Row(
children: [
Image.asset(
'sc_images/general/sc_icon_wallet_coin.png',
width: 18.w,
height: 18.w,
),
SizedBox(width: 4.w),
Expanded(
child: Consumer<SocialChatUserProfileManager>(
builder: (context, ref, child) {
return Text(
'${_balanceText(ref.myBalance)}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 12.sp,
fontWeight: FontWeight.w700,
fontStyle: FontStyle.italic,
height: 1,
),
);
},
),
),
],
),
],
),
),
Image.asset(
'sc_images/index/sc_icon_wallet_icon.png',
width: 40.w,
height: 40.w,
),
],
),
),
);
}
Widget _buildVipEntryCard(SocialChatUserProfileManager profileManager) {
final vipLevel = _vipEntryLevel(profileManager);
final isActiveVip = vipLevel > 0;
final badgeResource = isActiveVip ? _vipStatus?.badge : null;
return SCDebounceWidget(
onTap: () async {
await SCNavigatorUtils.push(context, VipRoute.detail);
if (mounted) {
_refreshAfterVipReturn(profileManager);
}
},
child: Container(
height: 68.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.w),
border: Border.all(color: const Color(0xFFFFD37A), width: 1.w),
image: const DecorationImage(
image: AssetImage('sc_images/vip/sc_vip_entry_bg.png'),
fit: BoxFit.fill,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.18),
blurRadius: 10.w,
offset: Offset(0, 4.w),
),
],
),
clipBehavior: Clip.antiAlias,
child: Stack(
children: [
PositionedDirectional(
top: 12.w,
start: 14.w,
child: Text(
'VIP',
style: TextStyle(
color: Colors.white,
fontFamily: 'Source Han Sans SC',
fontSize: 16.sp,
fontWeight: FontWeight.w700,
height: 1,
),
),
),
PositionedDirectional(
start: 13.w,
bottom: 10.w,
child:
_isVipStatusLoading && _vipStatus == null
? SizedBox(
width: 16.w,
height: 16.w,
child: CircularProgressIndicator(
color: const Color(0xFFFFD155),
strokeWidth: 2.w,
),
)
: isActiveVip
? _buildVipLevelMark(vipLevel)
: Text(
'Not activated',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: const Color(
0xFFFFE8A7,
).withValues(alpha: 0.82),
fontSize: 11.sp,
fontWeight: FontWeight.w600,
height: 1,
),
),
),
PositionedDirectional(
end: 2.w,
top: 8.w,
child: Opacity(
opacity: isActiveVip ? 1 : 0.52,
child: _buildVipEntryBadge(resource: badgeResource),
),
),
],
),
),
);
}
Widget _buildVipEntryBadge({required SCVipResourceRes? resource}) {
final empty = SizedBox(width: 52.w, height: 52.w);
final url = resource?.previewUrl?.trim();
if (url == null || url.isEmpty) {
return empty;
}
return netImage(
url: url,
width: 52.w,
height: 52.w,
fit: BoxFit.contain,
noDefaultImg: true,
errorWidget: empty,
);
}
String _vipEntryBadgeLogValue(SCVipResourceRes? resource) {
if (resource == null) {
return 'null';
}
return '{id:${resource.resourceId},name:${resource.name},'
'hasPreviewUrl:${resource.hasPreviewUrl},'
'previewUrl:${resource.previewUrl}}';
}
int _vipEntryLevel(SocialChatUserProfileManager profileManager) {
final status = _vipStatus;
if (status != null && status.isActive && status.levelInt > 0) {
return status.levelInt.clamp(1, 5).toInt();
}
final vipResource = profileManager.currentUserProfile?.getVIP();
return _parseVipLevel(vipResource?.name) ??
_parseVipLevel(vipResource?.code) ??
0;
}
int? _parseVipLevel(String? value) {
final text = value?.trim();
if (text == null || text.isEmpty) {
return null;
}
final vipMatch = RegExp(
r'VIP\s*([1-5])',
caseSensitive: false,
).firstMatch(text);
if (vipMatch != null) {
return int.tryParse(vipMatch.group(1) ?? '');
}
final trailingLevelMatch = RegExp(r'([1-5])$').firstMatch(text);
if (trailingLevelMatch != null) {
return int.tryParse(trailingLevelMatch.group(1) ?? '');
}
return null;
}
Widget _buildVipLevelMark(int level) {
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset(
'sc_images/vip/sc_vip_letter_v.png',
width: 18.1.w,
height: 15.3.w,
fit: BoxFit.contain,
),
Image.asset(
'sc_images/vip/sc_vip_letter_i.png',
width: 8.3.w,
height: 15.3.w,
fit: BoxFit.contain,
),
Image.asset(
'sc_images/vip/sc_vip_letter_p.png',
width: 18.1.w,
height: 15.3.w,
fit: BoxFit.contain,
),
SizedBox(width: 3.w),
_buildVipLevelNumber(level),
],
);
}
Widget _buildVipLevelNumber(int level) {
return Image.asset(
'sc_images/vip/sc_vip_number_${level}_filled.png',
height: 15.247.w,
fit: BoxFit.fill,
color: const Color(0xFFFFD155),
colorBlendMode: BlendMode.srcIn,
);
}
List<_MenuRowData> _identityMenuItems(
SocialChatUserProfileManager userProfile,
) {
final items = <_MenuRowData>[];
final hasCurrentUser = userProfile.currentUserProfile != null;
final isAgent = userProfile.userIdentity?.agent ?? false;
final isYumiManager = userProfile.userIdentity?.yumiManager ?? false;
if (kDebugMode) {
final currentProfile = userProfile.currentUserProfile;
debugPrint(
'[MePage2] menu identity userId=${currentProfile?.id}, '
'account=${currentProfile?.account}, hasCurrentUser=$hasCurrentUser, '
'agent=${userProfile.userIdentity?.agent}, admin=${userProfile.userIdentity?.admin}, '
'manager=${userProfile.userIdentity?.manager}, yumiManager=${userProfile.userIdentity?.yumiManager}',
);
}
// 1. 主播/代理入口
if (hasCurrentUser) {
if (isAgent) {
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.agentCenter,
assetIcon: 'sc_images/index/sc_icon_me_agency_center.svg',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.agencyCenterUrl)}&showTitle=false&safeTop=true",
);
},
),
);
} else {
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.hostCenter,
assetIcon: 'sc_images/index/sc_icon_me_host_center.svg',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.hostCenterUrl)}&showTitle=false&safeTop=true",
);
},
),
);
}
}
// 2. BD 相关(非管理员时)
if (!(userProfile.userIdentity?.admin ?? false)) {
if (userProfile.userIdentity?.bdLeader ?? false) {
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.admin,
assetIcon: 'sc_images/index/sc_icon_me_admin.svg',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.bdLeaderUrl)}&showTitle=false&safeTop=true",
);
},
),
);
} else if (userProfile.userIdentity?.bd ?? false) {
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.bdCenter,
assetIcon: 'sc_images/index/sc_icon_bd_center.png',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.bdCenterUrl)}&showTitle=false",
);
},
),
);
}
}
// 3. 货运代理
if (userProfile.userIdentity?.freightAgent ?? false) {
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.rechargeAgency,
assetIcon: 'sc_images/index/sc_icon_me_recharge_agency.svg',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.coinSellerUrl)}&showTitle=false&safeTop=true",
);
},
),
);
}
// 4. 管理员注意你的原逻辑中管理员和上面BD逻辑有重叠这里按原样单独处理
if (userProfile.userIdentity?.admin ?? false) {
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.adminCenter,
assetIcon: 'sc_images/index/sc_icon_me_admin.svg',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.adminUrl)}&showTitle=false&safeTop=true",
);
},
),
);
}
if (isYumiManager) {
if (kDebugMode) {
debugPrint('[MePage2] show Manager Center');
}
items.add(
_MenuRowData(
title: SCAppLocalizations.of(context)!.managerCenter,
assetIcon: 'sc_images/index/sc_icon_me_manager_center.svg',
onTap: () {
SCNavigatorUtils.push(
context,
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.managerCenterUrl)}&showTitle=false",
);
},
),
);
} else if (kDebugMode) {
debugPrint(
'[MePage2] hide Manager Center because yumiManager=$isYumiManager',
);
}
return items;
}
Widget _buildMenuGridCard(List<_MenuRowData> items) {
if (items.isEmpty) return const SizedBox.shrink();
return _buildGlassCard(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w),
radius: 14.w,
child: LayoutBuilder(
builder: (context, constraints) {
final itemWidth = constraints.maxWidth / 4;
return Wrap(
alignment: WrapAlignment.start,
runSpacing: 18.w,
children:
items.map((item) {
return SizedBox(
width: itemWidth,
child: _buildMenuGridItem(item),
);
}).toList(),
);
},
),
);
}
Widget _buildMenuGridItem(_MenuRowData item) {
return SCDebounceWidget(
onTap: item.onTap,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildMenuGridIcon(item.assetIcon!),
SizedBox(height: 16.w),
Text(
item.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 10.sp,
fontWeight: FontWeight.w600,
height: 1,
),
),
],
),
);
}
Widget _buildMenuGridIcon(String assetIcon) {
if (assetIcon.endsWith('.svg')) {
return SvgPicture.asset(
assetIcon,
width: 28.w,
height: 28.w,
fit: BoxFit.contain,
);
}
return Image.asset(
assetIcon,
width: 28.w,
height: 28.w,
color: Colors.white,
fit: BoxFit.contain,
);
}
Widget _buildEntryItem({
required String title,
required String iconPath,
required VoidCallback onTap,
double? iconWidth,
double? iconHeight,
int badgeCount = 0,
}) {
return Expanded(
child: SCDebounceWidget(
onTap: onTap,
child: Container(
height: 88.w,
color: Colors.transparent,
child: Stack(
children: [
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
iconPath,
width: (iconWidth ?? 50).w,
height: (iconHeight ?? 50).w,
fit: BoxFit.contain,
),
SizedBox(height: 4.w),
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 15.sp,
fontWeight: FontWeight.w600,
height: 1.2,
),
),
],
),
),
if (badgeCount > 0) _buildEntryBadge(badgeCount),
],
),
),
),
);
}
Widget _buildEntryBadge(int count) {
final text = count > 99 ? '99+' : count.toString();
return PositionedDirectional(
top: 11.w,
end: 14.w,
child: Container(
constraints: BoxConstraints(minWidth: 18.w),
height: 18.w,
padding: EdgeInsets.symmetric(horizontal: 5.w),
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xFFFF4B4B),
borderRadius: BorderRadius.circular(9.w),
border: Border.all(color: Colors.white, width: 1.w),
),
child: Text(
text,
maxLines: 1,
style: TextStyle(
color: Colors.white,
fontSize: 10.sp,
fontWeight: FontWeight.w700,
height: 1,
),
),
),
);
}
Widget _buildStatItem({
required String value,
required String label,
required VoidCallback onTap,
}) {
return Expanded(
child: SCDebounceWidget(
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: scHomeShellAccentColor.withValues(alpha: 0.7),
fontSize: 12.sp,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
SizedBox(height: 1.w),
Text(
value,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: scHomeShellAccentColor,
fontSize: 16.sp,
fontWeight: FontWeight.w700,
height: 1.5,
),
),
],
),
),
);
}
Widget _buildStatDivider() {
return Container(
width: 0.5.w,
height: 16.w,
color: Colors.white.withValues(alpha: 0.32),
margin: EdgeInsets.symmetric(horizontal: 6.w),
);
}
Widget _buildGlassCard({
required Widget child,
required EdgeInsets padding,
required double radius,
}) {
return Container(
width: double.infinity,
padding: padding,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(radius),
border: Border.all(
color: scHomeShellAccentColor.withValues(alpha: 0.2),
width: 1.w,
),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFF111A1B).withValues(alpha: 0.62),
const Color(0xFF2C4D42).withValues(alpha: 0.44),
],
),
),
child: child,
);
}
String _counterText(String type) {
return _counterMap[type]?.quantity ?? '0';
}
String _balanceText(double balance) {
if (balance >= 1000) {
return '${(balance / 1000).toStringAsFixed(2)}k';
}
return balance.toStringAsFixed(balance % 1 == 0 ? 0 : 2);
}
}
class _MenuRowData {
const _MenuRowData({
required this.title,
required this.onTap,
this.assetIcon,
});
final String title;
final String? assetIcon;
final VoidCallback onTap;
}