This commit is contained in:
roxy 2026-05-22 20:41:57 +08:00
parent d8ef488a28
commit cbb811a18a
12 changed files with 365 additions and 1481 deletions

View File

@ -49,7 +49,6 @@ import 'package:yumi/ui_kit/components/dialog/dialog_base.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/app/constants/sc_room_msg_type.dart';
import 'package:yumi/app/config/business_logic_strategy.dart';
import 'package:yumi/app/constants/sc_screen.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
@ -61,12 +60,10 @@ import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart';
import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/modules/index/main_route.dart';
import 'package:yumi/ui_kit/widgets/sc_nine_patch_image.dart';
import 'package:yumi/ui_kit/widgets/room/cp/room_cp_invite_dialog.dart';
import 'package:yumi/ui_kit/widgets/room/cp/room_cp_relation_formed_dialog.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart';
import '../../shared/business_logic/models/res/sc_system_invit_message_res.dart';
@ -1764,91 +1761,14 @@ class _MessageItem extends StatelessWidget {
return;
}
SmartDialog.dismiss(tag: RoomCpInviteDialog.dialogTag);
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
if (agree) {
unawaited(
rtmProvider.sendCpRelationEstablishedSystemMessage(
applyId: applyId,
invite: invite,
currentProfile: currentProfile,
),
);
_sendCpRelationEstablishedRoomNotice(
invite: invite,
currentProfile: currentProfile,
);
} else {
unawaited(
rtmProvider.sendCpInviteRejectedSystemMessage(
applyId: applyId,
invite: invite,
currentProfile: currentProfile,
),
);
}
_cpMessageLog(
'process submitted applyId=$applyId agree=$agree '
'wait_backend_result=true',
);
unawaited(profileManager.refreshLoadedCpProfiles());
updateCall();
}
void _sendCpRelationEstablishedRoomNotice({
required SCSystemInvitMessageRes invite,
required SocialChatUserProfile? currentProfile,
}) {
try {
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
final groupId =
rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount
?.trim() ??
"";
if (groupId.isEmpty) {
return;
}
final currentName = _cpDisplayName(
nickname: currentProfile?.userNickname,
account: currentProfile?.account,
fallback: currentProfile?.id,
);
final inviteName = _cpDisplayName(
nickname: invite.userNickname,
account: invite.account,
actualAccount: invite.actualAccount,
);
final relationType = scNormalizeCpRelationType(invite.relationType);
Provider.of<RtmProvider>(context, listen: false).dispatchMessage(
Msg(
groupId: groupId,
msg: scBuildCpRelationEstablishedNotice(
userName1: inviteName,
userName2: currentName,
relationType: relationType,
),
type: SCRoomMsgType.cpSystemNotice,
noticeAction: "RELATION_ESTABLISHED",
user: currentProfile,
toUser: SocialChatUserProfile(
account: _firstCpInviteValue([
invite.actualAccount,
invite.account,
]),
userNickname: inviteName,
userAvatar: invite.userAvatar ?? "",
),
targetUserIds: _compactCpTargetIds([
currentProfile?.id,
currentProfile?.account,
invite.actualAccount,
invite.account,
]),
role: relationType,
),
addLocal: true,
);
} catch (error) {
_cpMessageLog('send room established notice skipped error=$error');
}
}
Map<String, dynamic>? _customDataFromMessage(V2TimMessage msg) {
if (msg.elemType != MessageElemType.V2TIM_ELEM_TYPE_CUSTOM) {
return null;
@ -1932,6 +1852,15 @@ class _MessageItem extends StatelessWidget {
content["result"]?.toString() ?? "",
content["state"]?.toString() ?? "",
].map((value) => value.trim().toUpperCase()).join("|");
final expiredValue = _cpInviteBoolValue([
data["expired"],
data["isExpired"],
content["expired"],
content["isExpired"],
]);
if (expiredValue == true) {
return _CpInviteMessageState.expired;
}
final agreeValue = _cpInviteBoolValue([
data["agree"],
data["accepted"],
@ -2157,24 +2086,6 @@ class _MessageItem extends StatelessWidget {
return "User";
}
List<String> _compactCpTargetIds(Iterable<String?> values) {
return values
.map((value) => value?.trim() ?? "")
.where((value) => value.isNotEmpty)
.toSet()
.toList();
}
String? _firstCpInviteValue(Iterable<String?> values) {
for (final value in values) {
final text = value?.trim() ?? "";
if (text.isNotEmpty) {
return text;
}
}
return null;
}
String _firstNonBlankString(
Iterable<String?> values, {
String fallback = "",

View File

@ -26,7 +26,6 @@ import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart';
import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart';
import 'package:yumi/app_localizations.dart';
import 'package:yumi/app/constants/sc_room_msg_type.dart';
import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart';
import 'package:yumi/ui_kit/components/dialog/dialog_base.dart';
import 'package:yumi/ui_kit/components/sc_tts.dart';
@ -35,7 +34,6 @@ import 'package:yumi/app/constants/sc_screen.dart';
import 'package:yumi/shared/tools/sc_date_utils.dart';
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
import 'package:yumi/services/general/sc_app_general_manager.dart';
import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
@ -44,11 +42,9 @@ import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/app/config/business_logic_strategy.dart';
import 'package:yumi/ui_kit/widgets/room/cp/room_cp_relation_formed_dialog.dart';
import 'package:yumi/ui_kit/widgets/room/cp/room_cp_invite_dialog.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
import '../../../shared/data_sources/models/enum/sc_sysytem_message_type.dart';
import '../../../shared/business_logic/models/res/sc_system_invit_message_res.dart';
import '../../../shared/business_logic/models/res/login_res.dart';
enum _CpInviteMessageState { pending, expired, rejected, accepted }
@ -103,10 +99,7 @@ class _MessageSystemPageState extends State<MessageSystemPage> {
count: 100,
lastMsgID: null,
);
List<V2TimMessage> messages =
(v2timValueCallback.data ?? [])
.where((message) => !_isCpBuildSystemMessage(message))
.toList();
List<V2TimMessage> messages = v2timValueCallback.data ?? [];
// List<V2TimMessage> messages = await FTIM.getMessageManager().getMessages(conversation: currentConversation);
currentConversationMessageList ??= [];
currentConversationMessageList?.clear();
@ -128,14 +121,6 @@ class _MessageSystemPageState extends State<MessageSystemPage> {
return;
}
if (message.userID != currentConversation?.userID) return;
if (_isCpBuildSystemMessage(message)) {
debugPrint(
'[CP][Invite] skip system conversation CP_BUILD '
'msgId=${message.msgID}',
);
return;
}
int? index;
for (var element in currentConversationMessageList) {
if (msgId != null) {
@ -193,26 +178,6 @@ class _MessageSystemPageState extends State<MessageSystemPage> {
} catch (_) {}
}
bool _isCpBuildSystemMessage(V2TimMessage message) {
if (message.elemType != MessageElemType.V2TIM_ELEM_TYPE_CUSTOM) {
return false;
}
final content = message.customElem?.data;
if (content == null || content.trim().isEmpty) {
return false;
}
try {
final data = jsonDecode(content);
if (data is! Map) {
return false;
}
final noticeType = (data["noticeType"] ?? data["type"])?.toString();
return noticeType == SCSysytemMessageType.CP_BUILD.name;
} catch (_) {
return false;
}
}
@override
Widget build(BuildContext context) {
return Stack(
@ -938,6 +903,29 @@ class _MessageItem extends StatelessWidget {
content["result"]?.toString() ?? "",
content["state"]?.toString() ?? "",
].map((value) => value.trim().toUpperCase()).join("|");
final expiredValue = _cpInviteBoolValue([
data["expired"],
data["isExpired"],
content["expired"],
content["isExpired"],
]);
if (expiredValue == true) {
return _CpInviteMessageState.expired;
}
final agreeValue = _cpInviteBoolValue([
data["agree"],
data["accepted"],
data["isAgree"],
content["agree"],
content["accepted"],
content["isAgree"],
]);
if (agreeValue == true) {
return _CpInviteMessageState.accepted;
}
if (agreeValue == false) {
return _CpInviteMessageState.rejected;
}
if (values.contains("TIMEOUT") || values.contains("EXPIRE")) {
return _CpInviteMessageState.expired;
}
@ -956,6 +944,30 @@ class _MessageItem extends StatelessWidget {
return _CpInviteMessageState.pending;
}
bool? _cpInviteBoolValue(Iterable<dynamic> values) {
for (final value in values) {
if (value is bool) {
return value;
}
final text = value?.toString().trim().toLowerCase() ?? "";
if (text.isEmpty) {
continue;
}
if (text == "true" || text == "1" || text == "yes" || text == "agree") {
return true;
}
if (text == "false" ||
text == "0" ||
text == "no" ||
text == "reject" ||
text == "decline" ||
text == "refuse") {
return false;
}
}
return null;
}
bool _isCpInvitePayload(Map<String, dynamic> data) {
final content = _cpContentMap(data);
final text =
@ -1848,136 +1860,22 @@ class _MessageItem extends StatelessWidget {
return null;
}
String _cpNoticeDisplayName({
String? nickname,
String? account,
String? actualAccount,
String? fallback,
}) {
for (final value in [nickname, actualAccount, account, fallback]) {
final text = value?.trim() ?? "";
if (text.isNotEmpty) {
return text;
}
}
return "User";
}
void _handleCpInviteAccepted(
String applyId,
SCSystemInvitMessageRes? invite,
) {
if (invite == null) {
return;
}
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
final currentName = _cpNoticeDisplayName(
nickname: currentProfile?.userNickname,
account: currentProfile?.account,
fallback: currentProfile?.id,
);
final inviteName = _cpNoticeDisplayName(
nickname: invite.userNickname,
account: invite.account,
actualAccount: invite.actualAccount,
);
final relationType = scNormalizeCpRelationType(invite.relationType);
unawaited(
Provider.of<RtmProvider>(
context,
listen: false,
).sendCpRelationEstablishedSystemMessage(
applyId: applyId,
invite: invite,
currentProfile: currentProfile,
),
);
_sendCpRelationEstablishedRoomNotice(
leftName: inviteName,
rightName: currentName,
relationType: relationType,
currentProfile: currentProfile,
inviteAccount: invite.account,
inviteActualAccount: invite.actualAccount,
inviteAvatar: invite.userAvatar ?? "",
);
}
void _sendCpRelationEstablishedRoomNotice({
required String leftName,
required String rightName,
required String relationType,
required SocialChatUserProfile? currentProfile,
required String? inviteAccount,
required String? inviteActualAccount,
required String inviteAvatar,
}) {
try {
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
final groupId =
rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount
?.trim() ??
"";
if (groupId.isEmpty) {
return;
}
Provider.of<RtmProvider>(context, listen: false).dispatchMessage(
Msg(
groupId: groupId,
msg: scBuildCpRelationEstablishedNotice(
userName1: leftName,
userName2: rightName,
relationType: relationType,
),
type: SCRoomMsgType.cpSystemNotice,
noticeAction: "RELATION_ESTABLISHED",
user: currentProfile,
toUser: SocialChatUserProfile(
account: _firstCpNoticeValue([inviteActualAccount, inviteAccount]),
userNickname: leftName,
userAvatar: inviteAvatar,
),
targetUserIds: _compactCpTargetIds([
currentProfile?.id,
currentProfile?.account,
inviteActualAccount,
inviteAccount,
]),
role: relationType,
),
addLocal: true,
);
} catch (_) {}
}
List<String> _compactCpTargetIds(Iterable<String?> values) {
return values
.map((value) => value?.trim() ?? "")
.where((value) => value.isNotEmpty)
.toSet()
.toList();
}
String? _firstCpNoticeValue(Iterable<String?> values) {
for (final value in values) {
final text = value?.trim() ?? "";
if (text.isNotEmpty) {
return text;
}
}
return null;
}
Future<void> _acceptCpInviteDirect(
String id,
SCSystemInvitMessageRes? cpInvite,
) async {
final success = await Provider.of<SocialChatUserProfileManager>(
final profileManager = Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
).cpRlationshipProcessApply(context, id, true);
);
final success = await profileManager.cpRlationshipProcessApply(
context,
id,
true,
);
if (success && context.mounted) {
_handleCpInviteAccepted(id, cpInvite);
SmartDialog.dismiss(tag: RoomCpInviteDialog.dialogTag);
unawaited(profileManager.refreshLoadedCpProfiles());
}
}
@ -1985,21 +1883,18 @@ class _MessageItem extends StatelessWidget {
String id,
SCSystemInvitMessageRes? cpInvite,
) async {
final success = await Provider.of<SocialChatUserProfileManager>(
final profileManager = Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
).cpRlationshipProcessApply(context, id, false);
if (success && cpInvite != null && context.mounted) {
unawaited(
Provider.of<RtmProvider>(
context,
listen: false,
).sendCpInviteRejectedSystemMessage(
applyId: id,
invite: cpInvite,
currentProfile: AccountStorage().getCurrentUser()?.userProfile,
),
);
);
final success = await profileManager.cpRlationshipProcessApply(
context,
id,
false,
);
if (success && context.mounted) {
SmartDialog.dismiss(tag: RoomCpInviteDialog.dialogTag);
unawaited(profileManager.refreshLoadedCpProfiles());
}
}

View File

@ -25,7 +25,6 @@ import 'package:yumi/main.dart';
import 'package:provider/provider.dart';
import 'package:yumi/app/constants/sc_room_msg_type.dart';
import 'package:yumi/app/constants/sc_screen.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
import 'package:yumi/shared/tools/sc_dialog_utils.dart';
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
@ -36,7 +35,6 @@ import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/ui_kit/widgets/gift/sc_gift_combo_send_button.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
import 'package:yumi/modules/wallet/wallet_route.dart';
import 'package:yumi/modules/gift/gift_tab_page.dart';
import 'package:yumi/modules/gift/cp_rights/cp_rights_guide_page.dart';
import 'package:yumi/services/gift/room_gift_combo_send_controller.dart';
@ -870,14 +868,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
children: [
SizedBox(width: 15.w),
GestureDetector(
onTap: () {
SmartDialog.dismiss(tag: "showGiftControl");
SCNavigatorUtils.push(
navigatorKey.currentState!.context,
WalletRoute.recharge,
replace: false,
);
},
onTap: _openRoomRechargeBottomSheet,
child: Container(
padding: EdgeInsets.symmetric(
vertical: 8.w,
@ -1569,7 +1560,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
animationCount: request.clickCount,
);
}
_syncLocalCpInviteAfterGift(request);
await _refreshGiftBackpack();
_refreshCpStateAfterGift(request, profileManager);
return;
@ -1635,7 +1625,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
);
}
profileManager?.updateBalance(result);
_syncLocalCpInviteAfterGift(request);
_refreshCpStateAfterGift(request, profileManager);
} catch (e) {
final errorMessage =
@ -1679,41 +1668,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
unawaited(profileManager?.refreshLoadedCpProfiles());
}
void _syncLocalCpInviteAfterGift(_GiftSendRequest request) {
final relationType = request.cpRelationType;
if (relationType == null || request.acceptUserIds.isEmpty) {
return;
}
final context = navigatorKey.currentState?.context;
if (context == null) {
return;
}
final receiverId = request.acceptUserIds.first.trim();
if (receiverId.isEmpty) {
return;
}
final receiverProfile =
request.acceptUsers.isNotEmpty
? request.acceptUsers.first.user
: SocialChatUserProfile(id: receiverId, account: receiverId);
_giftFxLog(
'sync local CP invite after gift '
'receiver=$receiverId relationType=$relationType giftId=${request.gift.id}',
);
unawaited(
Provider.of<RtmProvider>(
context,
listen: false,
).insertLocalOutgoingCpGiftInvite(
receiverId: receiverId,
receiverProfile: receiverProfile,
relationType: relationType,
gift: request.gift,
giftCount: request.quantity,
),
);
}
Future<List<MicRes>> _giveBackpackGiftToRecipients(
SCChatRoomRepository repository,
_GiftSendRequest request,
@ -2044,6 +1998,17 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
});
}
void _openRoomRechargeBottomSheet() {
SmartDialog.dismiss(tag: "showGiftControl");
Future<void>.delayed(const Duration(milliseconds: 80), () {
final navigator = navigatorKey.currentState;
if (navigator == null || !navigator.mounted) {
return;
}
RoomRechargeBottomSheet.show(navigator.context);
});
}
void _openCpRightsGuide() {
SmartDialog.dismiss(tag: "showGiftControl");
Future<void>.delayed(const Duration(milliseconds: 80), () {

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:yumi/app_localizations.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/ui_kit/components/text/sc_text.dart';
@ -14,7 +13,6 @@ import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repositor
import 'package:yumi/shared/business_logic/models/res/bags_list_res.dart';
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/ui_kit/widgets/bag/props_bag_chatbox_detail_dialog.dart';
import 'package:yumi/ui_kit/widgets/countdown_timer.dart';
import 'package:yumi/modules/store/store_route.dart';
import 'package:yumi/ui_kit/widgets/store/store_bag_page_helpers.dart';
@ -237,27 +235,6 @@ class _BagsChatboxPageState
),
),
),
PositionedDirectional(
bottom: 5.w,
end: 5.w,
child: GestureDetector(
onTap: () {
selecteChatbox = res;
setState(() {});
_showDetail(res);
},
behavior: HitTestBehavior.opaque,
child: Container(
padding: EdgeInsets.all(5.w),
child: Image.asset(
"sc_images/store/sc_icon_shop_item_search.png",
width: 18.w,
height: 18.w,
color: Colors.white,
),
),
),
),
],
),
),
@ -305,21 +282,6 @@ class _BagsChatboxPageState
}
}
void _showDetail(BagsListRes res) {
SmartDialog.show(
tag: "showPropsDetail",
alignment: Alignment.center,
animationType: SmartAnimationType.fade,
builder: (_) {
return PropsBagChatboxDetailDialog(res);
},
onDismiss: () {
myChatbox = AccountStorage().getChatbox();
setState(() {});
},
);
}
Future<bool> _use(BagsListRes res, bool unload) async {
SCLoadingManager.show(context: context);
final localizations = SCAppLocalizations.of(context)!;

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:provider/provider.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
import 'package:yumi/app_localizations.dart';
@ -18,7 +17,6 @@ import 'package:yumi/ui_kit/components/sc_page_list.dart';
import 'package:yumi/ui_kit/components/sc_tts.dart';
import 'package:yumi/ui_kit/components/text/sc_text.dart';
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
import 'package:yumi/ui_kit/widgets/bag/props_bag_data_card_detail_dialog.dart';
import 'package:yumi/ui_kit/widgets/countdown_timer.dart';
import 'package:yumi/ui_kit/widgets/store/store_bag_page_helpers.dart';
@ -249,27 +247,6 @@ class BagsDataCardPageState
),
),
),
PositionedDirectional(
bottom: 5.w,
end: 5.w,
child: GestureDetector(
onTap: () {
selectedDataCard = res;
setState(() {});
_showDetail(res);
},
behavior: HitTestBehavior.opaque,
child: Container(
padding: EdgeInsets.all(5.w),
child: Image.asset(
"sc_images/store/sc_icon_shop_item_search.png",
width: 18.w,
height: 18.w,
color: Colors.white,
),
),
),
),
],
),
),
@ -326,21 +303,6 @@ class BagsDataCardPageState
}
}
void _showDetail(BagsListRes res) {
SmartDialog.show(
tag: "showPropsDetail",
alignment: Alignment.center,
animationType: SmartAnimationType.fade,
builder: (_) {
return PropsBagDataCardDetailDialog(res);
},
onDismiss: () {
myDataCard = AccountStorage().getDataCard();
setState(() {});
},
);
}
Future<bool> _use(BagsListRes res, bool unload) async {
SCLoadingManager.show(context: context);
final profileManager = Provider.of<SocialChatUserProfileManager>(

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:yumi/app_localizations.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
import 'package:yumi/shared/tools/sc_loading_manager.dart';
@ -16,7 +15,6 @@ import 'package:yumi/ui_kit/components/sc_page_list.dart';
import 'package:yumi/ui_kit/components/text/sc_text.dart';
import 'package:yumi/ui_kit/components/sc_tts.dart';
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
import 'package:yumi/ui_kit/widgets/bag/props_bag_headdress_detail_dialog.dart';
import 'package:yumi/ui_kit/widgets/countdown_timer.dart';
import 'package:yumi/ui_kit/widgets/store/store_bag_page_helpers.dart';
import 'package:provider/provider.dart';
@ -237,27 +235,6 @@ class _BagsHeaddressPageState
),
),
),
PositionedDirectional(
bottom: 5.w,
end: 5.w,
child: GestureDetector(
onTap: () {
selecteHeaddress = res;
setState(() {});
_showDetail(res);
},
behavior: HitTestBehavior.opaque,
child: Container(
padding: EdgeInsets.all(5.w),
child: Image.asset(
"sc_images/store/sc_icon_shop_item_search.png",
width: 13.w,
height: 13.w,
color: Colors.white,
),
),
),
),
],
),
),
@ -306,21 +283,6 @@ class _BagsHeaddressPageState
}
}
void _showDetail(BagsListRes res) {
SmartDialog.show(
tag: "showPropsDetail",
alignment: Alignment.center,
animationType: SmartAnimationType.fade,
builder: (_) {
return PropsBagHeaddressDetailDialog(res);
},
onDismiss: () {
myHeaddress = AccountStorage().getHeaddress();
setState(() {});
},
);
}
Future<bool> _use(BagsListRes res, bool unload) async {
SCLoadingManager.show(context: context);
final localizations = SCAppLocalizations.of(context)!;

File diff suppressed because it is too large Load Diff

View File

@ -141,6 +141,7 @@ class SCFirstRechargeRewardItem {
const SCFirstRechargeRewardItem({
required this.id,
required this.type,
required this.typeName,
required this.name,
required this.content,
required this.quantity,
@ -152,33 +153,58 @@ class SCFirstRechargeRewardItem {
factory SCFirstRechargeRewardItem.fromJson(dynamic json) {
final map = _asMap(json);
final content = _readString(map['content']);
final explicitDays = _firstInt(map, const [
final badgeText = _firstString(map, const [
'badgeText',
'cornerText',
'tagText',
'durationText',
'validityText',
]);
final explicitDays = _firstDays(map, const [
'days',
'day',
'durationDays',
'validDays',
'validityDays',
'validity',
'validDay',
'expireDays',
'effectiveDays',
'duration',
'validPeriod',
]);
return SCFirstRechargeRewardItem(
id: _readString(map['id']),
type: _readString(map['type']),
typeName: _firstString(map, const [
'typeName',
'rewardTypeName',
'itemTypeName',
'propTypeName',
'goodsTypeName',
'categoryName',
'displayTypeName',
'typeLabel',
'typeDesc',
]),
name: _readString(map['name']),
content: content,
quantity: _readInt(map['quantity']),
cover: _readString(map['cover']),
badgeText: _firstString(map, const [
'badgeText',
'cornerText',
'tagText',
'durationText',
'validityText',
]),
days: explicitDays > 0 ? explicitDays : _readDaysFromText(content),
badgeText: badgeText,
days:
explicitDays > 0
? explicitDays
: _firstPositiveInt([
_readDaysFromText(badgeText),
_readDaysFromText(content),
]),
);
}
final String id;
final String type;
final String typeName;
final String name;
final String content;
final int quantity;
@ -193,27 +219,37 @@ class SCFirstRechargeRewardItem {
normalizedType.contains('CANDY');
}
String get displayTitle {
if (isCoinReward && quantity > 0) {
return '${_formatNumber(quantity)} Coins';
int get coinAmount {
if (!isCoinReward) {
return 0;
}
return _firstNonEmpty([name, content, quantity > 0 ? 'x$quantity' : '']);
return _firstPositiveInt([
quantity,
_readAmountFromText(content),
_readAmountFromText(name),
_readAmountFromText(badgeText),
]);
}
String get displayTitle {
if (isCoinReward) {
final amount = coinAmount;
return amount > 0 ? '${_formatNumber(amount)} Coins' : '';
}
return _firstNonEmpty([
typeName,
_formatRewardTypeName(type),
name,
content,
quantity > 0 ? 'x$quantity' : '',
]);
}
String get displayBadgeText {
if (isCoinReward) {
return '';
}
if (badgeText.trim().isNotEmpty) {
return badgeText.trim();
}
if (days > 0) {
return '${days}Days';
}
if (quantity > 1) {
return 'x$quantity';
}
return '';
return '${days > 0 ? days : 1}d';
}
}
@ -270,9 +306,18 @@ int _readInt(dynamic value) {
return 0;
}
int _firstInt(Map<String, dynamic> map, List<String> keys) {
int _firstDays(Map<String, dynamic> map, List<String> keys) {
for (final key in keys) {
final value = _readInt(map[key]);
final value = _readDaysValue(map[key]);
if (value > 0) {
return value;
}
}
return 0;
}
int _firstPositiveInt(List<int> values) {
for (final value in values) {
if (value > 0) {
return value;
}
@ -300,6 +345,31 @@ String _firstNonEmpty(List<String> values) {
return '';
}
int _readDaysValue(dynamic value) {
if (value is num) {
return value.toInt();
}
final text = _readString(value);
if (text.isEmpty) {
return 0;
}
final numeric = int.tryParse(text);
if (numeric != null) {
return numeric;
}
return _readDaysFromText(text);
}
int _readAmountFromText(String text) {
final match = RegExp(r'\d[\d,\.]*').firstMatch(text);
if (match == null) {
return 0;
}
final normalized = (match.group(0) ?? '').replaceAll(',', '');
final value = num.tryParse(normalized);
return value == null ? 0 : value.round();
}
String _readDiscountText(Map<String, dynamic> map) {
for (final key in const [
'discountText',
@ -370,6 +440,21 @@ int _readDaysFromText(String text) {
return int.tryParse(match.group(1) ?? '') ?? 0;
}
String _formatRewardTypeName(String type) {
final normalized = type.trim();
if (normalized.isEmpty) {
return '';
}
return normalized
.split(RegExp(r'[_\-\s]+'))
.where((segment) => segment.isNotEmpty)
.map((segment) {
final lower = segment.toLowerCase();
return lower[0].toUpperCase() + lower.substring(1);
})
.join(' ');
}
String _formatNumber(num value) {
final text = value.toInt().toString();
final buffer = StringBuffer();

View File

@ -88,13 +88,13 @@ class FirstRechargeDialogData {
),
FirstRechargeReward(
title: 'Vehicle',
badgeText: '3Days',
badgeText: '3d',
cover: _FirstRechargeAssets.rewardVehicle,
style: FirstRechargeRewardStyle.prop,
),
FirstRechargeReward(
title: 'Frame',
badgeText: '3Days',
badgeText: '3d',
cover: _FirstRechargeAssets.rewardFrame,
style: FirstRechargeRewardStyle.prop,
),
@ -113,13 +113,13 @@ class FirstRechargeDialogData {
rewards: [
FirstRechargeReward(
title: 'Avatar Frame',
badgeText: '7Days',
badgeText: '7d',
cover: _FirstRechargeAssets.rewardAvatarFrame,
style: FirstRechargeRewardStyle.prop,
),
FirstRechargeReward(
title: 'Room Frame',
badgeText: '7Days',
badgeText: '7d',
cover: _FirstRechargeAssets.rewardFrame,
style: FirstRechargeRewardStyle.prop,
),
@ -130,7 +130,7 @@ class FirstRechargeDialogData {
),
FirstRechargeReward(
title: 'Vehicle',
badgeText: '7Days',
badgeText: '7d',
cover: _FirstRechargeAssets.rewardVehicle,
style: FirstRechargeRewardStyle.prop,
),
@ -142,7 +142,7 @@ class FirstRechargeDialogData {
),
FirstRechargeReward(
title: 'Profile Frame',
badgeText: '7Days',
badgeText: '7d',
cover: _FirstRechargeAssets.rewardAvatarFrame,
style: FirstRechargeRewardStyle.prop,
),
@ -197,16 +197,16 @@ class FirstRechargeDialogData {
static bool _isDisplayableRewardItem(SCFirstRechargeRewardItem item) {
if (item.isCoinReward) {
return item.quantity > 0 || item.displayTitle.trim().isNotEmpty;
return item.coinAmount > 0;
}
return item.name.trim().isNotEmpty || item.cover.trim().isNotEmpty;
return item.displayTitle.trim().isNotEmpty || item.cover.trim().isNotEmpty;
}
static String _rewardTitle(SCFirstRechargeRewardItem item) {
if (item.isCoinReward) {
return item.displayTitle;
}
return item.name.trim();
return item.displayTitle;
}
static String _formatPriceText(SCFirstRechargeLevelConfig level) {

View File

@ -11,7 +11,6 @@ import 'package:yumi/main.dart';
import 'package:yumi/modules/wallet/recharge/recharge_page.dart';
import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/services/payment/google_payment_manager.dart';
import 'package:yumi/shared/business_logic/models/res/sc_product_config_res.dart';
import 'package:yumi/shared/tools/sc_loading_manager.dart';
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
@ -279,8 +278,7 @@ class _RoomRechargeBottomSheetState extends State<RoomRechargeBottomSheet> {
Widget _buildFallbackProductItem(int index) {
return _RechargeProductCard(
selected: index == _fallbackSelectedIndex,
amount: '150,000',
originalAmount: '100,000',
amount: '100,000',
price: 'USD 4.99',
onTap: () => setState(() => _fallbackSelectedIndex = index),
);
@ -294,32 +292,12 @@ class _RoomRechargeBottomSheetState extends State<RoomRechargeBottomSheet> {
final product = processor.productMap[productConfig.produc.id];
return _RechargeProductCard(
selected: productConfig.isSelecte,
amount: _formatAmount(_totalCandy(product)),
originalAmount: _formatOriginalAmount(product),
amount: _formatAmount((product?.obtainCandy ?? 0).round()),
price: productConfig.produc.price,
onTap: () => processor.chooseProductConfig(index),
);
}
String _formatOriginalAmount(SCProductConfigRes? product) {
if (product == null) {
return '';
}
final obtainCandy = (product.obtainCandy ?? 0).round();
final rewardCandy = (product.rewardCandy ?? 0).round();
if (obtainCandy <= 0 || rewardCandy <= 0) {
return '';
}
return _formatAmount(obtainCandy);
}
int _totalCandy(SCProductConfigRes? product) {
if (product == null) {
return 0;
}
return ((product.obtainCandy ?? 0) + (product.rewardCandy ?? 0)).round();
}
String _formatAmount(int value) {
final sign = value < 0 ? '-' : '';
final digits = value.abs().toString();
@ -415,14 +393,12 @@ class _RechargeProductCard extends StatelessWidget {
const _RechargeProductCard({
required this.selected,
required this.amount,
required this.originalAmount,
required this.price,
required this.onTap,
});
final bool selected;
final String amount;
final String originalAmount;
final String price;
final VoidCallback onTap;
@ -476,26 +452,6 @@ class _RechargeProductCard extends StatelessWidget {
),
],
),
SizedBox(height: 4.w),
Visibility(
visible: originalAmount.isNotEmpty,
maintainAnimation: true,
maintainSize: true,
maintainState: true,
child: Text(
originalAmount,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.6),
fontSize: 10.sp,
height: 1,
fontWeight: FontWeight.w400,
decoration: TextDecoration.lineThrough,
decorationColor: Colors.white.withValues(alpha: 0.6),
),
),
),
],
),
),

View File

@ -1014,3 +1014,27 @@
- [ ] 18:06:26 检查 iOS 模拟器设备,准备进行运行验证。
- [x] 18:06:55 已确认本机可用 iPhone 17 Pro 与 iPhone 17 Pro Max 模拟器,开始用 iPhone 17 Pro 编译运行验证。
- [ ] 20:27:16 新增调整:首充弹窗非金币奖励文本改用类型名称,左上角天数显示为 Xd缺省 1d。
- [ ] 20:27:44 已定位当前实现:非金币标题取 name角标取 badgeText/Days开始确认接口字段并调整模型。
- [ ] 20:28:12 接口文档示例未包含 typeName按兼容字段解析类型名称并保留 type 可读兜底。
- [x] 20:29:02 已修改首充奖励模型与 UI 映射:非金币标题优先类型名称,角标统一 Xd 且默认 1d。
- [ ] 20:29:12 开始格式化并检查首充奖励模型与弹窗文件。
- [ ] 20:29:34 静态检查发现旧的 _firstInt 已无引用,开始移除死代码。
- [x] 20:29:56 已完成格式化与静态检查:首充奖励模型、弹窗文件 flutter analyze 无问题。
- [ ] 20:38:55 反馈修正:金币类型标题必须显示金币金额,非金币才显示类型名称;开始调整解析兜底。
- [ ] 20:39:21 已确认金币 quantity 为空时会误用类型兜底;开始新增 coinAmount 并限制金币 item 只显示金额。
- [x] 20:39:46 已修正金币标题逻辑:金币只显示解析出的金额 Coins非金币才走类型名称。
- [ ] 20:39:56 开始格式化并静态检查金币金额显示修正。
- [x] 20:41:16 金币金额显示修正已通过格式化与 flutter analyze 检查。