diff --git a/YuMi/Modules/YMMine/Model/Medals/MedalsModel.h b/YuMi/Modules/YMMine/Model/Medals/MedalsModel.h index ff901bf8..c7de3d27 100644 --- a/YuMi/Modules/YMMine/Model/Medals/MedalsModel.h +++ b/YuMi/Modules/YMMine/Model/Medals/MedalsModel.h @@ -22,12 +22,17 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, copy) NSString *picUrl; @property (nonatomic, assign) BOOL useStatus; +/// 将 expireSeconds 转换为 "yyyy/MM/dd" 格式的字符串 +- (NSString *)expireDateString; + @end @interface MedalSeriesItemVo : PIBaseModel @property (nonatomic, assign) NSInteger medalLevel; @property (nonatomic, copy) NSString *seriesName; +@property (nonatomic, copy) NSString *mp4Url; +@property (nonatomic, copy) NSString *picUrl; @property (nonatomic, copy) NSArray *medalVos; @end diff --git a/YuMi/Modules/YMMine/Model/Medals/MedalsModel.m b/YuMi/Modules/YMMine/Model/Medals/MedalsModel.m index 4b4b908a..260c44c2 100644 --- a/YuMi/Modules/YMMine/Model/Medals/MedalsModel.m +++ b/YuMi/Modules/YMMine/Model/Medals/MedalsModel.m @@ -9,6 +9,23 @@ @implementation MedalVo +/// 将 expireSeconds 转换为 "yyyy/MM/dd" 格式的字符串 +- (NSString *)expireDateString { + if (self.expireSeconds <= 0) { + return YMLocalizedString(@"20.20.61_text_9"); + } + + // 将秒转换为 NSDate + NSDate *expireDate = [NSDate dateWithTimeIntervalSince1970:self.expireSeconds]; + + // 创建日期格式化器 + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + formatter.dateFormat = @"yyyy/MM/dd"; + + // 返回格式化后的字符串 + return [NSString stringWithFormat:YMLocalizedString(@"20.20.61_text_8"), [formatter stringFromDate:expireDate]]; +} + @end @implementation MedalSeriesItemVo diff --git a/YuMi/Modules/YMMine/View/Medals/MedalsCollectionViewCell.m b/YuMi/Modules/YMMine/View/Medals/MedalsCollectionViewCell.m index ddfc64ae..82c55aa3 100644 --- a/YuMi/Modules/YMMine/View/Medals/MedalsCollectionViewCell.m +++ b/YuMi/Modules/YMMine/View/Medals/MedalsCollectionViewCell.m @@ -12,7 +12,7 @@ #import "MedalsLevelIndicatorView.h" -@interface MedalsCollectionViewCell () +@interface MedalsCollectionViewCell () @property(nonatomic, copy) NSString *imagePath; @property(nonatomic, copy) NSString *mp4Path; @@ -154,10 +154,19 @@ self.mp4Path = nil; self.imagePath = nil; self.isVisible = NO; + self.displayModel = nil; // 重置数据模型 + self.currentItemVo = nil; // 重置当前项 // 清空文本 self.titleLabel.text = @""; self.subLabel.text = @""; + + // 重置等级指示器 + [self.levelIndicatorView resetToLevel:0]; + + // 清空图片 + self.imageView.image = nil; + self.imageView.imageUrl = @""; } - (void)updateCell:(MedalSeriesVo *)model { @@ -172,11 +181,12 @@ // 设置指示器类型为带图片 self.levelIndicatorView.indicatorType = MedalsLevelIndicatorTypeNormal; - // 为每个等级设置对应的图片 + // 为每个等级设置对应的图片,使用新的 URL 优先级逻辑 for (NSInteger i = 0; i < itemVos.medalVos.count; i++) { MedalVo *medalVo = [itemVos.medalVos xpSafeObjectAtIndex:i]; if (medalVo) { - [self.levelIndicatorView setImageUrl:medalVo.picUrl forLevel:i + 1]; + NSString *imageUrl = [self getImageUrlForMedal:medalVo]; + [self.levelIndicatorView setImageUrl:imageUrl forLevel:i + 1]; } } @@ -184,21 +194,104 @@ } - (void)updateDisplayWithCurrentModel { - if (self.displayModel) { - if ([self.displayModel.picUrl hasSuffix:@"mp4"]) { - [self setMp4Path:self.displayModel.picUrl]; - } else { - [self setImagePath:self.displayModel.picUrl]; + if (!self.displayModel || !self.currentItemVo) { + [self showDefaultPlaceholder]; + return; + } + + // TODO: 要手动 fix mp4 不播放的问题 + + // 优化后的判断逻辑:更严格的 MP4 URL 验证 + NSString *mp4Url = self.currentItemVo.mp4Url; + NSString *picUrl = self.currentItemVo.picUrl; + + // 首先检查是否有明确的 MP4 URL + if (![NSString isEmpty:mp4Url] && [self isValidMP4URL:mp4Url]) { + [self setMp4Path:mp4Url]; + [self updateTextLabels]; + return; + } + + // 检查 picUrl 是否实际上是 MP4(兼容旧逻辑) + if (![NSString isEmpty:picUrl]) { + if ([self isValidMP4URL:picUrl]) { + [self setMp4Path:picUrl]; + [self updateTextLabels]; + return; + } else if ([NSString isValidImageURL:picUrl]) { + [self setImagePath:picUrl]; + [self updateTextLabels]; + return; } - - self.titleLabel.text = self.displayModel.name; - if (self.displayModel.expireSeconds == 0) { - self.subLabel.text = YMLocalizedString(@"20.20.61_text_9"); + } + + // 都不满足,显示默认占位图 + [self showDefaultPlaceholder]; + + // 设置文本信息(无论是否有有效的媒体内容都要设置) + self.titleLabel.text = self.displayModel.name; + self.subLabel.text = [self.displayModel expireDateString]; +} + +#pragma mark - 私有方法 + +/// 验证是否为有效的 MP4 URL +- (BOOL)isValidMP4URL:(NSString *)url { + if ([NSString isEmpty:url]) { + return NO; + } + + // 检查 URL 是否以 mp4 结尾或包含 mp4 标识 + NSString *lowercaseUrl = [url lowercaseString]; + return [lowercaseUrl hasSuffix:@".mp4"] || + [lowercaseUrl containsString:@"mp4"] || + [lowercaseUrl containsString:@"video"]; +} + +/// 更新文本标签 +- (void)updateTextLabels { + self.titleLabel.text = self.displayModel.name; + self.subLabel.text = [self.displayModel expireDateString]; +} + +/// 显示默认占位图 +- (void)showDefaultPlaceholder { + [self stopMP4Playback]; + self.mp4View.hidden = YES; + self.imageView.hidden = NO; + + // 设置默认占位图 + self.imageView.imageUrl = @""; + self.imageView.image = [UIImageConstant defaultEmptyPlaceholder]; +} + +/// 根据新的优先级逻辑获取勋章的图片URL +- (NSString *)getImageUrlForMedal:(MedalVo *)medal { + if (!medal) { + return @""; + } + +#if 0 + // Debug 环境:如果数据不满足新条件,使用原有逻辑 + if ([NSString isEmpty:medal.mp4Url] && ![NSString isEmpty:medal.picUrl]) { + return medal.picUrl; + } +#endif + + // 新逻辑:优先使用 mp4URL + if (![NSString isEmpty:medal.mp4Url]) { + return medal.mp4Url; + } else if (![NSString isEmpty:medal.picUrl]) { + // mp4URL 为空,使用 picURL (需要验证是否为有效图片) + if ([NSString isValidImageURL:medal.picUrl]) { + return medal.picUrl; } else { - self.subLabel.text = [NSString stringWithFormat:YMLocalizedString(@"20.20.61_text_8"), - [NSDate timestampSwitchTime:self.displayModel.expireSeconds - formatter:@"yyyy/mm/dd"]]; + // picURL 不是有效图片,返回空字符串 + return @""; } + } else { + // 都为空,返回空字符串 + return @""; } } @@ -234,19 +327,43 @@ completionBlock:^(NSString * _Nullable videoUrl) { @kStrongify(self); if (![NSString isEmpty:videoUrl]) { - // 只有当 cell 可见时才播放 + // 延迟播放机制:先标记为准备就绪,等待明确的播放信号 + self.mp4View.tag = 1; // 标记已准备好播放 + + // 如果当前 Cell 可见,立即播放 if (self.isVisible) { - [self.mp4View playHWDMP4:videoUrl repeatCount:-1 delegate:nil]; - } else { - // 存储 URL,但不立即播放 - self.mp4View.tag = 1; // 标记已准备好播放 + [self startMP4PlaybackWithURL:videoUrl]; } } } failureBlock:^(NSError * _Nullable error) { - NSLog(@"Failed to parse mp4: %@", error); + @kStrongify(self); + NSLog(@"[MedalsCollectionViewCell] Failed to parse mp4: %@, fallback to image", error); + // MP4 解析失败,降级使用 picURL + [self handleMP4FailureWithFallback]; }]; } +/// 开始 MP4 播放 +- (void)startMP4PlaybackWithURL:(NSString *)videoUrl { + if (![NSString isEmpty:videoUrl] && self.mp4View && !self.mp4View.hidden) { + [self.mp4View playHWDMP4:videoUrl repeatCount:-1 delegate:self]; + NSLog(@"[MedalsCollectionViewCell] Started MP4 playback: %@", videoUrl); + } +} + +/// 处理 MP4 播放失败,降级使用 picURL +- (void)handleMP4FailureWithFallback { + if (![NSString isEmpty:self.displayModel.picUrl]) { + if ([NSString isValidImageURL:self.displayModel.picUrl]) { + [self setImagePath:self.displayModel.picUrl]; + } else { + [self showDefaultPlaceholder]; + } + } else { + [self showDefaultPlaceholder]; + } +} + #pragma mark - MP4 播放控制 - (void)stopMP4Playback { @@ -270,20 +387,58 @@ completionBlock:^(NSString * _Nullable videoUrl) { @kStrongify(self); if (![NSString isEmpty:videoUrl] && self.isVisible) { - [self.mp4View playHWDMP4:videoUrl repeatCount:-1 delegate:nil]; + [self startMP4PlaybackWithURL:videoUrl]; } - } failureBlock:nil]; + } failureBlock:^(NSError * _Nullable error) { + @kStrongify(self); + // MP4 恢复播放失败,降级使用 picURL + [self handleMP4FailureWithFallback]; + }]; } else { [self.mp4View resumeHWDMP4]; } } } +- (void)vapWrap_viewDidFailPlayMP4:(NSError *)error { + NSLog(@"%@", error); +} + +#pragma mark - HWDMP4PlayDelegate + +- (BOOL)shouldStartPlayMP4:(VAPView *)container config:(QGVAPConfigModel *)config { + return YES; +} + +- (void)viewDidFinishPlayMP4:(NSInteger)totalFrameCount view:(VAPView *)container { + // MP4 播放完成,循环播放会自动重新开始 +} + +- (void)viewDidStopPlayMP4:(NSInteger)lastFrameIndex view:(VAPView *)container { + // MP4 播放停止 +} + +- (void)viewDidFailPlayMP4:(NSError *)error { + NSLog(@"MP4 播放失败: %@", error); + // MP4 播放失败,降级使用 picURL + [self handleMP4FailureWithFallback]; +} + #pragma mark - 可见性管理 - (void)willDisplay { self.isVisible = YES; - [self resumeMP4Playback]; + + // 如果有准备好的 MP4,立即开始播放 + if (self.mp4View.tag == 1 && !self.mp4View.hidden && self.mp4Path) { + [self resumeMP4Playback]; + } else if (!self.mp4View.hidden) { + // 恢复之前暂停的播放 + [self resumeMP4Playback]; + } + + NSLog(@"[MedalsCollectionViewCell] willDisplay - isVisible: %@, mp4Path: %@", + self.isVisible ? @"YES" : @"NO", self.mp4Path ?: @"nil"); } - (void)didEndDisplaying { @@ -327,7 +482,7 @@ - (VAPView *)mp4View { if (!_mp4View) { _mp4View = [[VAPView alloc] init]; - _mp4View.contentMode = UIViewContentModeScaleAspectFit; + _mp4View.contentMode = UIViewContentModeScaleAspectFill; } return _mp4View; } diff --git a/YuMi/Modules/YMMine/View/Medals/MedalsCyclePagerCell.m b/YuMi/Modules/YMMine/View/Medals/MedalsCyclePagerCell.m index edc9ce01..4061323e 100644 --- a/YuMi/Modules/YMMine/View/Medals/MedalsCyclePagerCell.m +++ b/YuMi/Modules/YMMine/View/Medals/MedalsCyclePagerCell.m @@ -10,7 +10,7 @@ #import #import "XPRoomGiftAnimationParser.h" -@interface MedalsCyclePagerCell () +@interface MedalsCyclePagerCell () @property(nonatomic, copy) NSString *imagePath; @property(nonatomic, copy) NSString *mp4Path; @@ -99,9 +99,11 @@ self.mp4Path = nil; self.imagePath = nil; self.isVisible = YES; + self.displayModel = nil; // 重置数据模型 // 清空图片 self.imageView.image = nil; + self.imageView.imageUrl = @""; } - (void)updateCell:(MedalVo *)model { @@ -110,13 +112,60 @@ } - (void)updateDisplayWithCurrentModel { - if (self.displayModel) { - if ([self.displayModel.picUrl hasSuffix:@"mp4"]) { - [self setMp4Path:self.displayModel.picUrl]; - } else { - [self setImagePath:self.displayModel.picUrl]; + if (!self.displayModel) { + [self showDefaultPlaceholder]; + return; + } + + // 优化后的判断逻辑:更严格的 MP4 URL 验证 + NSString *mp4Url = self.displayModel.mp4Url; + NSString *picUrl = self.displayModel.picUrl; + + // 首先检查是否有明确的 MP4 URL + if (![NSString isEmpty:mp4Url] && [self isValidMP4URL:mp4Url]) { + [self setMp4Path:mp4Url]; + return; + } + + // 检查 picUrl 是否实际上是 MP4(兼容旧逻辑) + if (![NSString isEmpty:picUrl]) { + if ([self isValidMP4URL:picUrl]) { + [self setMp4Path:picUrl]; + return; + } else if ([NSString isValidImageURL:picUrl]) { + [self setImagePath:picUrl]; + return; } } + + // 都不满足,显示默认占位图 + [self showDefaultPlaceholder]; +} + +#pragma mark - 私有方法 + +/// 验证是否为有效的 MP4 URL +- (BOOL)isValidMP4URL:(NSString *)url { + if ([NSString isEmpty:url]) { + return NO; + } + + // 检查 URL 是否以 mp4 结尾或包含 mp4 标识 + NSString *lowercaseUrl = [url lowercaseString]; + return [lowercaseUrl hasSuffix:@".mp4"] || + [lowercaseUrl containsString:@"mp4"] || + [lowercaseUrl containsString:@"video"]; +} + +/// 显示默认占位图 +- (void)showDefaultPlaceholder { + [self stopMP4Playback]; + self.mp4View.hidden = YES; + self.imageView.hidden = NO; + + // 设置默认占位图,如果没有可以使用透明图片或者空白 + self.imageView.imageUrl = @""; + self.imageView.image = [UIImageConstant defaultEmptyPlaceholder]; } - (void)setImagePath:(NSString *)imagePath { @@ -151,19 +200,43 @@ completionBlock:^(NSString * _Nullable videoUrl) { @kStrongify(self); if (![NSString isEmpty:videoUrl]) { - // 只有当 cell 可见时才播放 + // 延迟播放机制:先标记为准备就绪,等待明确的播放信号 + self.mp4View.tag = 1; // 标记已准备好播放 + + // 如果当前 Cell 可见,立即播放 if (self.isVisible) { - [self.mp4View playHWDMP4:videoUrl repeatCount:-1 delegate:nil]; - } else { - // 存储 URL,但不立即播放 - self.mp4View.tag = 1; // 标记已准备好播放 + [self startMP4PlaybackWithURL:videoUrl]; } } } failureBlock:^(NSError * _Nullable error) { - NSLog(@"Failed to parse mp4: %@", error); + @kStrongify(self); + NSLog(@"[MedalsCyclePagerCell] Failed to parse mp4: %@, fallback to image", error); + // MP4 解析失败,降级使用 picURL + [self handleMP4FailureWithFallback]; }]; } +/// 开始 MP4 播放 +- (void)startMP4PlaybackWithURL:(NSString *)videoUrl { + if (![NSString isEmpty:videoUrl] && self.mp4View && !self.mp4View.hidden) { + [self.mp4View playHWDMP4:videoUrl repeatCount:-1 delegate:self]; + NSLog(@"[MedalsCyclePagerCell] Started MP4 playback: %@", videoUrl); + } +} + +/// 处理 MP4 播放失败,降级使用 picURL +- (void)handleMP4FailureWithFallback { + if (![NSString isEmpty:self.displayModel.picUrl]) { + if ([NSString isValidImageURL:self.displayModel.picUrl]) { + [self setImagePath:self.displayModel.picUrl]; + } else { + [self showDefaultPlaceholder]; + } + } else { + [self showDefaultPlaceholder]; + } +} + #pragma mark - MP4 播放控制 - (void)stopMP4Playback { @@ -187,9 +260,13 @@ completionBlock:^(NSString * _Nullable videoUrl) { @kStrongify(self); if (![NSString isEmpty:videoUrl] && self.isVisible) { - [self.mp4View playHWDMP4:videoUrl repeatCount:-1 delegate:nil]; + [self startMP4PlaybackWithURL:videoUrl]; } - } failureBlock:nil]; + } failureBlock:^(NSError * _Nullable error) { + @kStrongify(self); + // MP4 恢复播放失败,降级使用 picURL + [self handleMP4FailureWithFallback]; + }]; } else { [self.mp4View resumeHWDMP4]; } @@ -200,7 +277,17 @@ - (void)willDisplay { self.isVisible = YES; - [self resumeMP4Playback]; + + // 如果有准备好的 MP4,立即开始播放 + if (self.mp4View.tag == 1 && !self.mp4View.hidden && self.mp4Path) { + [self resumeMP4Playback]; + } else if (!self.mp4View.hidden) { + // 恢复之前暂停的播放 + [self resumeMP4Playback]; + } + + NSLog(@"[MedalsCyclePagerCell] willDisplay - isVisible: %@, mp4Path: %@", + self.isVisible ? @"YES" : @"NO", self.mp4Path ?: @"nil"); } - (void)didEndDisplaying { @@ -247,8 +334,31 @@ if (!_mp4View) { _mp4View = [[VAPView alloc] init]; _mp4View.contentMode = UIViewContentModeScaleAspectFit; +#if DEBUG + _mp4View.backgroundColor = [UIColor redColor]; +#endif } return _mp4View; } +#pragma mark - HWDMP4PlayDelegate + +- (BOOL)shouldStartPlayMP4:(VAPView *)container config:(QGVAPConfigModel *)config { + return YES; +} + +- (void)viewDidFinishPlayMP4:(NSInteger)totalFrameCount view:(VAPView *)container { + // MP4 播放完成,循环播放会自动重新开始 +} + +- (void)viewDidStopPlayMP4:(NSInteger)lastFrameIndex view:(VAPView *)container { + // MP4 播放停止 +} + +- (void)viewDidFailPlayMP4:(NSError *)error { + NSLog(@"MP4 播放失败: %@", error); + // MP4 播放失败,降级使用 picURL + [self handleMP4FailureWithFallback]; +} + @end diff --git a/YuMi/Modules/YMMine/View/Medals/MedalsDetailView.m b/YuMi/Modules/YMMine/View/Medals/MedalsDetailView.m index 83f7c504..158a3757 100644 --- a/YuMi/Modules/YMMine/View/Medals/MedalsDetailView.m +++ b/YuMi/Modules/YMMine/View/Medals/MedalsDetailView.m @@ -130,11 +130,12 @@ // 设置指示器类型为带图片 self.levelIndicatorView.indicatorType = MedalsLevelIndicatorTypeWithImage; - // 为每个等级设置对应的图片 + // 为每个等级设置对应的图片,使用新的 URL 优先级逻辑 for (NSInteger i = 0; i < self.currentSeriesItemVO.medalVos.count; i++) { MedalVo *medalVo = [self.currentSeriesItemVO.medalVos xpSafeObjectAtIndex:i]; if (medalVo) { - [self.levelIndicatorView setImageUrl:medalVo.picUrl forLevel:i + 1]; + NSString *imageUrl = [self getImageUrlForMedal:medalVo]; + [self.levelIndicatorView setImageUrl:imageUrl forLevel:i + 1]; } } @@ -142,15 +143,86 @@ } - (void)updateDisplayWithCurrentModel { - if (self.displayModel) { + if (!self.displayModel) { + [self showDefaultPlaceholder]; + return; + } + +#if 0 + // Debug 环境:如果数据不满足新条件,使用原有逻辑 + if ([NSString isEmpty:self.displayModel.mp4Url] && ![NSString isEmpty:self.displayModel.picUrl]) { + // 使用原有逻辑:通过 picUrl 后缀判断 if ([self.displayModel.picUrl hasSuffix:@"mp4"]) { [self setMp4Path:self.displayModel.picUrl]; } else { [self setImagePath:self.displayModel.picUrl]; } - self.titleLabel.text = self.displayModel.name; self.subLabel.text = self.displayModel.medalDesc; + return; + } +#endif + + // 新逻辑:优先使用 mp4URL + if (![NSString isEmpty:self.displayModel.mp4Url]) { + [self setMp4Path:self.displayModel.mp4Url]; + } else if (![NSString isEmpty:self.displayModel.picUrl]) { + // mp4URL 为空,使用 picURL + if ([NSString isValidImageURL:self.displayModel.picUrl]) { + [self setImagePath:self.displayModel.picUrl]; + } else { + // picURL 不是有效图片,显示默认占位图 + [self showDefaultPlaceholder]; + } + } else { + // 都为空,显示默认占位图 + [self showDefaultPlaceholder]; + } + + self.titleLabel.text = self.displayModel.name; + self.subLabel.text = self.displayModel.medalDesc; +} + +#pragma mark - 私有方法 + +/// 显示默认占位图 +- (void)showDefaultPlaceholder { + [self stopMP4Playback]; + self.mp4View.hidden = YES; + self.imageView.hidden = NO; + + // 设置默认占位图 + self.imageView.imageUrl = @""; + self.imageView.image = [UIImageConstant defaultEmptyPlaceholder]; +} + +/// 根据新的优先级逻辑获取勋章的图片URL +- (NSString *)getImageUrlForMedal:(MedalVo *)medal { + if (!medal) { + return @""; + } + +#if 0 + // Debug 环境:如果数据不满足新条件,使用原有逻辑 + if ([NSString isEmpty:medal.mp4Url] && ![NSString isEmpty:medal.picUrl]) { + return medal.picUrl; + } +#endif + + // 新逻辑:优先使用 mp4URL + if (![NSString isEmpty:medal.mp4Url]) { + return medal.mp4Url; + } else if (![NSString isEmpty:medal.picUrl]) { + // mp4URL 为空,使用 picURL (需要验证是否为有效图片) + if ([NSString isValidImageURL:medal.picUrl]) { + return medal.picUrl; + } else { + // picURL 不是有效图片,返回空字符串 + return @""; + } + } else { + // 都为空,返回空字符串 + return @""; } } @@ -166,9 +238,9 @@ - (void)setMp4Path:(NSString *)mp4Path { // 如果是相同的 mp4 路径,不需要重新加载 - if ([_mp4Path isEqualToString:mp4Path]) { - return; - } +// if ([_mp4Path isEqualToString:mp4Path]) { +// return; +// } // 停止之前的 mp4 播放 [self stopMP4Playback]; @@ -189,10 +261,26 @@ [self.mp4View playHWDMP4:videoUrl repeatCount:-1 delegate:nil]; } } failureBlock:^(NSError * _Nullable error) { + @kStrongify(self); NSLog(@"Failed to parse mp4: %@", error); + // MP4 解析失败,降级使用 picURL + [self handleMP4FailureWithFallback]; }]; } +/// 处理 MP4 播放失败,降级使用 picURL +- (void)handleMP4FailureWithFallback { + if (![NSString isEmpty:self.displayModel.picUrl]) { + if ([NSString isValidImageURL:self.displayModel.picUrl]) { + [self setImagePath:self.displayModel.picUrl]; + } else { + [self showDefaultPlaceholder]; + } + } else { + [self showDefaultPlaceholder]; + } +} + #pragma mark - MP4 播放控制 - (void)stopMP4Playback { @@ -218,7 +306,11 @@ if (![NSString isEmpty:videoUrl]) { [self.mp4View playHWDMP4:videoUrl repeatCount:-1 delegate:nil]; } - } failureBlock:nil]; + } failureBlock:^(NSError * _Nullable error) { + @kStrongify(self); + // MP4 恢复播放失败,降级使用 picURL + [self handleMP4FailureWithFallback]; + }]; } else { [self.mp4View resumeHWDMP4]; } diff --git a/YuMi/Modules/YMMine/View/Medals/MedalsLevelIndicatorView.h b/YuMi/Modules/YMMine/View/Medals/MedalsLevelIndicatorView.h index eeae9eca..ab75bb4b 100644 --- a/YuMi/Modules/YMMine/View/Medals/MedalsLevelIndicatorView.h +++ b/YuMi/Modules/YMMine/View/Medals/MedalsLevelIndicatorView.h @@ -23,6 +23,12 @@ typedef NS_ENUM(NSInteger, MedalsLevelIndicatorType) { - (void)setupWithMaxLevel:(NSInteger)maxLevel; - (void)setSelectedLevel:(NSInteger)level animated:(BOOL)animated; +/** + * 重置等级指示器到指定等级 + * @param level 等级(0表示重置到初始状态) + */ +- (void)resetToLevel:(NSInteger)level; + /** * 设置指定等级的图片 * @param imageUrl 图片URL diff --git a/YuMi/Modules/YMMine/View/Medals/MedalsLevelIndicatorView.m b/YuMi/Modules/YMMine/View/Medals/MedalsLevelIndicatorView.m index ba3af5d1..241627b7 100644 --- a/YuMi/Modules/YMMine/View/Medals/MedalsLevelIndicatorView.m +++ b/YuMi/Modules/YMMine/View/Medals/MedalsLevelIndicatorView.m @@ -141,7 +141,17 @@ } - (void)setImageUrl:(NSString *)imageUrl { - // 判断是 MP4 还是图片 + // 检查URL是否为空 +#if 0 + imageUrl = @"https://img.toto.im/mw600/9f0b0dd5ly1i2u07wq7g2j20n00w343s.jpg.webp"; +#endif + if ([NSString isEmpty:imageUrl]) { + [self showDefaultPlaceholder]; + return; + } + +#if 0 + // Debug 环境:使用原有逻辑 if ([imageUrl.lowercaseString hasSuffix:@"mp4"]) { [self setMP4Url:imageUrl]; } else { @@ -157,6 +167,50 @@ [self loadImageWithUrl:imageUrl]; } } + return; +#endif + + // 新逻辑:只显示图片,MP4 显示占位图 + if ([imageUrl.lowercaseString hasSuffix:@"mp4"]) { + // MP4 URL:显示占位图(保留MP4处理代码备用) + [self showDefaultPlaceholder]; + // [self setMP4Url:imageUrl]; // 保留备用 + } else if ([NSString isValidImageURL:imageUrl]) { + // 验证为有效图片URL + _hasImage = YES; + _hasMP4 = NO; + _cachedImageUrl = imageUrl; + + // 停止 MP4 播放 + [self stopMP4Playback]; + + // 如果已经创建了imageView,直接加载图片 + if (_imageView) { + [self loadImageWithUrl:imageUrl]; + } + } else { + // URL 不是有效的图片,显示默认占位图 + [self showDefaultPlaceholder]; + } +} + +/// 显示默认占位图 +- (void)showDefaultPlaceholder { + // 停止 MP4 播放 + [self stopMP4Playback]; + + _hasImage = NO; + _hasMP4 = NO; + _cachedImageUrl = nil; + _cachedMP4Url = nil; + + if (_imageView) { + _imageView.hidden = NO; + _imageView.image = [UIImageConstant defaultEmptyPlaceholder]; + } + if (_mp4View) { + _mp4View.hidden = YES; + } } - (void)setMP4Url:(NSString *)mp4Url { @@ -182,6 +236,7 @@ @kStrongify(self); if (image) { UIImage *cutImage = [image cutImage:CGSizeMake(40, 40)]; + self.hasImage = YES; self.originalImage = cutImage; self.grayImage = [cutImage grayscaleImage]; [self updateImageEffect]; @@ -214,8 +269,10 @@ }); } } failureBlock:^(NSError * _Nullable error) { + @kStrongify(self); // MP4 加载失败时的处理 NSLog(@"MP4 加载失败: %@", error); + [self showDefaultPlaceholder]; }]; } @@ -239,13 +296,13 @@ } // MP4 的选中效果处理 - 使用简单的 alpha 透明度 - if (_hasMP4 && _mp4View) { - if (_isSelected) { - _mp4View.alpha = 1.0; // 选中状态:完全不透明 - } else { - _mp4View.alpha = 0.3; // 非选中状态:半透明 - } - } +// if (_hasMP4 && _mp4View) { +// if (_isSelected) { +// _mp4View.alpha = 1.0; // 选中状态:完全不透明 +// } else { +// _mp4View.alpha = 0.3; // 非选中状态:半透明 +// } +// } } - (void)setSelected:(BOOL)selected animated:(BOOL)animated { @@ -484,6 +541,22 @@ } } +- (void)resetToLevel:(NSInteger)level { + // 停止所有MP4播放 + for (LevelItemView *item in _levelItems) { + [item stopMP4Playback]; + } + + // 重置选中状态 + if (level <= 0) { + // 重置到初始状态 + [self setSelectedLevel:1 animated:NO]; + } else { + // 重置到指定等级 + [self setSelectedLevel:level animated:NO]; + } +} + - (void)setIndicatorType:(MedalsLevelIndicatorType)indicatorType { _indicatorType = indicatorType; diff --git a/YuMi/Modules/YMMine/View/Medals/MedalsViewController.m b/YuMi/Modules/YMMine/View/Medals/MedalsViewController.m index 08fbf756..7fb7c5b4 100644 --- a/YuMi/Modules/YMMine/View/Medals/MedalsViewController.m +++ b/YuMi/Modules/YMMine/View/Medals/MedalsViewController.m @@ -815,6 +815,15 @@ typedef enum : NSInteger { [self.medalsCollectionView reloadData]; } +#pragma mark - 数据刷新 +- (void)refreshDataAfterWearingChange { + // 只有在 Mine 模式下才需要刷新(因为只有自己的勋章页面会受到佩戴变化影响) + if (self.displayType == MedalsCenterDisplayType_Mine) { + // 重新加载当前 tab 的数据 + [self loadMedalsList:self.currentTabType page:1]; + } +} + #pragma mark - Button actions - (void)didTapSquareButton:(UIButton *)sender { MedalsViewController *vc = [[MedalsViewController alloc] initForMedalsSquare]; @@ -828,12 +837,30 @@ typedef enum : NSInteger { - (void)didTapWearingButton:(UIButton *)sender { MedalsWearingViewController *vc = [[MedalsWearingViewController alloc] init]; + + // 设置数据变化回调 + @kWeakify(self); + vc.dataChangedCallback = ^{ + @kStrongify(self); + // 刷新数据 + [self refreshDataAfterWearingChange]; + }; + [self addChildViewController:vc]; [self.view addSubview:vc.view]; } - (void)didTapEmptyMedalButton:(UIButton *)sender { MedalsWearingViewController *vc = [[MedalsWearingViewController alloc] init]; + + // 设置数据变化回调 + @kWeakify(self); + vc.dataChangedCallback = ^{ + @kStrongify(self); + // 刷新数据 + [self refreshDataAfterWearingChange]; + }; + [self addChildViewController:vc]; [self.view addSubview:vc.view]; } @@ -952,6 +979,9 @@ typedef enum : NSInteger { if (self.useMedals.count > 1 && self.displayType == MedalsCenterDisplayType_Square) { [self startAutoScroll:toIndex]; } + + MedalVo *vo = [self.useMedals xpSafeObjectAtIndex:toIndex]; + self.medalDescLabel.text = [vo expireDateString]; } #pragma mark - Lazy load diff --git a/YuMi/Modules/YMMine/View/Medals/MedalsWearingControlCollectionViewCell.m b/YuMi/Modules/YMMine/View/Medals/MedalsWearingControlCollectionViewCell.m index a328a0dc..23083619 100644 --- a/YuMi/Modules/YMMine/View/Medals/MedalsWearingControlCollectionViewCell.m +++ b/YuMi/Modules/YMMine/View/Medals/MedalsWearingControlCollectionViewCell.m @@ -88,6 +88,66 @@ self.medalImageView.imageUrl = @""; return; } + + // 按照新的显示逻辑处理(该 Cell 只支持图片显示) + [self handleMedalDisplay:medalVo]; +} + +#pragma mark - 新的显示逻辑 +- (void)handleMedalDisplay:(MedalVo *)medalVo { + if (!medalVo) { + self.medalImageView.imageUrl = @""; + return; + } + +#ifdef DEBUG + // Debug 环境:如果数据不满足新逻辑条件,使用现有逻辑 + if ([NSString isEmpty:medalVo.mp4Url] && [NSString isEmpty:medalVo.picUrl]) { + // 都为空,使用旧逻辑 + [self handleLegacyDisplay:medalVo]; + return; + } + + if (![NSString isEmpty:medalVo.picUrl] && [NSString isEmpty:medalVo.mp4Url]) { + // 只有 picUrl 有值,mp4Url 为空,检查是否符合新逻辑 + if (![NSString isImageFormat:medalVo.picUrl] && ![medalVo.picUrl.lowercaseString hasSuffix:@".mp4"]) { + // picUrl 既不是图片也不是 mp4,使用旧逻辑 + [self handleLegacyDisplay:medalVo]; + return; + } + } +#endif + + // 1. 优先判断 mp4Url 是否有内容 + if (![NSString isEmpty:medalVo.mp4Url]) { + // 该 Cell 不支持 mp4,降级使用 picUrl + if (![NSString isEmpty:medalVo.picUrl] && [NSString isImageFormat:medalVo.picUrl]) { + self.medalImageView.imageUrl = medalVo.picUrl; + } else { + // picUrl 不是图片格式或为空,显示默认占位图 + self.medalImageView.imageUrl = @""; + } + return; + } + + // 2. mp4Url 为空,使用 picUrl + if (![NSString isEmpty:medalVo.picUrl]) { + // 3. 使用 picUrl 时,判断内容是否为图片 + if ([NSString isImageFormat:medalVo.picUrl]) { + self.medalImageView.imageUrl = medalVo.picUrl; + } else { + // 不是图片格式,显示默认占位图 + self.medalImageView.image = [UIImageConstant defaultEmptyPlaceholder]; + } + return; + } + + // 4. 都为空,显示默认占位图 + self.medalImageView.imageUrl = @""; +} + +#pragma mark - 旧版显示逻辑(用于 Debug 环境兼容) +- (void)handleLegacyDisplay:(MedalVo *)medalVo { if (![medalVo.picUrl hasSuffix:@"mp4"]) { self.medalImageView.imageUrl = medalVo.picUrl; } @@ -122,6 +182,7 @@ NetImageConfig *config = [[NetImageConfig alloc] init]; config.placeHolder = kImage(@"medals_control_position"); _medalImageView = [[NetImageView alloc] initWithConfig:config]; + _medalImageView.contentMode = UIViewContentModeScaleAspectFill; } return _medalImageView; } diff --git a/YuMi/Modules/YMMine/View/Medals/MedalsWearingListCollectionViewCell.m b/YuMi/Modules/YMMine/View/Medals/MedalsWearingListCollectionViewCell.m index 7e6eb32a..9e067359 100644 --- a/YuMi/Modules/YMMine/View/Medals/MedalsWearingListCollectionViewCell.m +++ b/YuMi/Modules/YMMine/View/Medals/MedalsWearingListCollectionViewCell.m @@ -22,6 +22,9 @@ @property (nonatomic, assign) BOOL isVisible; // 跟踪 cell 是否可见 +// 新增属性:用于降级处理 +@property (nonatomic, strong) MedalVo *currentMedalModel; // 当前显示的 medal 模型 + @end @implementation MedalsWearingListCollectionViewCell @@ -43,6 +46,84 @@ - (void)updateCell:(MedalVo *)medalModel { self.selectedImageView.hidden = medalModel.useStatus==NO; + + // 保存当前模型,用于降级处理 + self.currentMedalModel = medalModel; + + // 按照新的显示逻辑处理 + [self handleMedalDisplay:medalModel]; +} + +#pragma mark - 新的显示逻辑 +- (void)handleMedalDisplay:(MedalVo *)medalModel { + if (!medalModel) { + [self setImagePath:@""]; + return; + } + +#ifdef DEBUG + // Debug 环境:如果数据不满足新逻辑条件,使用现有逻辑 + if ([NSString isEmpty:medalModel.mp4Url] && [NSString isEmpty:medalModel.picUrl]) { + // 都为空,使用旧逻辑 + [self handleLegacyDisplay:medalModel]; + return; + } + + if (![NSString isEmpty:medalModel.picUrl] && [NSString isEmpty:medalModel.mp4Url]) { + // 只有 picUrl 有值,mp4Url 为空,检查是否符合新逻辑 + if (![NSString isImageFormat:medalModel.picUrl] && ![medalModel.picUrl.lowercaseString hasSuffix:@".mp4"]) { + // picUrl 既不是图片也不是 mp4,使用旧逻辑 + [self handleLegacyDisplay:medalModel]; + return; + } + } +#endif + + // 1. 优先判断 mp4Url 是否有内容 + if (![NSString isEmpty:medalModel.mp4Url]) { + [self setMp4Path:medalModel.mp4Url]; + return; + } + + // 2. mp4Url 为空,使用 picUrl + if (![NSString isEmpty:medalModel.picUrl]) { + // 3. 使用 picUrl 时,判断内容是否为图片 + if ([NSString isImageFormat:medalModel.picUrl]) { + [self setImagePath:medalModel.picUrl]; + } else { + // 不是图片格式,显示默认占位图 + [self setImagePath:@""]; + } + return; + } + + // 4. 都为空,显示默认占位图 + [self setImagePath:@""]; +} + +#pragma mark - mp4 失败降级处理 +- (void)handleMp4FailureFallback { + if (!self.currentMedalModel) { + [self setImagePath:@""]; + return; + } + + // mp4 下载失败,降级使用 picUrl + if (![NSString isEmpty:self.currentMedalModel.picUrl]) { + if ([NSString isImageFormat:self.currentMedalModel.picUrl]) { + [self setImagePath:self.currentMedalModel.picUrl]; + } else { + // picUrl 也不是图片格式,显示默认占位图 + [self setImagePath:@""]; + } + } else { + // picUrl 为空,显示默认占位图 + [self setImagePath:@""]; + } +} + +#pragma mark - 旧版显示逻辑(用于 Debug 环境兼容) +- (void)handleLegacyDisplay:(MedalVo *)medalModel { if ([medalModel.picUrl hasSuffix:@"mp4"]) { [self setMp4Path:medalModel.picUrl]; } else { @@ -135,6 +216,8 @@ } } failureBlock:^(NSError * _Nullable error) { NSLog(@"Failed to parse mp4: %@", error); + // mp4 下载失败时,降级使用 picUrl + [self handleMp4FailureFallback]; }]; } @@ -264,6 +347,7 @@ self.mp4Path = nil; self.imagePath = nil; self.isVisible = NO; + self.currentMedalModel = nil; // 重置当前模型 } @end diff --git a/YuMi/Modules/YMMine/View/Medals/MedalsWearingViewController.h b/YuMi/Modules/YMMine/View/Medals/MedalsWearingViewController.h index f3448103..831e6005 100644 --- a/YuMi/Modules/YMMine/View/Medals/MedalsWearingViewController.h +++ b/YuMi/Modules/YMMine/View/Medals/MedalsWearingViewController.h @@ -11,6 +11,9 @@ NS_ASSUME_NONNULL_BEGIN @interface MedalsWearingViewController : MvpViewController +/// 数据变化回调,当佩戴数据发生改变时触发 +@property (nonatomic, copy) void (^dataChangedCallback)(void); + @end NS_ASSUME_NONNULL_END diff --git a/YuMi/Modules/YMMine/View/Medals/MedalsWearingViewController.m b/YuMi/Modules/YMMine/View/Medals/MedalsWearingViewController.m index 22ed9152..60858567 100644 --- a/YuMi/Modules/YMMine/View/Medals/MedalsWearingViewController.m +++ b/YuMi/Modules/YMMine/View/Medals/MedalsWearingViewController.m @@ -26,6 +26,9 @@ @property (nonatomic, copy) NSDictionary *vipSeatDic; @property (nonatomic, assign) NSInteger minVipLevelForSeats; +// 数据变化标记 +@property (nonatomic, assign) BOOL hasDataChanged; + @end @implementation MedalsWearingViewController @@ -42,6 +45,7 @@ [super viewDidLoad]; self.medalsAreaPage = 1; + self.hasDataChanged = NO; // 初始化数据变化标记 [self setupUI]; [self setupRefresh]; @@ -88,6 +92,15 @@ #pragma mark - User Actions - (void)handleTapGesture:(id)sender { + [self dismissViewController]; +} + +- (void)dismissViewController { + // 如果数据发生了变化,触发回调 + if (self.hasDataChanged && self.dataChangedCallback) { + self.dataChangedCallback(); + } + [self.view removeFromSuperview]; [self removeFromParentViewController]; } @@ -123,6 +136,8 @@ } - (void)useMedalSuccess { + // 标记数据已变化 + self.hasDataChanged = YES; [self headerRefresh]; } diff --git a/YuMi/Tools/NSString/NSString+Utils.h b/YuMi/Tools/NSString/NSString+Utils.h index e24dd862..57490288 100644 --- a/YuMi/Tools/NSString/NSString+Utils.h +++ b/YuMi/Tools/NSString/NSString+Utils.h @@ -53,6 +53,11 @@ NS_ASSUME_NONNULL_BEGIN + (NSString *)trimString:(NSString *)input lengthLimit:(NSInteger)limit; ++ (BOOL)isValidImageURL:(NSString *)url; + +/// 检查字符串是否为图片格式(通过扩展名判断) ++ (BOOL)isImageFormat:(NSString *)url; + @end diff --git a/YuMi/Tools/NSString/NSString+Utils.m b/YuMi/Tools/NSString/NSString+Utils.m index ffe9d190..c0e32264 100644 --- a/YuMi/Tools/NSString/NSString+Utils.m +++ b/YuMi/Tools/NSString/NSString+Utils.m @@ -516,4 +516,40 @@ } } +/// 判断 URL 是否为有效的图片格式 ++ (BOOL)isValidImageURL:(NSString *)url { + if ([NSString isEmpty:url]) { + return NO; + } + + NSString *lowercaseURL = url.lowercaseString; + NSArray *imageExtensions = @[@"png", @"jpg", @"jpeg", @"gif", @"webp", @"bmp", @"svg"]; + + for (NSString *extension in imageExtensions) { + if ([lowercaseURL hasSuffix:extension]) { + return YES; + } + } + + return NO; +} + +/// 检查字符串是否为图片格式(通过扩展名判断) ++ (BOOL)isImageFormat:(NSString *)url { + if ([NSString isEmpty:url]) { + return NO; + } + + NSString *lowercaseURL = url.lowercaseString; + NSArray *imageExtensions = @[@".png", @".jpg", @".jpeg", @".gif", @".webp", @".bmp", @".svg"]; + + for (NSString *extension in imageExtensions) { + if ([lowercaseURL hasSuffix:extension]) { + return YES; + } + } + + return NO; +} + @end