feat: render VIP profile card resources

This commit is contained in:
zhx 2026-05-16 02:21:43 +08:00
parent 57ce270b4f
commit 56a562b770
6 changed files with 230 additions and 94 deletions

View File

@ -587,7 +587,11 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
return Stack(
fit: StackFit.expand,
children: [
SCDataCardResourceView(resource: dataCard, fit: BoxFit.cover),
SCDataCardResourceView(
resource: dataCard,
fit: BoxFit.cover,
fallback: DecoratedBox(decoration: _defaultDataCardDecoration()),
),
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
@ -606,6 +610,21 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
);
}
BoxDecoration _defaultDataCardDecoration() {
return const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xff18F2B1),
Color(0xffE4FFF6),
Color(0xffF7FFFC),
Color(0xffFFFFFF),
],
),
);
}
PropsResources? _activeDataCard(SocialChatUserProfile? profile) {
final now = DateTime.now().millisecondsSinceEpoch;
for (final item in profile?.useProps ?? const <UseProps>[]) {

View File

@ -13,6 +13,7 @@ import 'package:yumi/shared/tools/sc_loading_manager.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/components/sc_tts.dart';
import 'package:yumi/ui_kit/widgets/props/sc_data_card_resource_view.dart';
class VipDetailPage extends StatefulWidget {
const VipDetailPage({super.key});
@ -58,7 +59,6 @@ class _VipDetailPageState extends State<VipDetailPage> {
final status = results[1] as SCVipStatusRes;
final resolvedStatus = _resolveStatus(status, home.state);
final selectedLevel = _resolveInitialLevel(home, resolvedStatus);
_logLoadedVipData(home, resolvedStatus, selectedLevel);
if (!mounted) return;
setState(() {
@ -141,11 +141,6 @@ class _VipDetailPageState extends State<VipDetailPage> {
_selectedLevel = level;
_preview = null;
});
_logSelectedLevelResources(
level,
config: _levelConfigFor(level),
status: _status,
);
_loadPreviewForLevel(level);
}
@ -201,6 +196,7 @@ class _VipDetailPageState extends State<VipDetailPage> {
await _loadVipData(showLoading: false);
}
} catch (error) {
debugPrint('VIP purchase failed: $error');
} finally {
SCLoadingManager.hide();
if (mounted) {
@ -214,56 +210,6 @@ class _VipDetailPageState extends State<VipDetailPage> {
return 'vip-buy-$userId-${DateTime.now().millisecondsSinceEpoch}';
}
void _logLoadedVipData(
SCVipHomeRes home,
SCVipStatusRes? status,
int selectedLevel,
) {
if (home.configured != true) {}
if (home.levels.isEmpty) {}
_logSelectedLevelResources(
selectedLevel,
config: _levelConfigFromHome(home, selectedLevel),
status: status,
);
}
void _logSelectedLevelResources(
int level, {
required SCVipLevelConfigRes? config,
required SCVipStatusRes? status,
}) {
final state = status?.levelInt == level ? status : null;
if (config == null && state == null) {
return;
}
final badge = _preferResource(config?.badge, state?.badge);
final avatarFrame = _preferResource(
config?.avatarFrame,
state?.avatarFrame,
);
final entryEffect = _preferResource(
config?.entryEffect,
state?.entryEffect,
);
final chatBubble = _preferResource(config?.chatBubble, state?.chatBubble);
final floatPicture = _preferResource(
config?.floatPicture,
state?.floatPicture,
);
}
String _resourceLogValue(SCVipResourceRes? resource) {
if (!_hasResource(resource)) {
return 'null';
}
return '{id:${resource?.resourceId},name:${resource?.name},'
'hasPreviewUrl:${resource?.hasPreviewUrl},'
'hasSourceUrl:${resource?.hasSourceUrl},'
'previewUrl:${resource?.previewUrl},'
'sourceUrl:${resource?.sourceResourceUrl}}';
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -524,6 +470,8 @@ class _VipDetailPageState extends State<VipDetailPage> {
Widget _buildLevelCard() {
final assetLevel = _assetLevel;
final backgroundCard = _selectedBackgroundCard;
final effectImage = _selectedEffectImage ?? _selectedLongBadge;
return Center(
child: SizedBox(
width: 351.w,
@ -532,18 +480,23 @@ class _VipDetailPageState extends State<VipDetailPage> {
clipBehavior: Clip.none,
children: [
Positioned.fill(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.w),
border: Border.all(
color: const Color(0xFFEBCB76),
width: 1.w,
),
image: DecorationImage(
image: AssetImage(
'sc_images/vip/sc_vip_card_bg_$assetLevel.png',
child: ClipRRect(
borderRadius: BorderRadius.circular(8.w),
child: _buildVipCardBackground(
resource: backgroundCard,
assetLevel: assetLevel,
),
),
),
Positioned.fill(
child: IgnorePointer(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.w),
border: Border.all(
color: const Color(0xFFEBCB76),
width: 1.w,
),
fit: BoxFit.fill,
),
),
),
@ -575,8 +528,8 @@ class _VipDetailPageState extends State<VipDetailPage> {
PositionedDirectional(
end: 24.w,
top: -36.w,
child: _buildResourceImage(
resource: _selectedBadge,
child: _buildDynamicResourceVisual(
resource: effectImage,
width: 135.w,
height: 135.w,
fallbackIcon: Icons.workspace_premium_outlined,
@ -588,6 +541,27 @@ class _VipDetailPageState extends State<VipDetailPage> {
);
}
Widget _buildVipCardBackground({
required SCVipResourceRes? resource,
required int assetLevel,
}) {
final fallback = Image.asset(
'sc_images/vip/sc_vip_card_bg_$assetLevel.png',
fit: BoxFit.fill,
width: double.infinity,
height: double.infinity,
);
if (!_hasResourceUrl(resource)) {
return fallback;
}
return SCDataCardResourceView(
sourceUrl: resource?.sourceResourceUrl,
coverUrl: resource?.previewUrl,
fit: BoxFit.fill,
fallback: fallback,
);
}
Widget _buildLevelDivider() {
final ornamentLayout = _ornamentLayoutForLevel(_assetLevel);
return SizedBox(
@ -713,9 +687,14 @@ class _VipDetailPageState extends State<VipDetailPage> {
);
}
SCVipResourceRes? get _selectedBadge => _selectedResource(
fromConfig: (config) => config.badge,
fromState: (status) => status.badge,
SCVipResourceRes? get _selectedLongBadge => _selectedResource(
fromConfig: (config) => config.longBadge,
fromState: (status) => status.longBadge,
);
SCVipResourceRes? get _selectedShortBadge => _selectedResource(
fromConfig: (config) => config.shortBadge,
fromState: (status) => status.shortBadge,
);
SCVipResourceRes? get _selectedAvatarFrame => _selectedResource(
@ -738,6 +717,16 @@ class _VipDetailPageState extends State<VipDetailPage> {
fromState: (status) => status.floatPicture,
);
SCVipResourceRes? get _selectedBackgroundCard => _selectedResource(
fromConfig: (config) => config.backgroundCard,
fromState: (status) => status.backgroundCard,
);
SCVipResourceRes? get _selectedEffectImage => _selectedResource(
fromConfig: (config) => config.effectImage,
fromState: (status) => status.effectImage,
);
SCVipResourceRes? _selectedResource({
required SCVipResourceRes? Function(SCVipLevelConfigRes config) fromConfig,
required SCVipResourceRes? Function(SCVipStatusRes status) fromState,
@ -768,8 +757,14 @@ class _VipDetailPageState extends State<VipDetailPage> {
final items = <_VipFeatureData>[];
_addResourceItem(
items,
resource: _selectedBadge,
fallbackTitle: 'VIP Badge',
resource: _selectedLongBadge,
fallbackTitle: 'Long Badge',
fallbackIcon: Icons.workspace_premium_outlined,
);
_addResourceItem(
items,
resource: _selectedShortBadge,
fallbackTitle: 'Short Badge',
fallbackIcon: Icons.workspace_premium_outlined,
);
_addResourceItem(
@ -797,12 +792,19 @@ class _VipDetailPageState extends State<VipDetailPage> {
fallbackIcon: Icons.airline_seat_recline_extra,
wide: true,
);
_addResourceItem(
items,
resource: _selectedBackgroundCard,
fallbackTitle: 'Profile Card',
fallbackIcon: Icons.badge_outlined,
wide: true,
);
return items;
}
List<_VipFeatureData> get _vipPrivilegeItems {
// The VIP home API currently returns resource perks only:
// badge/avatarFrame/entryEffect/chatBubble/floatPicture.
// badges/avatarFrame/entryEffect/chatBubble/floatPicture/backgroundCard.
// Do not render demo-only functional privileges without backend data.
return const <_VipFeatureData>[];
}
@ -844,6 +846,40 @@ class _VipDetailPageState extends State<VipDetailPage> {
return fallbackTitle;
}
Widget _buildDynamicResourceVisual({
required SCVipResourceRes? resource,
required double width,
required double height,
required IconData fallbackIcon,
}) {
final sourceUrl = resource?.sourceResourceUrl?.trim();
final previewUrl = resource?.previewUrl?.trim();
final hasSource = sourceUrl != null && sourceUrl.isNotEmpty;
final hasPreview = previewUrl != null && previewUrl.isNotEmpty;
if (hasSource || hasPreview) {
return SCDataCardResourceView(
sourceUrl: sourceUrl,
coverUrl: previewUrl,
width: width,
height: height,
fit: BoxFit.contain,
fallback: _buildResourceImage(
resource: resource,
width: width,
height: height,
fallbackIcon: fallbackIcon,
),
);
}
return _buildResourceImage(
resource: resource,
width: width,
height: height,
fallbackIcon: fallbackIcon,
);
}
Widget _buildResourceImage({
required SCVipResourceRes? resource,
required double width,

View File

@ -110,10 +110,14 @@ class SCVipStatusRes {
String? startAt,
String? expireAt,
SCVipResourceRes? badge,
SCVipResourceRes? longBadge,
SCVipResourceRes? shortBadge,
SCVipResourceRes? avatarFrame,
SCVipResourceRes? entryEffect,
SCVipResourceRes? chatBubble,
SCVipResourceRes? floatPicture,
SCVipResourceRes? backgroundCard,
SCVipResourceRes? effectImage,
}) {
_userId = userId;
_sysOrigin = sysOrigin;
@ -125,10 +129,14 @@ class SCVipStatusRes {
_startAt = startAt;
_expireAt = expireAt;
_badge = badge;
_longBadge = longBadge;
_shortBadge = shortBadge;
_avatarFrame = avatarFrame;
_entryEffect = entryEffect;
_chatBubble = chatBubble;
_floatPicture = floatPicture;
_backgroundCard = backgroundCard;
_effectImage = effectImage;
}
SCVipStatusRes.fromJson(dynamic json) {
@ -144,10 +152,14 @@ class SCVipStatusRes {
_startAt = _stringValue(map['startAt']);
_expireAt = _stringValue(map['expireAt']);
_badge = _resourceValue(map['badge']);
_longBadge = _resourceValue(map['longBadge']) ?? _badge;
_shortBadge = _resourceValue(map['shortBadge']);
_avatarFrame = _resourceValue(map['avatarFrame']);
_entryEffect = _resourceValue(map['entryEffect']);
_chatBubble = _resourceValue(map['chatBubble']);
_floatPicture = _resourceValue(map['floatPicture']);
_backgroundCard = _resourceValue(map['backgroundCard']);
_effectImage = _resourceValue(map['effectImage']);
}
String? _userId;
@ -160,10 +172,14 @@ class SCVipStatusRes {
String? _startAt;
String? _expireAt;
SCVipResourceRes? _badge;
SCVipResourceRes? _longBadge;
SCVipResourceRes? _shortBadge;
SCVipResourceRes? _avatarFrame;
SCVipResourceRes? _entryEffect;
SCVipResourceRes? _chatBubble;
SCVipResourceRes? _floatPicture;
SCVipResourceRes? _backgroundCard;
SCVipResourceRes? _effectImage;
String? get userId => _userId;
@ -183,7 +199,11 @@ class SCVipStatusRes {
String? get expireAt => _expireAt;
SCVipResourceRes? get badge => _badge;
SCVipResourceRes? get badge => longBadge;
SCVipResourceRes? get longBadge => _longBadge ?? _badge;
SCVipResourceRes? get shortBadge => _shortBadge;
SCVipResourceRes? get avatarFrame => _avatarFrame;
@ -193,6 +213,10 @@ class SCVipStatusRes {
SCVipResourceRes? get floatPicture => _floatPicture;
SCVipResourceRes? get backgroundCard => _backgroundCard;
SCVipResourceRes? get effectImage => _effectImage;
bool get isActive => _active == true && (_level ?? 0) > 0;
int get levelInt => (_level ?? 0).toInt();
@ -216,11 +240,17 @@ class SCVipStatusRes {
map['displayName'] = _displayName;
map['startAt'] = _startAt;
map['expireAt'] = _expireAt;
if (_badge != null) map['badge'] = _badge?.toJson();
if (badge != null) map['badge'] = badge?.toJson();
if (longBadge != null) map['longBadge'] = longBadge?.toJson();
if (_shortBadge != null) map['shortBadge'] = _shortBadge?.toJson();
if (_avatarFrame != null) map['avatarFrame'] = _avatarFrame?.toJson();
if (_entryEffect != null) map['entryEffect'] = _entryEffect?.toJson();
if (_chatBubble != null) map['chatBubble'] = _chatBubble?.toJson();
if (_floatPicture != null) map['floatPicture'] = _floatPicture?.toJson();
if (_backgroundCard != null) {
map['backgroundCard'] = _backgroundCard?.toJson();
}
if (_effectImage != null) map['effectImage'] = _effectImage?.toJson();
return map;
}
}
@ -234,10 +264,14 @@ class SCVipLevelConfigRes {
num? durationDays,
num? priceGold,
SCVipResourceRes? badge,
SCVipResourceRes? longBadge,
SCVipResourceRes? shortBadge,
SCVipResourceRes? avatarFrame,
SCVipResourceRes? entryEffect,
SCVipResourceRes? chatBubble,
SCVipResourceRes? floatPicture,
SCVipResourceRes? backgroundCard,
SCVipResourceRes? effectImage,
bool? canPurchase,
num? payableGold,
}) {
@ -248,10 +282,14 @@ class SCVipLevelConfigRes {
_durationDays = durationDays;
_priceGold = priceGold;
_badge = badge;
_longBadge = longBadge;
_shortBadge = shortBadge;
_avatarFrame = avatarFrame;
_entryEffect = entryEffect;
_chatBubble = chatBubble;
_floatPicture = floatPicture;
_backgroundCard = backgroundCard;
_effectImage = effectImage;
_canPurchase = canPurchase;
_payableGold = payableGold;
}
@ -266,10 +304,14 @@ class SCVipLevelConfigRes {
_durationDays = _numValue(map['durationDays']);
_priceGold = _numValue(map['priceGold']);
_badge = _resourceValue(map['badge']);
_longBadge = _resourceValue(map['longBadge']) ?? _badge;
_shortBadge = _resourceValue(map['shortBadge']);
_avatarFrame = _resourceValue(map['avatarFrame']);
_entryEffect = _resourceValue(map['entryEffect']);
_chatBubble = _resourceValue(map['chatBubble']);
_floatPicture = _resourceValue(map['floatPicture']);
_backgroundCard = _resourceValue(map['backgroundCard']);
_effectImage = _resourceValue(map['effectImage']);
_canPurchase = _boolValue(map['canPurchase']);
_payableGold = _numValue(map['payableGold']);
}
@ -281,10 +323,14 @@ class SCVipLevelConfigRes {
num? _durationDays;
num? _priceGold;
SCVipResourceRes? _badge;
SCVipResourceRes? _longBadge;
SCVipResourceRes? _shortBadge;
SCVipResourceRes? _avatarFrame;
SCVipResourceRes? _entryEffect;
SCVipResourceRes? _chatBubble;
SCVipResourceRes? _floatPicture;
SCVipResourceRes? _backgroundCard;
SCVipResourceRes? _effectImage;
bool? _canPurchase;
num? _payableGold;
@ -300,7 +346,11 @@ class SCVipLevelConfigRes {
num? get priceGold => _priceGold;
SCVipResourceRes? get badge => _badge;
SCVipResourceRes? get badge => longBadge;
SCVipResourceRes? get longBadge => _longBadge ?? _badge;
SCVipResourceRes? get shortBadge => _shortBadge;
SCVipResourceRes? get avatarFrame => _avatarFrame;
@ -310,6 +360,10 @@ class SCVipLevelConfigRes {
SCVipResourceRes? get floatPicture => _floatPicture;
SCVipResourceRes? get backgroundCard => _backgroundCard;
SCVipResourceRes? get effectImage => _effectImage;
bool? get canPurchase => _canPurchase;
num? get payableGold => _payableGold;
@ -332,11 +386,17 @@ class SCVipLevelConfigRes {
map['enabled'] = _enabled;
map['durationDays'] = _durationDays;
map['priceGold'] = _priceGold;
if (_badge != null) map['badge'] = _badge?.toJson();
if (badge != null) map['badge'] = badge?.toJson();
if (longBadge != null) map['longBadge'] = longBadge?.toJson();
if (_shortBadge != null) map['shortBadge'] = _shortBadge?.toJson();
if (_avatarFrame != null) map['avatarFrame'] = _avatarFrame?.toJson();
if (_entryEffect != null) map['entryEffect'] = _entryEffect?.toJson();
if (_chatBubble != null) map['chatBubble'] = _chatBubble?.toJson();
if (_floatPicture != null) map['floatPicture'] = _floatPicture?.toJson();
if (_backgroundCard != null) {
map['backgroundCard'] = _backgroundCard?.toJson();
}
if (_effectImage != null) map['effectImage'] = _effectImage?.toJson();
map['canPurchase'] = _canPurchase;
map['payableGold'] = _payableGold;
return map;

View File

@ -1,13 +1,16 @@
enum SCPropsType {
AVATAR_FRAME,
FLOAT_PICTURE,
DATA_CARD,
CHAT_BUBBLE,
RIDE,
LAYOUT,
THEME,
BADGE,
NOBLE_VIP,
FRAGMENTS,
RED_PACKET,
}
// ignore_for_file: constant_identifier_names
enum SCPropsType {
AVATAR_FRAME,
FLOAT_PICTURE,
DATA_CARD,
CHAT_BUBBLE,
RIDE,
LAYOUT,
THEME,
BADGE,
NOBLE_VIP,
VIP_EFFECT_IMAGE,
FRAGMENTS,
RED_PACKET,
}

View File

@ -362,6 +362,7 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
_controller = controller;
controller.setAnimListener(
onFailed: (_, __, ___) => _markFailed(),
onVideoComplete: _handleVideoComplete,
onVideoStart: () {
if (mounted && _failed) {
setState(() => _failed = false);
@ -371,6 +372,19 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
_play();
}
void _handleVideoComplete() {
if (!mounted || !widget.loop || _failed) {
return;
}
unawaited(
Future<void>.delayed(Duration.zero, () {
if (mounted && widget.loop && !_failed) {
return _play();
}
}),
);
}
Future<void> _play() async {
final controller = _controller;
final url = widget.url.trim();

View File

@ -270,7 +270,11 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
return Stack(
fit: StackFit.expand,
children: [
SCDataCardResourceView(resource: dataCard, fit: BoxFit.cover),
SCDataCardResourceView(
resource: dataCard,
fit: BoxFit.fill,
fallback: DecoratedBox(decoration: _sheetDecoration()),
),
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(