最终版本

This commit is contained in:
roxy 2026-05-25 23:32:49 +08:00
parent 10d5e821f7
commit 8b05bb367a
6 changed files with 431 additions and 12 deletions

View File

@ -16,7 +16,7 @@ class SCVariant1Config implements AppConfig {
@override @override
String get apiHost => const String.fromEnvironment( String get apiHost => const String.fromEnvironment(
'API_HOST', 'API_HOST',
defaultValue: 'http://192.168.110.64:1100/', defaultValue: 'https://jvapi.haiyihy.com/',
); // --dart-define=API_HOST 线https://jvapi.haiyihy.com/ ); // --dart-define=API_HOST 线https://jvapi.haiyihy.com/
@override @override

View File

@ -141,6 +141,7 @@ class SCFirstRechargeRewardItem {
const SCFirstRechargeRewardItem({ const SCFirstRechargeRewardItem({
required this.id, required this.id,
required this.type, required this.type,
required this.detailType,
required this.typeName, required this.typeName,
required this.name, required this.name,
required this.content, required this.content,
@ -153,6 +154,7 @@ class SCFirstRechargeRewardItem {
factory SCFirstRechargeRewardItem.fromJson(dynamic json) { factory SCFirstRechargeRewardItem.fromJson(dynamic json) {
final map = _asMap(json); final map = _asMap(json);
final content = _readString(map['content']); final content = _readString(map['content']);
final detailType = _readRewardDetailType(map);
final badgeText = _firstString(map, const [ final badgeText = _firstString(map, const [
'badgeText', 'badgeText',
'cornerText', 'cornerText',
@ -176,6 +178,7 @@ class SCFirstRechargeRewardItem {
return SCFirstRechargeRewardItem( return SCFirstRechargeRewardItem(
id: _readString(map['id']), id: _readString(map['id']),
type: _readString(map['type']), type: _readString(map['type']),
detailType: detailType,
typeName: _firstString(map, const [ typeName: _firstString(map, const [
'typeName', 'typeName',
'rewardTypeName', 'rewardTypeName',
@ -190,7 +193,7 @@ class SCFirstRechargeRewardItem {
name: _readString(map['name']), name: _readString(map['name']),
content: content, content: content,
quantity: _readInt(map['quantity']), quantity: _readInt(map['quantity']),
cover: _readString(map['cover']), cover: _readRewardCover(map, detailType: detailType),
badgeText: badgeText, badgeText: badgeText,
days: days:
explicitDays > 0 explicitDays > 0
@ -204,6 +207,7 @@ class SCFirstRechargeRewardItem {
final String id; final String id;
final String type; final String type;
final String detailType;
final String typeName; final String typeName;
final String name; final String name;
final String content; final String content;
@ -238,6 +242,7 @@ class SCFirstRechargeRewardItem {
} }
return _firstNonEmpty([ return _firstNonEmpty([
_inferRewardTypeName( _inferRewardTypeName(
detailType: detailType,
typeName: typeName, typeName: typeName,
type: type, type: type,
name: name, name: name,
@ -254,7 +259,52 @@ class SCFirstRechargeRewardItem {
if (isCoinReward) { if (isCoinReward) {
return ''; return '';
} }
return '${days > 0 ? days : 1}d'; final directBadgeText = badgeText.trim();
if (directBadgeText.isNotEmpty) {
return directBadgeText;
}
if (isCountBasedReward) {
return 'x${quantity > 0 ? quantity : 1}';
}
if (usesQuantityAsDuration) {
final durationDays = _firstPositiveInt([days, quantity]);
return '${durationDays > 0 ? durationDays : 1}d';
}
return days > 0 ? '${days}d' : '';
}
bool get isCountBasedReward {
return _isGiftRewardType(_rewardTypeSearchText);
}
bool get usesQuantityAsDuration {
final explicitTypeSearchText =
[
detailType,
typeName,
type,
].where((value) => value.trim().isNotEmpty).join(' ').toLowerCase();
if (_isGiftRewardType(explicitTypeSearchText) ||
_isBadgeRewardType(explicitTypeSearchText)) {
return false;
}
if (_usesQuantityAsDuration(explicitTypeSearchText)) {
return true;
}
if (_isBadgeRewardType(_rewardTypeSearchText)) {
return false;
}
return _usesQuantityAsDuration(_rewardTypeSearchText);
}
String get _rewardTypeSearchText {
return [
detailType,
typeName,
type,
name,
content,
].where((value) => value.trim().isNotEmpty).join(' ').toLowerCase();
} }
} }
@ -340,6 +390,106 @@ String _firstString(Map<String, dynamic> map, List<String> keys) {
return ''; return '';
} }
String _readRewardDetailType(Map<String, dynamic> map) {
final direct = _firstString(map, const [
'detailType',
'rewardDetailType',
'itemDetailType',
'propType',
'propsType',
'goodsType',
'categoryType',
'materialType',
'resourceType',
'resourceTypeCode',
]);
if (direct.isNotEmpty) {
return direct;
}
for (final key in const ['propsResources', 'propsResource', 'resource']) {
final nested = _asMap(map[key]);
final nestedType = _firstString(nested, const [
'detailType',
'type',
'propType',
'propsType',
'materialType',
'resourceType',
]);
if (nestedType.isNotEmpty) {
return nestedType;
}
}
return '';
}
String _readRewardCover(
Map<String, dynamic> map, {
required String detailType,
}) {
final typeName = _normalizeRewardToken(detailType);
if (typeName == 'noblevip' || typeName == 'vip') {
final directShortBadge = _firstString(map, const [
'shortBadgeCover',
'shortBadgeCoverUrl',
'shortBadgeUrl',
'vipShortBadgeCover',
'vipShortBadgeUrl',
'badgeCover',
'badgeCoverUrl',
]);
if (directShortBadge.isNotEmpty) {
return directShortBadge;
}
final nestedShortBadge = _readNestedCover(map['shortBadge']);
if (nestedShortBadge.isNotEmpty) {
return nestedShortBadge;
}
}
final direct = _firstString(map, const [
'cover',
'coverUrl',
'imageUrl',
'iconUrl',
'photo',
'picture',
'giftPhoto',
'resourceUrl',
'url',
]);
if (direct.isNotEmpty) {
return direct;
}
for (final key in const ['propsResources', 'propsResource', 'resource']) {
final nestedCover = _readNestedCover(map[key]);
if (nestedCover.isNotEmpty) {
return nestedCover;
}
}
return '';
}
String _readNestedCover(dynamic value) {
final nested = _asMap(value);
if (nested.isEmpty) {
return '';
}
return _firstString(nested, const [
'cover',
'coverUrl',
'imageUrl',
'iconUrl',
'photo',
'picture',
'url',
'resourceUrl',
]);
}
String _firstNonEmpty(List<String> values) { String _firstNonEmpty(List<String> values) {
for (final value in values) { for (final value in values) {
final text = value.trim(); final text = value.trim();
@ -461,12 +611,18 @@ String _formatRewardTypeName(String type) {
} }
String _inferRewardTypeName({ String _inferRewardTypeName({
required String detailType,
required String typeName, required String typeName,
required String type, required String type,
required String name, required String name,
required String content, required String content,
required String cover, required String cover,
}) { }) {
final detailTypeName = _normalizeSpecificRewardTypeName(detailType);
if (detailTypeName.isNotEmpty) {
return detailTypeName;
}
final explicitTypeName = _normalizeSpecificRewardTypeName(typeName); final explicitTypeName = _normalizeSpecificRewardTypeName(typeName);
if (explicitTypeName.isNotEmpty) { if (explicitTypeName.isNotEmpty) {
return explicitTypeName; return explicitTypeName;
@ -474,6 +630,8 @@ String _inferRewardTypeName({
final searchable = final searchable =
[ [
detailType,
typeName,
type, type,
name, name,
content, content,
@ -506,6 +664,37 @@ String _inferRewardTypeName({
])) { ])) {
return 'Vehicles'; return 'Vehicles';
} }
if (_containsAny(searchable, const [
'chat_bubble',
'chatbubble',
'bubble',
'聊天气泡',
'气泡',
])) {
return 'Chat Bubble';
}
if (_containsAny(searchable, const [
'data_card',
'datacard',
'profile_card',
'资料卡',
])) {
return 'Data Card';
}
final textSearchable =
[
detailType,
typeName,
type,
name,
content,
].where((value) => value.trim().isNotEmpty).join(' ').toLowerCase();
if (_containsAny(textSearchable, const ['gift', 'present', '礼物'])) {
return 'Gift';
}
if (_containsAny(textSearchable, const ['badge', 'medal', '徽章', '勋章'])) {
return 'Badge';
}
if (_containsAny(searchable, const [ if (_containsAny(searchable, const [
'vip', 'vip',
'svip', 'svip',
@ -528,6 +717,13 @@ String _normalizeSpecificRewardTypeName(String value) {
if (normalized == 'prop' || normalized == 'props' || normalized == '道具') { if (normalized == 'prop' || normalized == 'props' || normalized == '道具') {
return ''; return '';
} }
if (normalized == 'noblevip' ||
normalized == 'vip' ||
normalized.contains('svip') ||
text.contains('贵族') ||
text.contains('会员')) {
return 'VIP';
}
if (normalized.contains('frame') || text.contains('')) { if (normalized.contains('frame') || text.contains('')) {
return 'Frames'; return 'Frames';
} }
@ -539,14 +735,76 @@ String _normalizeSpecificRewardTypeName(String value) {
text.contains('车辆')) { text.contains('车辆')) {
return 'Vehicles'; return 'Vehicles';
} }
if (normalized.contains('vip') || if (normalized == 'gift' ||
text.contains('贵族') || normalized == 'gifts' ||
text.contains('会员')) { normalized.contains('present') ||
return 'VIP'; text.contains('礼物')) {
return 'Gift';
}
if (normalized == 'chatbubble' ||
normalized.contains('chatbubble') ||
normalized.contains('bubble') ||
text.contains('聊天气泡') ||
text.contains('气泡')) {
return 'Chat Bubble';
}
if (normalized == 'datacard' ||
normalized.contains('datacard') ||
normalized.contains('profilecard') ||
text.contains('资料卡')) {
return 'Data Card';
}
if (normalized == 'badge' ||
normalized.contains('medal') ||
text.contains('徽章') ||
text.contains('勋章')) {
return 'Badge';
} }
return text; return text;
} }
String _normalizeRewardToken(String value) {
return value.trim().toLowerCase().replaceAll(RegExp(r'[_\-\s]+'), '');
}
bool _isGiftRewardType(String value) {
final normalized = _normalizeRewardToken(value);
return normalized.contains('gift') ||
normalized.contains('present') ||
value.contains('礼物');
}
bool _isBadgeRewardType(String value) {
final normalized = _normalizeRewardToken(value);
return normalized.contains('badge') ||
normalized.contains('medal') ||
value.contains('徽章') ||
value.contains('勋章');
}
bool _usesQuantityAsDuration(String value) {
final normalized = _normalizeRewardToken(value);
return normalized.contains('noblevip') ||
normalized.contains('vip') ||
normalized.contains('ride') ||
normalized.contains('vehicle') ||
normalized.contains('mount') ||
normalized.contains('avatarframe') ||
normalized.contains('frame') ||
normalized.contains('chatbubble') ||
normalized.contains('bubble') ||
normalized.contains('datacard') ||
normalized.contains('profilecard') ||
value.contains('贵族') ||
value.contains('会员') ||
value.contains('座驾') ||
value.contains('坐骑') ||
value.contains('头像框') ||
value.contains('聊天气泡') ||
value.contains('气泡') ||
value.contains('资料卡');
}
bool _containsAny(String value, List<String> tokens) { bool _containsAny(String value, List<String> tokens) {
for (final token in tokens) { for (final token in tokens) {
if (value.contains(token)) { if (value.contains(token)) {

View File

@ -844,6 +844,9 @@ class _RewardCover extends StatelessWidget {
if (title.contains('gift')) { if (title.contains('gift')) {
return _FirstRechargeAssets.rewardGift; return _FirstRechargeAssets.rewardGift;
} }
if (title.contains('badge') || title.contains('medal')) {
return _FirstRechargeAssets.rewardBadge;
}
return ''; return '';
} }
} }
@ -983,6 +986,7 @@ class _FirstRechargeAssets {
'sc_images/first_recharge/reward_coin_stack.png'; 'sc_images/first_recharge/reward_coin_stack.png';
static const String rewardGift = 'sc_images/first_recharge/reward_gift.png'; static const String rewardGift = 'sc_images/first_recharge/reward_gift.png';
static const String rewardVip = 'sc_images/vip/sc_vip_badge_1.png'; static const String rewardVip = 'sc_images/vip/sc_vip_badge_1.png';
static const String rewardBadge = 'sc_images/index/sc_icon_medals.png';
static const String buyNowButton = static const String buyNowButton =
'sc_images/first_recharge/buy_now_button.png'; 'sc_images/first_recharge/buy_now_button.png';
static const String discountBadge = static const String discountBadge =

View File

@ -140,11 +140,14 @@ class _FloatingGameScreenWidgetState extends State<FloatingGameScreenWidget>
final currentRoomId = final currentRoomId =
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ""; rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
final messageRoomId = (widget.message.roomId ?? "").trim(); final messageRoomId = (widget.message.roomId ?? "").trim();
if (!_isValidRoomId(messageRoomId)) {
return;
}
final isSameVisibleRoom = final isSameVisibleRoom =
rtcProvider.shouldShowRoomVisualEffects && rtcProvider.shouldShowRoomVisualEffects &&
rtcProvider.isVoiceRoomRouteVisible && rtcProvider.isVoiceRoomRouteVisible &&
currentRoomId.isNotEmpty && currentRoomId.isNotEmpty &&
(messageRoomId.isEmpty || currentRoomId == messageRoomId); currentRoomId == messageRoomId;
if (isSameVisibleRoom) { if (isSameVisibleRoom) {
showRoomGameListSheet( showRoomGameListSheet(
@ -153,9 +156,7 @@ class _FloatingGameScreenWidgetState extends State<FloatingGameScreenWidget>
); );
return; return;
} }
if (messageRoomId.isNotEmpty) { SCRoomUtils.goRoom(messageRoomId, currentContext, fromFloting: true);
SCRoomUtils.goRoom(messageRoomId, currentContext, fromFloting: true);
}
}, },
); );
} }
@ -453,6 +454,10 @@ class _FloatingGameScreenWidgetState extends State<FloatingGameScreenWidget>
return gameId.isEmpty ? null : gameId; return gameId.isEmpty ? null : gameId;
} }
bool _isValidRoomId(String roomId) {
return roomId.isNotEmpty && roomId != "0";
}
String _formatCoins(num? value) { String _formatCoins(num? value) {
final coins = value ?? 0; final coins = value ?? 0;
if (coins >= 100000) { if (coins >= 100000) {

View File

@ -289,7 +289,7 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
return; return;
} }
final messageRoomId = (widget.message.roomId ?? "").trim(); final messageRoomId = (widget.message.roomId ?? "").trim();
if (messageRoomId.isEmpty) { if (!_isValidRoomId(messageRoomId)) {
return; return;
} }
final rtcProvider = Provider.of<RealTimeCommunicationManager>( final rtcProvider = Provider.of<RealTimeCommunicationManager>(
@ -330,6 +330,10 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
_swipeController.forward(); _swipeController.forward();
} }
bool _isValidRoomId(String roomId) {
return roomId.isNotEmpty && roomId != "0";
}
String _winnerName() { String _winnerName() {
return _firstNonBlank([ return _firstNonBlank([
widget.message.userName, widget.message.userName,

View File

@ -0,0 +1,148 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yumi/shared/business_logic/models/res/sc_first_recharge_reward_res.dart';
void main() {
group('SCFirstRechargeRewardItem display mapping', () {
test('maps known non-coin detail types', () {
final cases = <String, String>{
'NOBLE_VIP': 'VIP',
'RIDE': 'Vehicles',
'AVATAR_FRAME': 'Frames',
'CHAT_BUBBLE': 'Chat Bubble',
'DATA_CARD': 'Data Card',
'GIFT': 'Gift',
'BADGE': 'Badge',
};
cases.forEach((detailType, expectedTitle) {
final item = SCFirstRechargeRewardItem.fromJson({
'id': detailType,
'type': 'PROPS',
'detailType': detailType,
'name': 'Concrete material name',
'quantity': 7,
'cover': 'https://example.com/$detailType.png',
});
expect(item.displayTitle, expectedTitle);
});
});
test('uses quantity as days for duration based props', () {
for (final detailType in const [
'NOBLE_VIP',
'RIDE',
'AVATAR_FRAME',
'CHAT_BUBBLE',
'DATA_CARD',
]) {
final item = SCFirstRechargeRewardItem.fromJson({
'id': detailType,
'type': 'PROPS',
'detailType': detailType,
'quantity': 9,
});
expect(item.displayBadgeText, '9d');
}
});
test('treats vip aliases as duration rewards', () {
final item = SCFirstRechargeRewardItem.fromJson({
'id': 11,
'type': 'PROPS',
'typeName': 'VIP',
'quantity': 5,
'shortBadgeCoverUrl': 'https://example.com/vip-short.png',
});
expect(item.displayTitle, 'VIP');
expect(item.displayBadgeText, '5d');
});
test('uses detailType before material name for vehicle rewards', () {
final item = SCFirstRechargeRewardItem.fromJson({
'id': 1,
'type': 'PROPS',
'detailType': 'RIDE',
'name': 'Meteor Trail',
'content': '1962463978629165057',
'quantity': 7,
'cover': 'https://example.com/material.png',
});
expect(item.displayTitle, 'Vehicles');
expect(item.displayBadgeText, '7d');
});
test('uses quantity badge for gift rewards', () {
final item = SCFirstRechargeRewardItem.fromJson({
'id': 2,
'type': 'PROPS',
'detailType': 'GIFT',
'name': 'Rose',
'content': '1001',
'quantity': 3,
'cover': 'https://example.com/rose.png',
});
expect(item.displayTitle, 'Gift');
expect(item.displayBadgeText, 'x3');
});
test('does not invent a duration badge for badge rewards', () {
final item = SCFirstRechargeRewardItem.fromJson({
'id': 22,
'type': 'PROPS',
'detailType': 'BADGE',
'name': 'VIP Medal',
'quantity': 7,
'cover': 'https://example.com/badge.png',
});
expect(item.displayTitle, 'Badge');
expect(item.displayBadgeText, '');
});
test('keeps explicit corner text from the API', () {
final item = SCFirstRechargeRewardItem.fromJson({
'id': 3,
'type': 'PROPS',
'detailType': 'GIFT',
'name': 'Rose',
'quantity': 3,
'cornerText': 'x8',
});
expect(item.displayBadgeText, 'x8');
});
test('reads nested resource type when direct detailType is missing', () {
final item = SCFirstRechargeRewardItem.fromJson({
'id': 4,
'type': 'PROPS',
'name': 'Meteor Trail',
'quantity': 3,
'propsResources': {'type': 'RIDE'},
});
expect(item.displayTitle, 'Vehicles');
expect(item.displayBadgeText, '3d');
});
test('uses vip short badge cover before generic cover', () {
final item = SCFirstRechargeRewardItem.fromJson({
'id': 5,
'type': 'PROPS',
'detailType': 'NOBLE_VIP',
'quantity': 7,
'cover': 'https://example.com/long-badge.png',
'shortBadge': {'coverUrl': 'https://example.com/short-badge.png'},
});
expect(item.displayTitle, 'VIP');
expect(item.cover, 'https://example.com/short-badge.png');
expect(item.displayBadgeText, '7d');
});
});
}