1203 lines
37 KiB
Dart
1203 lines
37 KiB
Dart
import 'dart:async';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter_screenutil/flutter_screenutil.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/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/svga/sc_svga_asset_widget.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';
|
||
static const double _inviteActivityHorizontalScale = 750 / 662;
|
||
|
||
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 SafeArea(
|
||
top: true,
|
||
bottom: false,
|
||
child: Consumer<SocialChatUserProfileManager>(
|
||
builder: (context, profileManager, child) {
|
||
return SingleChildScrollView(
|
||
physics: const BouncingScrollPhysics(),
|
||
padding: EdgeInsets.fromLTRB(10.w, 12.w, 10.w, 24.w),
|
||
child: Column(
|
||
children: [
|
||
_buildProfileSection(profileManager),
|
||
SizedBox(height: 12.w),
|
||
_buildStatsCard(),
|
||
SizedBox(height: 12.w),
|
||
_buildEntryRow(),
|
||
SizedBox(height: 12.w),
|
||
_buildWalletVipRow(profileManager),
|
||
_buildInviteActivityBanner(),
|
||
_buildMenuCard2(),
|
||
SizedBox(height: 12.w),
|
||
_buildMenuCard([
|
||
_MenuRowData(
|
||
title: SCAppLocalizations.of(context)!.settings,
|
||
assetIcon: 'sc_images/index/sc_icon_settings.png',
|
||
onTap: () {
|
||
SCNavigatorUtils.push(
|
||
context,
|
||
SettingsRoute.settings,
|
||
replace: false,
|
||
);
|
||
},
|
||
),
|
||
_MenuRowData(
|
||
title: SCAppLocalizations.of(context)!.language,
|
||
assetIcon: 'sc_images/general/sc_icon_setting_language.png',
|
||
onTap: () {
|
||
SCNavigatorUtils.push(context, SCMainRoute.language);
|
||
},
|
||
),
|
||
]),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildProfileSection(SocialChatUserProfileManager profileManager) {
|
||
final profile = profileManager.currentUserProfile;
|
||
|
||
return SCDebounceWidget(
|
||
onTap: () {
|
||
SCNavigatorUtils.push(
|
||
context,
|
||
'${SCMainRoute.person}?isMe=true&tageId=${profile?.id ?? ""}',
|
||
replace: false,
|
||
);
|
||
},
|
||
child: Column(
|
||
children: [
|
||
SizedBox(
|
||
width: 240.w,
|
||
height: 130.w,
|
||
child: Stack(
|
||
alignment: Alignment.topCenter,
|
||
children: [
|
||
head(
|
||
url: profile?.userAvatar ?? '',
|
||
width: 96.w,
|
||
height: 96.w,
|
||
shape: BoxShape.circle,
|
||
border: Border.all(color: Colors.white, width: 1),
|
||
headdress: profile?.getHeaddress()?.sourceUrl,
|
||
headdressCover: profile?.getHeaddress()?.cover,
|
||
showDefault: true,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
SizedBox(height: 8.w),
|
||
Text(
|
||
profile?.userNickname ?? '',
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 18.sp,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
SizedBox(height: 2.w),
|
||
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: 16.sp,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
normalTextStyle: TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 16.sp,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
animationFit: BoxFit.contain,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildStatsCard() {
|
||
return _buildGlassCard(
|
||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w),
|
||
radius: 8.w,
|
||
child: 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 _buildEntryRow() {
|
||
return Row(
|
||
children: [
|
||
_buildEntryItem(
|
||
title: SCAppLocalizations.of(context)!.store,
|
||
iconPath: 'sc_images/index/sc_icon_shop.png',
|
||
onTap: () => SCNavigatorUtils.push(context, StoreRoute.list),
|
||
),
|
||
SizedBox(width: 10.w),
|
||
_buildEntryItem(
|
||
title: SCAppLocalizations.of(context)!.level,
|
||
iconPath: 'sc_images/index/sc_icon_level.png',
|
||
onTap: () => SCNavigatorUtils.push(context, SCMainRoute.levelList),
|
||
),
|
||
SizedBox(width: 10.w),
|
||
_buildEntryItem(
|
||
title: SCAppLocalizations.of(context)!.bag,
|
||
iconPath: 'sc_images/index/sc_icon_bag.png',
|
||
onTap: () => SCNavigatorUtils.push(context, StoreRoute.bags),
|
||
),
|
||
SizedBox(width: 10.w),
|
||
_buildEntryItem(
|
||
title: 'cp',
|
||
iconPath: 'sc_images/index/sc_icon_cp_entry.png',
|
||
iconWidth: 47,
|
||
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),
|
||
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: 18.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,
|
||
);
|
||
}
|
||
|
||
Widget _buildInviteActivityBanner() {
|
||
return SCDebounceWidget(
|
||
onTap: _openInviteActivity,
|
||
child: LayoutBuilder(
|
||
builder: (context, constraints) {
|
||
final width =
|
||
constraints.maxWidth.isFinite
|
||
? constraints.maxWidth
|
||
: ScreenUtil().screenWidth - 20.w;
|
||
final height = width / 5;
|
||
|
||
return SizedBox(
|
||
width: width,
|
||
height: height,
|
||
child: Stack(
|
||
clipBehavior: Clip.none,
|
||
alignment: Alignment.center,
|
||
children: [
|
||
Positioned.fill(
|
||
child: Transform(
|
||
alignment: Alignment.center,
|
||
transform: Matrix4.diagonal3Values(
|
||
_inviteActivityHorizontalScale,
|
||
1.0,
|
||
1.0,
|
||
),
|
||
child: SCSvgaAssetWidget(
|
||
assetPath: _inviteActivitySvgaAsset,
|
||
width: width,
|
||
height: height,
|
||
active: true,
|
||
loop: true,
|
||
fit: BoxFit.fill,
|
||
allowDrawingOverflow: true,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
String get _inviteActivitySvgaAsset {
|
||
switch (SCGlobalConfig.lang) {
|
||
case 'ar':
|
||
return 'sc_images/index/sc_invite_activity_rtl.svga';
|
||
case 'bn':
|
||
case 'fa':
|
||
return 'sc_images/index/sc_invite_activity_fa.svga';
|
||
case 'tr':
|
||
case 'pt':
|
||
return 'sc_images/index/sc_invite_activity_pt.svga';
|
||
default:
|
||
return 'sc_images/index/sc_invite_activity.svga';
|
||
}
|
||
}
|
||
|
||
Widget _buildMenuCard2() {
|
||
final userProfile = context.watch<SocialChatUserProfileManager>();
|
||
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_agent_center.png',
|
||
onTap: () {
|
||
SCNavigatorUtils.push(
|
||
context,
|
||
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.agencyCenterUrl)}&showTitle=false",
|
||
);
|
||
},
|
||
),
|
||
);
|
||
} else {
|
||
items.add(
|
||
_MenuRowData(
|
||
title: SCAppLocalizations.of(context)!.hostCenter,
|
||
assetIcon: 'sc_images/index/sc_icon_host_center.png',
|
||
onTap: () {
|
||
SCNavigatorUtils.push(
|
||
context,
|
||
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.hostCenterUrl)}&showTitle=false",
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// 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_bd_leader.png',
|
||
onTap: () {
|
||
SCNavigatorUtils.push(
|
||
context,
|
||
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.bdLeaderUrl)}&showTitle=false",
|
||
);
|
||
},
|
||
),
|
||
);
|
||
} 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_recharge_agency.png',
|
||
onTap: () {
|
||
SCNavigatorUtils.push(
|
||
context,
|
||
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.coinSellerUrl)}&showTitle=false",
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
// 4. 管理员(注意:你的原逻辑中管理员和上面BD逻辑有重叠,这里按原样单独处理)
|
||
if (userProfile.userIdentity?.admin ?? false) {
|
||
items.add(
|
||
_MenuRowData(
|
||
title: SCAppLocalizations.of(context)!.adminCenter,
|
||
assetIcon: 'sc_images/index/sc_icon_admin_center.png',
|
||
onTap: () {
|
||
SCNavigatorUtils.push(
|
||
context,
|
||
"${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.adminUrl)}&showTitle=false",
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
if (isYumiManager) {
|
||
if (kDebugMode) {
|
||
debugPrint('[MePage2] show Manager Center');
|
||
}
|
||
items.add(
|
||
_MenuRowData(
|
||
title: SCAppLocalizations.of(context)!.managerCenter,
|
||
assetIcon: 'sc_images/index/sc_icon_admin_center.png',
|
||
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 _buildMenuCard(items);
|
||
}
|
||
|
||
Widget _buildMenuCard(List<_MenuRowData> items) {
|
||
// 没有任何项时,不显示整个卡片
|
||
if (items.isEmpty) return const SizedBox.shrink();
|
||
return _buildGlassCard(
|
||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.w),
|
||
radius: 4.w,
|
||
child: Column(
|
||
children: [
|
||
for (int i = 0; i < items.length; i++) ...[_buildMenuRow(items[i])],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildMenuRow(_MenuRowData item) {
|
||
return SCDebounceWidget(
|
||
onTap: item.onTap,
|
||
child: SizedBox(
|
||
height: 46.w,
|
||
child: Row(
|
||
children: [
|
||
Image.asset(
|
||
item.assetIcon!,
|
||
width: 24.w,
|
||
height: 24.w,
|
||
color: Colors.white,
|
||
),
|
||
|
||
SizedBox(width: 12.w),
|
||
Expanded(
|
||
child: Text(
|
||
item.title,
|
||
style: TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 15.sp,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
Icon(
|
||
SCGlobalConfig.lang == 'ar'
|
||
? Icons.keyboard_arrow_left
|
||
: Icons.keyboard_arrow_right,
|
||
color: Colors.white,
|
||
size: 18.w,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
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: 110.w,
|
||
decoration: BoxDecoration(
|
||
image: const DecorationImage(
|
||
image: AssetImage('sc_images/person/sc_icon_me_menu_1_bg.png'),
|
||
fit: BoxFit.fill,
|
||
),
|
||
),
|
||
child: Stack(
|
||
children: [
|
||
Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Image.asset(
|
||
iconPath,
|
||
width: (iconWidth ?? 54).w,
|
||
height: (iconHeight ?? 54).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.w700,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
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(
|
||
children: [
|
||
Text(
|
||
value,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 16.sp,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
SizedBox(height: 1.w),
|
||
Text(
|
||
label,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 12.sp,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildStatDivider() {
|
||
return Container(
|
||
width: 0.5.w,
|
||
height: 16.w,
|
||
color: const Color(0xFFE6E6E6).withValues(alpha: 0.8),
|
||
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: const Color(0xFFB4FCCE), width: 1.w),
|
||
gradient: LinearGradient(
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
colors: [
|
||
const Color(0xFF0A7D49).withValues(alpha: 0.20),
|
||
const Color(0xFF031513).withValues(alpha: 0.35),
|
||
],
|
||
),
|
||
),
|
||
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;
|
||
}
|