最终版本

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
String get apiHost => const String.fromEnvironment(
'API_HOST',
defaultValue: 'http://192.168.110.64:1100/',
defaultValue: 'https://jvapi.haiyihy.com/',
); // --dart-define=API_HOST 线https://jvapi.haiyihy.com/
@override

View File

@ -141,6 +141,7 @@ class SCFirstRechargeRewardItem {
const SCFirstRechargeRewardItem({
required this.id,
required this.type,
required this.detailType,
required this.typeName,
required this.name,
required this.content,
@ -153,6 +154,7 @@ class SCFirstRechargeRewardItem {
factory SCFirstRechargeRewardItem.fromJson(dynamic json) {
final map = _asMap(json);
final content = _readString(map['content']);
final detailType = _readRewardDetailType(map);
final badgeText = _firstString(map, const [
'badgeText',
'cornerText',
@ -176,6 +178,7 @@ class SCFirstRechargeRewardItem {
return SCFirstRechargeRewardItem(
id: _readString(map['id']),
type: _readString(map['type']),
detailType: detailType,
typeName: _firstString(map, const [
'typeName',
'rewardTypeName',
@ -190,7 +193,7 @@ class SCFirstRechargeRewardItem {
name: _readString(map['name']),
content: content,
quantity: _readInt(map['quantity']),
cover: _readString(map['cover']),
cover: _readRewardCover(map, detailType: detailType),
badgeText: badgeText,
days:
explicitDays > 0
@ -204,6 +207,7 @@ class SCFirstRechargeRewardItem {
final String id;
final String type;
final String detailType;
final String typeName;
final String name;
final String content;
@ -238,6 +242,7 @@ class SCFirstRechargeRewardItem {
}
return _firstNonEmpty([
_inferRewardTypeName(
detailType: detailType,
typeName: typeName,
type: type,
name: name,
@ -254,7 +259,52 @@ class SCFirstRechargeRewardItem {
if (isCoinReward) {
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 '';
}
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) {
for (final value in values) {
final text = value.trim();
@ -461,12 +611,18 @@ String _formatRewardTypeName(String type) {
}
String _inferRewardTypeName({
required String detailType,
required String typeName,
required String type,
required String name,
required String content,
required String cover,
}) {
final detailTypeName = _normalizeSpecificRewardTypeName(detailType);
if (detailTypeName.isNotEmpty) {
return detailTypeName;
}
final explicitTypeName = _normalizeSpecificRewardTypeName(typeName);
if (explicitTypeName.isNotEmpty) {
return explicitTypeName;
@ -474,6 +630,8 @@ String _inferRewardTypeName({
final searchable =
[
detailType,
typeName,
type,
name,
content,
@ -506,6 +664,37 @@ String _inferRewardTypeName({
])) {
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 [
'vip',
'svip',
@ -528,6 +717,13 @@ String _normalizeSpecificRewardTypeName(String value) {
if (normalized == 'prop' || normalized == 'props' || normalized == '道具') {
return '';
}
if (normalized == 'noblevip' ||
normalized == 'vip' ||
normalized.contains('svip') ||
text.contains('贵族') ||
text.contains('会员')) {
return 'VIP';
}
if (normalized.contains('frame') || text.contains('')) {
return 'Frames';
}
@ -539,14 +735,76 @@ String _normalizeSpecificRewardTypeName(String value) {
text.contains('车辆')) {
return 'Vehicles';
}
if (normalized.contains('vip') ||
text.contains('贵族') ||
text.contains('会员')) {
return 'VIP';
if (normalized == 'gift' ||
normalized == 'gifts' ||
normalized.contains('present') ||
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;
}
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) {
for (final token in tokens) {
if (value.contains(token)) {

View File

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

View File

@ -140,11 +140,14 @@ class _FloatingGameScreenWidgetState extends State<FloatingGameScreenWidget>
final currentRoomId =
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
final messageRoomId = (widget.message.roomId ?? "").trim();
if (!_isValidRoomId(messageRoomId)) {
return;
}
final isSameVisibleRoom =
rtcProvider.shouldShowRoomVisualEffects &&
rtcProvider.isVoiceRoomRouteVisible &&
currentRoomId.isNotEmpty &&
(messageRoomId.isEmpty || currentRoomId == messageRoomId);
currentRoomId == messageRoomId;
if (isSameVisibleRoom) {
showRoomGameListSheet(
@ -153,9 +156,7 @@ class _FloatingGameScreenWidgetState extends State<FloatingGameScreenWidget>
);
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;
}
bool _isValidRoomId(String roomId) {
return roomId.isNotEmpty && roomId != "0";
}
String _formatCoins(num? value) {
final coins = value ?? 0;
if (coins >= 100000) {

View File

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