Commit cebed0a8 by suolong

用户进入限时表演窗口

parent b6f574a1
......@@ -1494,6 +1494,8 @@
BED659092C5B745E00668116 /* FUSLiveChatInputToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BED656062C5B745D00668116 /* FUSLiveChatInputToolView.swift */; };
BED6590A2C5B745E00668116 /* FUSLiveChatInputView.h in Headers */ = {isa = PBXBuildFile; fileRef = BED656072C5B745D00668116 /* FUSLiveChatInputView.h */; };
BED6590B2C5B745E00668116 /* FUSLiveChatInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = BED656082C5B745D00668116 /* FUSLiveChatInputView.m */; };
00F7AA012FA1234500AA0001 /* FUSLiveShowTimeAudienceEntryMainView.h in Headers */ = {isa = PBXBuildFile; fileRef = 00F7AA032FA1234500AA0001 /* FUSLiveShowTimeAudienceEntryMainView.h */; };
00F7AA022FA1234500AA0001 /* FUSLiveShowTimeAudienceEntryMainView.m in Sources */ = {isa = PBXBuildFile; fileRef = 00F7AA042FA1234500AA0001 /* FUSLiveShowTimeAudienceEntryMainView.m */; };
BED6590C2C5B745E00668116 /* FUSLiveChatToolGiftTipView.h in Headers */ = {isa = PBXBuildFile; fileRef = BED656092C5B745D00668116 /* FUSLiveChatToolGiftTipView.h */; };
BED6590D2C5B745E00668116 /* FUSLiveChatToolGiftTipView.m in Sources */ = {isa = PBXBuildFile; fileRef = BED6560A2C5B745D00668116 /* FUSLiveChatToolGiftTipView.m */; };
BED6590E2C5B745E00668116 /* FUSLiveShareView.h in Headers */ = {isa = PBXBuildFile; fileRef = BED6560B2C5B745D00668116 /* FUSLiveShareView.h */; };
......@@ -3918,6 +3920,8 @@
BED656062C5B745D00668116 /* FUSLiveChatInputToolView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FUSLiveChatInputToolView.swift; sourceTree = "<group>"; };
BED656072C5B745D00668116 /* FUSLiveChatInputView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FUSLiveChatInputView.h; sourceTree = "<group>"; };
BED656082C5B745D00668116 /* FUSLiveChatInputView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FUSLiveChatInputView.m; sourceTree = "<group>"; };
00F7AA032FA1234500AA0001 /* FUSLiveShowTimeAudienceEntryMainView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FUSLiveShowTimeAudienceEntryMainView.h; sourceTree = "<group>"; };
00F7AA042FA1234500AA0001 /* FUSLiveShowTimeAudienceEntryMainView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FUSLiveShowTimeAudienceEntryMainView.m; sourceTree = "<group>"; };
BED656092C5B745D00668116 /* FUSLiveChatToolGiftTipView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FUSLiveChatToolGiftTipView.h; sourceTree = "<group>"; };
BED6560A2C5B745D00668116 /* FUSLiveChatToolGiftTipView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FUSLiveChatToolGiftTipView.m; sourceTree = "<group>"; };
BED6560B2C5B745D00668116 /* FUSLiveShareView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FUSLiveShareView.h; sourceTree = "<group>"; };
......@@ -6921,6 +6925,7 @@
00E6CB0A2F4D509F00B63797 /* FUSLiveChatInputToolCell.swift */,
BED656072C5B745D00668116 /* FUSLiveChatInputView.h */,
BED656082C5B745D00668116 /* FUSLiveChatInputView.m */,
00F7AA052FA1234500AA0001 /* ShowTimeAudienceEntry */,
BED656092C5B745D00668116 /* FUSLiveChatToolGiftTipView.h */,
BED6560A2C5B745D00668116 /* FUSLiveChatToolGiftTipView.m */,
BED6560B2C5B745D00668116 /* FUSLiveShareView.h */,
......@@ -6939,6 +6944,15 @@
path = ChatInputView;
sourceTree = "<group>";
};
00F7AA052FA1234500AA0001 /* ShowTimeAudienceEntry */ = {
isa = PBXGroup;
children = (
00F7AA032FA1234500AA0001 /* FUSLiveShowTimeAudienceEntryMainView.h */,
00F7AA042FA1234500AA0001 /* FUSLiveShowTimeAudienceEntryMainView.m */,
);
path = ShowTimeAudienceEntry;
sourceTree = "<group>";
};
00F7FF882FB1000100AAAA09 /* ShowTimePopups */ = {
isa = PBXGroup;
children = (
......@@ -9167,6 +9181,7 @@
BE189E252C733B460008418B /* FSRLinkmicdictActiveView.h in Headers */,
BED658A92C5B745E00668116 /* FUSVideoEndedView.h in Headers */,
BED6590A2C5B745E00668116 /* FUSLiveChatInputView.h in Headers */,
00F7AA012FA1234500AA0001 /* FUSLiveShowTimeAudienceEntryMainView.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
......@@ -10968,6 +10983,7 @@
BED6586E2C5B745E00668116 /* VELPictureInPictureController.m in Sources */,
BED65A992C5B745F00668116 /* FUSLiveManagerCell.m in Sources */,
BED6590B2C5B745E00668116 /* FUSLiveChatInputView.m in Sources */,
00F7AA022FA1234500AA0001 /* FUSLiveShowTimeAudienceEntryMainView.m in Sources */,
00E6CB272F4E9AB800B63797 /* FUSLiveReleasePunishmentView.m in Sources */,
00E6CB282F4E9AB800B63797 /* FUSLiveGlobalSpecialEffectView.m in Sources */,
00E6CB292F4E9AB800B63797 /* FUSLivePunishListCell.m in Sources */,
......@@ -32,6 +32,8 @@
#import "FUSPayRoomCompanionDataModel.h"
#import "FUSPayRoomCompanionPopView.h"
#import "FUSRechargeViewController.h"
#import "FUSLiveShowTimeAudienceEntryMainView.h"
#import "FUSTicketShowCollectTicketToggleResultModel.h"
#import <FirebaseAnalytics/FirebaseAnalytics.h>
......@@ -999,6 +1001,28 @@ static NSString *const KLiveDataCenter_store_liveRTCData = @"LiveDataCenter_stor
[FUSAlertView showAlertWithTitle:nil message:errorDict[@"msg"] cancelButtonTitle:[NSString fus_localString:@"确定"] otherButtonTitles:nil clickBlock:^(NSInteger buttonIndex) {
[FUSLiveHelper fus_quitLiveWithCompletion:nil];
}];
} else if (code == -40013) {
dispatch_async(dispatch_get_main_queue(), ^{
FUSStreamPlayStatus status = [self.liveVC.playView fus_statusForUID:currentRoomId];
if (status == FUSStreamPlayStatusPlaying
|| status == FUSStreamPlayStatusLoading
|| status == FUSStreamPlayStatusPause) {
[self.liveVC.playView fus_stopWithUID:currentRoomId];
[self.liveVC.playView fus_destroyAllPlayer];
[self.liveVC fus_stopPlay];
[self.liveVC destroyPlayPictureInPicture];
}
UIView *rootView = self.liveVC.view ?: [UIViewController fus_topViewController].view;
FUSLiveShowTimeAudienceEntryMainView *entry = [FUSLiveShowTimeAudienceEntryMainView fus_showOnView:rootView actionHandler:^(FUSLiveShowTimeAudienceEntryMainViewAction action, NSInteger selectedOptionIndex) {
if (action == FUSLiveShowTimeAudienceEntryMainViewActionDismiss) {
[FUSLiveHelper fus_quitLiveWithCompletion:nil];
}
}];
NSString *channelId = [[errorDict[@"channelId"] description] ?: @"" copy];
NSString *roundId = [[errorDict[@"roundId"] description] ?: @"" copy];
[self fus_requestTicketShowBuyGetDataAndFillEntry:entry roomId:currentRoomId channelId:channelId roundId:roundId];
});
return;
} else {
// 其他情况
[FUSAlertView showAlertWithTitle:nil message:msg cancelButtonTitle:nil otherButtonTitles:@[[NSString fus_localString:@"确定"], [NSString fus_localString:@"退出"]] clickBlock:^(NSInteger buttonIndex) {
......@@ -1016,6 +1040,28 @@ static NSString *const KLiveDataCenter_store_liveRTCData = @"LiveDataCenter_stor
}
}
/// 请求 /ticketshow/buy/getdata 并用返回模型刷新“限时表演”观众入场弹窗
- (void)fus_requestTicketShowBuyGetDataAndFillEntry:(FUSLiveShowTimeAudienceEntryMainView *)entry
roomId:(NSString *)roomId
channelId:(NSString *)channelId
roundId:(NSString *)roundId
{
__weak typeof(self) weakSelf = self;
[FUSLiveHttpHelper fus_ticketShowBuyGetDataWithRoomId:(roomId ?: @"")
channelId:([NSString isNull:channelId] ? @"" : channelId)
roundId:([NSString isNull:roundId] ? @"" : roundId)
succeed:^(FUSTicketShowCollectTicketToggleResultModel *model) {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *anchorFace = [[weakSelf.roomInfoModel.face description] length] > 0 ? [weakSelf.roomInfoModel.face description] : @"";
[entry fus_updateWithBuyGetDataModel:model anchorFacePath:anchorFace];
});
} failure:^(NSString *msg, NSInteger code) {
dispatch_async(dispatch_get_main_queue(), ^{
[FUSDialogView fus_showDialog:(msg ?: @"")];
});
}];
}
/**
付费房磨砂弹层(-20065)处理:拉取磨砂数据并展示付费入口视图
*/
......
......@@ -20,8 +20,8 @@
@property (nonatomic, strong) UIImageView *ticketImageView;
/// 票数文本(示例:x1 / x10)
@property (nonatomic, strong) UILabel *countLabel;
/// 价格前置小圆点(视觉分隔
@property (nonatomic, strong) UIView *priceDotView;
/// 价格前置小图标(home_list_pay
@property (nonatomic, strong) UIImageView *priceDotView;
/// 价格文本(示例:100)
@property (nonatomic, strong) UILabel *priceLabel;
/// 价格区域容器(用于让小圆点与价格文本整体居中)
......@@ -61,10 +61,9 @@
self.countLabel.text = @"x--";
[self.cardView addSubview:self.countLabel];
self.priceDotView = [[UIView alloc] initWithFrame:CGRectZero];
self.priceDotView.backgroundColor = [UIColor colorWithHex:@"#52DDE2"];
self.priceDotView.layer.cornerRadius = 3;
self.priceDotView.layer.masksToBounds = YES;
self.priceDotView = [[UIImageView alloc] initWithFrame:CGRectZero];
self.priceDotView.contentMode = UIViewContentModeScaleAspectFit;
self.priceDotView.image = [FUSShowRoomCenterBunble imageNamed:@"home_list_pay"];
[self.cardView addSubview:self.priceDotView];
self.priceLabel = [[UILabel alloc] initWithFrame:CGRectZero];
......@@ -108,13 +107,13 @@
make.top.equalTo(self.countLabel.mas_bottom).offset(6);
make.centerX.equalTo(self.cardView);
make.height.mas_equalTo(16);
make.width.equalTo(self.priceLabel.mas_width).offset(10);
make.width.equalTo(self.priceLabel.mas_width).offset(16);
}];
[self.priceDotView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.priceContainerView);
make.centerY.equalTo(self.priceContainerView);
make.width.height.mas_equalTo(6);
make.width.height.mas_equalTo(12);
}];
[self.priceLabel mas_makeConstraints:^(MASConstraintMaker *make) {
......
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@class FUSTicketShowCollectTicketToggleResultModel;
/// 观众进入“限时表演中”房间时的入口弹窗交互类型
typedef NS_ENUM(NSInteger, FUSLiveShowTimeAudienceEntryMainViewAction) {
/// 关闭弹窗(点击背景或右上角关闭按钮)
FUSLiveShowTimeAudienceEntryMainViewActionDismiss = 0,
/// 点击“进入限时表演”主按钮
FUSLiveShowTimeAudienceEntryMainViewActionEnter = 1,
/// 点击“活动须知”入口
FUSLiveShowTimeAudienceEntryMainViewActionNotice = 2,
/// 点击票券档位(仅改变选择,不直接发起业务)
FUSLiveShowTimeAudienceEntryMainViewActionSelectTicketOption = 3,
};
/// 票券档位字典 Key:票数文案(示例:x1 / x10 / x5)
FOUNDATION_EXPORT NSString * const FUSLiveShowTimeAudienceEntryTicketOptionCountTextKey;
/// 票券档位字典 Key:价格文案(示例:100;不可用时建议传“--”,避免误导为免费)
FOUNDATION_EXPORT NSString * const FUSLiveShowTimeAudienceEntryTicketOptionPriceTextKey;
/// 票券档位字典 Key:贴附标签文案(示例:直接开始/抢当MVP;为空则不展示)
FOUNDATION_EXPORT NSString * const FUSLiveShowTimeAudienceEntryTicketOptionTagTextKey;
/// “限时表演中”观众入场弹窗(对齐 FUSPayRoomEntryMainView 的展示方式:全屏遮罩 + 居中白色卡片)
@interface FUSLiveShowTimeAudienceEntryMainView : UIView
/// 交互事件回调(仅分发用户意图;接口请求与跳转由外部决定)
@property (nonatomic, copy, nullable) void (^actionHandler)(FUSLiveShowTimeAudienceEntryMainViewAction action, NSInteger selectedOptionIndex);
/// 点击背景是否触发关闭(部分强引导场景可置 NO 以避免误触关闭)
@property (nonatomic, assign) BOOL dismissWhenTapBackground;
/// 在指定父视图上展示(会复用已存在实例,避免遮罩叠加造成层级/触摸问题)
/// - Parameters:
/// - rootView: 承载弹窗的父视图
/// - actionHandler: 交互回调(回传 action 与当前 selectedOptionIndex)
+ (instancetype)fus_showOnView:(UIView *)rootView
actionHandler:(void (^ _Nullable)(FUSLiveShowTimeAudienceEntryMainViewAction action, NSInteger selectedOptionIndex))actionHandler;
/// 基于 /ticketshow/buy/getdata 返回模型刷新整个弹窗展示(标题/倒计时/票档位等)
/// - Parameters:
/// - model: /ticketshow/buy/getdata 模型
/// - anchorFacePath: 主播头像地址(接口无头像,需外部传入)
- (void)fus_updateWithBuyGetDataModel:(FUSTicketShowCollectTicketToggleResultModel *)model
anchorFacePath:(nullable NSString *)anchorFacePath;
/// 关闭并移除弹窗(默认带渐隐缩放动画)
- (void)fus_dismissAnimated:(BOOL)animated;
@end
NS_ASSUME_NONNULL_END
#import "FUSLiveShowTimeAudienceEntryMainView.h"
#import <FUSCommon/FUSCommon.h>
#import <FUSFoundation/FUSFoundation.h>
#import <Masonry/Masonry.h>
#import "FUSShowRoomCenterBunble.h"
#import "FUSTicketShowCollectTicketToggleResultModel.h"
#import "CBAutoScrollLabel.h"
/// 复用同类弹窗实例,避免重复叠加遮罩导致触摸穿透/层级错乱
static NSInteger const kFUSLiveShowTimeAudienceEntryMainViewTag = 90917002;
NSString * const FUSLiveShowTimeAudienceEntryTicketOptionCountTextKey = @"countText";
NSString * const FUSLiveShowTimeAudienceEntryTicketOptionPriceTextKey = @"priceText";
NSString * const FUSLiveShowTimeAudienceEntryTicketOptionTagTextKey = @"tagText";
@interface FUSLiveShowTimeAudienceEntryTicketOptionCardView : UIControl
/// 选择框卡片容器(负责圆角/描边的选中态展示)
@property (nonatomic, strong) UIView *cardView;
/// 票券图标(资源:live_room_ticket)
@property (nonatomic, strong) UIImageView *ticketImageView;
/// 票数文案(示例:x1 / x10)
@property (nonatomic, strong) UILabel *countLabel;
/// 价格前置小圆点(视觉分隔)
@property (nonatomic, strong) UIImageView *priceDotView;
/// 价格文案(示例:100)
@property (nonatomic, strong) UILabel *priceLabel;
/// 价格区域容器(用于让小圆点与价格文案整体居中)
@property (nonatomic, strong) UIView *priceContainerView;
/// 贴附标签(示例:直接开始/抢当MVP;为空则隐藏)
@property (nonatomic, strong) UIButton *tagButton;
/// 刷新档位展示(仅更新 UI,不涉及业务决策;option 内使用本文件定义的 Key)
- (void)fus_setupWithOption:(NSDictionary<NSString *, NSString *> *)option;
@end
@implementation FUSLiveShowTimeAudienceEntryTicketOptionCardView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (!self) {
return nil;
}
self.backgroundColor = UIColor.clearColor;
self.cardView = [[UIView alloc] initWithFrame:CGRectZero];
self.cardView.backgroundColor = [UIColor colorWithHex:@"#D4D3D3"];
self.cardView.layer.cornerRadius = 10;
self.cardView.layer.masksToBounds = YES;
self.cardView.layer.borderWidth = 1;
self.cardView.layer.borderColor = [UIColor colorWithHex:@"#E6E8EB"].CGColor;
self.cardView.userInteractionEnabled = NO;
[self addSubview:self.cardView];
self.ticketImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
self.ticketImageView.contentMode = UIViewContentModeScaleAspectFit;
self.ticketImageView.image = [FUSShowRoomCenterBunble imageNamed:@"live_room_ticket"];
[self.cardView addSubview:self.ticketImageView];
self.countLabel = [[UILabel alloc] initWithFrame:CGRectZero];
self.countLabel.font = [UIFont fus_themeFont:12];
self.countLabel.textColor = [UIColor colorWithHex:@"#9AA0A6"];
self.countLabel.textAlignment = NSTextAlignmentCenter;
self.countLabel.text = @"x--";
[self.cardView addSubview:self.countLabel];
self.priceDotView = [[UIImageView alloc] initWithFrame:CGRectZero];
self.priceDotView.contentMode = UIViewContentModeScaleAspectFit;
self.priceDotView.image = [FUSShowRoomCenterBunble imageNamed:@"home_list_pay"];
[self.cardView addSubview:self.priceDotView];
self.priceLabel = [[UILabel alloc] initWithFrame:CGRectZero];
self.priceLabel.font = [UIFont fus_themeBoldFont:12];
self.priceLabel.textColor = [UIColor colorWithHex:@"#1F1F1F"];
self.priceLabel.text = @"--";
[self.cardView addSubview:self.priceLabel];
self.priceContainerView = [[UIView alloc] initWithFrame:CGRectZero];
self.priceContainerView.backgroundColor = UIColor.clearColor;
[self.cardView addSubview:self.priceContainerView];
[self.priceContainerView addSubview:self.priceDotView];
[self.priceContainerView addSubview:self.priceLabel];
self.tagButton = [UIButton buttonWithType:UIButtonTypeCustom];
self.tagButton.hidden = YES;
self.tagButton.userInteractionEnabled = NO;
[self.tagButton setBackgroundImage:[FUSShowRoomCenterBunble imageNamed:@"live_room_runBK"] forState:UIControlStateNormal];
[self.tagButton setTitleColor:UIColor.blackColor forState:UIControlStateNormal];
self.tagButton.titleLabel.font = [UIFont fus_themeBoldFont:11];
[self addSubview:self.tagButton];
[self.cardView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
[self.ticketImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.cardView).offset(12);
make.centerX.equalTo(self.cardView);
make.width.mas_equalTo(40);
make.height.mas_equalTo(21);
}];
[self.countLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.ticketImageView.mas_bottom).offset(4);
make.left.right.equalTo(self.cardView);
make.height.mas_equalTo(16);
}];
[self.priceContainerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.countLabel.mas_bottom).offset(6);
make.centerX.equalTo(self.cardView);
make.height.mas_equalTo(16);
make.width.equalTo(self.priceLabel.mas_width).offset(16);
}];
[self.priceDotView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.priceContainerView);
make.centerY.equalTo(self.priceContainerView);
make.width.height.mas_equalTo(12);
}];
[self.priceLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.priceDotView.mas_right).offset(4);
make.right.equalTo(self.priceContainerView);
make.centerY.equalTo(self.priceContainerView);
make.height.mas_equalTo(16);
}];
[self.tagButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self);
make.centerY.equalTo(self.cardView.mas_bottom);
make.width.mas_equalTo(57);
make.height.mas_equalTo(24);
}];
return self;
}
- (void)setSelected:(BOOL)selected {
[super setSelected:selected];
self.cardView.layer.borderWidth = (selected ? 1.5 : 1);
self.cardView.layer.borderColor = (selected ? [UIColor colorWithHex:@"#52DDE2"].CGColor : [UIColor colorWithHex:@"#E6E8EB"].CGColor);
}
- (void)fus_setupWithOption:(NSDictionary<NSString *, NSString *> *)option {
NSString *countText = [NSString stringWithObject:option[FUSLiveShowTimeAudienceEntryTicketOptionCountTextKey]];
NSString *priceText = [NSString stringWithObject:option[FUSLiveShowTimeAudienceEntryTicketOptionPriceTextKey]];
NSString *tagText = [NSString stringWithObject:option[FUSLiveShowTimeAudienceEntryTicketOptionTagTextKey]];
self.countLabel.text = ([NSString isNull:countText] ? @"x--" : countText);
self.priceLabel.text = ([NSString isNull:priceText] ? @"--" : priceText);
if (![NSString isNull:tagText]) {
self.tagButton.hidden = NO;
[self.tagButton setTitle:tagText forState:UIControlStateNormal];
} else {
self.tagButton.hidden = YES;
[self.tagButton setTitle:@"" forState:UIControlStateNormal];
}
[self setNeedsLayout];
}
@end
@interface FUSLiveShowTimeAudienceEntryTicketOptionGroupView : UIView
/// 档位卡片列表(根据传入 options 动态重建)
@property (nonatomic, strong) NSArray<FUSLiveShowTimeAudienceEntryTicketOptionCardView *> *optionViews;
/// 选择回调(外部负责记录选择并触发后续业务)
@property (nonatomic, copy, nullable) void (^selectHandler)(NSInteger index);
/// 更新档位数据(为保证布局稳定,采用销毁重建方式,避免残留旧视图状态)
- (void)fus_updateWithOptions:(NSArray<NSDictionary<NSString *, NSString *> *> *)options;
/// 设置选中态(仅改变 UI,便于外部统一管理状态)
- (void)fus_setSelectedIndex:(NSInteger)selectedIndex;
@end
@implementation FUSLiveShowTimeAudienceEntryTicketOptionGroupView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (!self) {
return nil;
}
self.backgroundColor = UIColor.clearColor;
self.optionViews = @[];
return self;
}
- (void)fus_updateWithOptions:(NSArray<NSDictionary<NSString *, NSString *> *> *)options {
for (UIView *v in self.optionViews) {
[v removeFromSuperview];
}
NSArray<NSDictionary<NSString *, NSString *> *> *validOptions = (options ?: @[]);
NSMutableArray<FUSLiveShowTimeAudienceEntryTicketOptionCardView *> *views = [[NSMutableArray alloc] initWithCapacity:validOptions.count];
for (NSInteger i = 0; i < validOptions.count; i++) {
FUSLiveShowTimeAudienceEntryTicketOptionCardView *card = [[FUSLiveShowTimeAudienceEntryTicketOptionCardView alloc] initWithFrame:CGRectZero];
card.tag = i;
[card addTarget:self action:@selector(fus_onTapOption:) forControlEvents:UIControlEventTouchUpInside];
[card fus_setupWithOption:validOptions[i]];
[self addSubview:card];
[views addObject:card];
}
self.optionViews = views.copy;
if (self.optionViews.count == 0) {
return;
}
CGFloat spacing = 12;
UIView *first = self.optionViews.firstObject;
[first mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.top.bottom.equalTo(self);
if (self.optionViews.count == 1) {
make.right.equalTo(self);
}
}];
for (NSInteger i = 1; i < self.optionViews.count; i++) {
UIView *prev = self.optionViews[i - 1];
UIView *cur = self.optionViews[i];
[cur mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(prev.mas_right).offset(spacing);
make.top.bottom.equalTo(self);
make.width.equalTo(first);
if (i == self.optionViews.count - 1) {
make.right.equalTo(self);
}
}];
}
}
- (void)fus_setSelectedIndex:(NSInteger)selectedIndex {
for (NSInteger i = 0; i < self.optionViews.count; i++) {
self.optionViews[i].selected = (i == selectedIndex);
}
}
- (void)fus_onTapOption:(UIControl *)sender {
NSInteger index = sender.tag;
if (self.selectHandler) {
self.selectHandler(index);
}
}
@end
@interface FUSLiveShowTimeAudienceEntryMainView ()
/// 背景遮罩按钮(用于拦截触摸并支持点击关闭)
@property (nonatomic, strong) UIButton *bgBtn;
/// 右上角关闭按钮(对齐项目通用关闭样式)
@property (nonatomic, strong) UIButton *closeBtn;
/// 白色内容卡片容器(承载所有展示内容)
@property (nonatomic, strong) UIView *cardView;
/// 标题(示例:精彩限时表演中,使用跑马灯滚动展示长文案)
@property (nonatomic, strong) CBAutoScrollLabel *titleLabel;
/// 主播头像
@property (nonatomic, strong) FUSRichIconView *anchorAvatarView;
/// 主标题(表演主题,跑马灯)
@property (nonatomic, strong) CBAutoScrollLabel *mainTitleLabel;
/// 表演结束倒计时文案(由外部驱动更新,UI 仅展示)
@property (nonatomic, strong) UILabel *countdownLabel;
/// 票券档位选择区(支持 1~N 档位)
@property (nonatomic, strong) FUSLiveShowTimeAudienceEntryTicketOptionGroupView *optionGroupView;
/// 主按钮(进入限时表演)
@property (nonatomic, strong) UIButton *enterButton;
/// 活动须知入口(带下划线文本,点击由外部跳转规则页/弹窗)
@property (nonatomic, strong) UIButton *noticeButton;
/// 当前选中的档位索引(用于对外回调,外部再决定对应的票数/价格)
@property (nonatomic, assign) NSInteger selectedOptionIndex;
/// 倒计时剩余秒数(单位:秒;由倒计时驱动逐步递减)
@property (nonatomic, assign) NSInteger countdownRemainingSeconds;
/// GCD 定时器:用于每秒刷新倒计时文本
/// 内存管理说明:
/// - 使用 dispatch_source_t,避免 RunLoop 依赖
/// - dealloc 与弹层关闭时均会取消,防止泄漏与回调野指针
@property (nonatomic, strong, nullable) dispatch_source_t countdownTimer;
@end
@implementation FUSLiveShowTimeAudienceEntryMainView
+ (instancetype)fus_showOnView:(UIView *)rootView
actionHandler:(void (^ _Nullable)(FUSLiveShowTimeAudienceEntryMainViewAction action, NSInteger selectedOptionIndex))actionHandler {
if (!rootView) {
return nil;
}
FUSLiveShowTimeAudienceEntryMainView *existView = [rootView viewWithTag:kFUSLiveShowTimeAudienceEntryMainViewTag];
if ([existView isKindOfClass:FUSLiveShowTimeAudienceEntryMainView.class]) {
[rootView bringSubviewToFront:existView];
existView.actionHandler = actionHandler;
[existView fus_showAnimated:YES];
return existView;
}
FUSLiveShowTimeAudienceEntryMainView *view = [[FUSLiveShowTimeAudienceEntryMainView alloc] initWithFrame:CGRectZero];
view.tag = kFUSLiveShowTimeAudienceEntryMainViewTag;
view.actionHandler = actionHandler;
[rootView addSubview:view];
[view mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(rootView);
}];
[view fus_showAnimated:YES];
return view;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.dismissWhenTapBackground = YES;
self.selectedOptionIndex = 0;
[self fus_buildUI];
[self fus_bindActions];
[self fus_updateTitleText:nil anchorName:nil anchorFacePath:nil countdownText:nil enterButtonTitle:nil noticeText:nil];
[self fus_updateTicketOptions:@[] selectedIndex:0];
}
return self;
}
- (void)fus_updateTitleText:(nullable NSString *)titleText
anchorName:(nullable NSString *)anchorName
anchorFacePath:(nullable NSString *)anchorFacePath
countdownText:(nullable NSString *)countdownText
enterButtonTitle:(nullable NSString *)enterButtonTitle
noticeText:(nullable NSString *)noticeText {
self.titleLabel.text = ([NSString isNull:titleText] ? [NSString fus_localString:@"精彩限时表演中"] : titleText);
[self.mainTitleLabel setText:([NSString isNull:anchorName] ? @"" : anchorName) refreshLabels:YES];
self.countdownLabel.text = ([NSString isNull:countdownText] ? [NSString fus_localString:@"表演结束倒计时:--:--"] : countdownText);
[self.enterButton setTitle:([NSString isNull:enterButtonTitle] ? [NSString fus_localString:@"进入限时表演"] : enterButtonTitle) forState:UIControlStateNormal];
NSString *notice = ([NSString isNull:noticeText] ? [NSString fus_localString:@"活动须知"] : noticeText);
NSDictionary *attrs = @{
NSFontAttributeName: [UIFont fus_themeFont:11],
NSForegroundColorAttributeName: [UIColor colorWithHex:@"#9B9B9B"],
NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle),
NSUnderlineColorAttributeName: [UIColor colorWithHex:@"#9B9B9B"]
};
[self.noticeButton setAttributedTitle:[[NSAttributedString alloc] initWithString:notice attributes:attrs] forState:UIControlStateNormal];
[self.anchorAvatarView fus_setupLiveIconWithFacePath:([NSString isNull:anchorFacePath] ? @"" : anchorFacePath) level:0 animated:NO];
}
- (void)fus_updateTicketOptions:(NSArray<NSDictionary<NSString *,NSString *> *> *)ticketOptions
selectedIndex:(NSInteger)selectedIndex {
NSArray<NSDictionary<NSString *, NSString *> *> *options = (ticketOptions ?: @[]);
[self.optionGroupView fus_updateWithOptions:options];
NSInteger selected = 0;
if (options.count > 0) {
selected = MAX(0, MIN((NSInteger)options.count - 1, selectedIndex));
}
self.selectedOptionIndex = selected;
[self.optionGroupView fus_setSelectedIndex:selected];
}
- (void)fus_updateWithBuyGetDataModel:(FUSTicketShowCollectTicketToggleResultModel *)model
anchorFacePath:(nullable NSString *)anchorFacePath {
NSString *theme = [model.showTheme description];
NSInteger remainMs = (model.stageData ? model.stageData.showStatusTime : model.showStatusTime);
NSInteger remainSec = MAX(0, (NSInteger)llround(((double)remainMs / 1000.0)));
[self fus_updateTitleText:nil
anchorName:(![NSString isNull:theme] ? theme : nil)
anchorFacePath:(![NSString isNull:anchorFacePath] ? anchorFacePath : nil)
countdownText:nil
enterButtonTitle:nil
noticeText:nil];
[self fus_startCountdownWithRemaining:remainSec];
NSInteger unitPrice = MAX(0, model.buyPrice);
NSInteger directNum = MAX(1, model.directBuyNum);
NSInteger strengthNum = MAX(1, model.showStrengthNum);
NSInteger mvpNum = MAX(1, model.mvpBuyNum);
NSString *priceText = [@(unitPrice) description];
NSDictionary *opt1 = @{
FUSLiveShowTimeAudienceEntryTicketOptionCountTextKey: [NSString stringWithFormat:@"x%ld", (long)directNum],
FUSLiveShowTimeAudienceEntryTicketOptionPriceTextKey: priceText,
};
NSDictionary *opt2 = @{
FUSLiveShowTimeAudienceEntryTicketOptionCountTextKey: [NSString stringWithFormat:@"x%ld", (long)strengthNum],
FUSLiveShowTimeAudienceEntryTicketOptionPriceTextKey: priceText,
};
NSDictionary *opt3 = @{
FUSLiveShowTimeAudienceEntryTicketOptionCountTextKey: [NSString stringWithFormat:@"x%ld", (long)mvpNum],
FUSLiveShowTimeAudienceEntryTicketOptionPriceTextKey: priceText,
};
[self fus_updateTicketOptions:@[opt1, opt2, opt3] selectedIndex:0];
}
- (void)fus_dismissAnimated:(BOOL)animated {
[self fus_stopCountdown];
if (!self.superview) {
return;
}
if (!animated) {
[self removeFromSuperview];
return;
}
__weak typeof(self) weakSelf = self;
[UIView animateWithDuration:0.25 animations:^{
weakSelf.alpha = 0;
weakSelf.cardView.transform = CGAffineTransformMakeScale(0.98, 0.98);
} completion:^(BOOL finished) {
[weakSelf removeFromSuperview];
}];
}
/// 生命周期释放:保证定时器被取消,避免野回调
- (void)dealloc {
[self fus_stopCountdown];
}
/// 启动倒计时
/// 说明:
/// - 先停止已有定时器,确保单一计时来源
/// - 首次设置即刻刷新一次 UI,再进入每秒触发
/// - UI 更新在主线程执行,保证线程安全
/// - 使用 weak/strong self,避免定时器闭包循环引用
- (void)fus_startCountdownWithRemaining:(NSInteger)sec {
[self fus_stopCountdown];
self.countdownRemainingSeconds = sec;
if (sec <= 0) {
self.countdownLabel.text = [NSString fus_localString:@"表演结束倒计时:--:--"];
return;
}
NSInteger mm = (self.countdownRemainingSeconds / 60);
NSInteger ss = (self.countdownRemainingSeconds % 60);
self.countdownLabel.text = [NSString stringWithFormat:[NSString fus_localString:@"表演结束倒计时:%02ld:%02ld"], (long)mm, (long)ss];
dispatch_queue_t q = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);
dispatch_source_t t = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q);
self.countdownTimer = t;
dispatch_source_set_timer(t, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC), NSEC_PER_SEC, (uint64_t)(0.1 * NSEC_PER_SEC));
__weak typeof(self) weakSelf = self;
dispatch_source_set_event_handler(t, ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
if (strongSelf.countdownRemainingSeconds <= 0) {
[strongSelf fus_stopCountdown];
dispatch_async(dispatch_get_main_queue(), ^{
strongSelf.countdownLabel.text = [NSString fus_localString:@"表演结束倒计时:--:--"];
});
return;
}
NSInteger cmm = (strongSelf.countdownRemainingSeconds / 60);
NSInteger css = (strongSelf.countdownRemainingSeconds % 60);
dispatch_async(dispatch_get_main_queue(), ^{
strongSelf.countdownLabel.text = [NSString stringWithFormat:[NSString fus_localString:@"表演结束倒计时:%02ld:%02ld"], (long)cmm, (long)css];
});
strongSelf.countdownRemainingSeconds = MAX(0, strongSelf.countdownRemainingSeconds - 1);
});
dispatch_resume(t);
}
/// 停止倒计时
/// 说明:
/// - 取消 GCD 定时器并置空
/// - 多次调用安全可重入
- (void)fus_stopCountdown {
if (self.countdownTimer) {
dispatch_source_cancel(self.countdownTimer);
self.countdownTimer = nil;
}
}
- (void)fus_buildUI {
self.alpha = 0;
self.backgroundColor = UIColor.clearColor;
self.bgBtn = [UIButton buttonWithType:UIButtonTypeCustom];
self.bgBtn.backgroundColor = [UIColor colorWithWhite:0 alpha:0.55];
[self addSubview:self.bgBtn];
self.closeBtn = [UIButton buttonWithType:UIButtonTypeCustom];
[self.closeBtn setImage:[FUSShowRoomCenterBunble imageNamed:@"live_board_close"] forState:UIControlStateNormal];
[self addSubview:self.closeBtn];
self.cardView = [[UIView alloc] initWithFrame:CGRectZero];
self.cardView.backgroundColor = UIColor.whiteColor;
self.cardView.layer.cornerRadius = 16;
self.cardView.layer.masksToBounds = YES;
[self addSubview:self.cardView];
self.titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
self.titleLabel.font = [UIFont fus_themeMediumFont:16];
self.titleLabel.textColor = [UIColor colorWithHex:@"#2A2A2A"];
self.titleLabel.textAlignment = NSTextAlignmentCenter;
[self.cardView addSubview:self.titleLabel];
self.anchorAvatarView = [[FUSRichIconView alloc] initWithFrame:CGRectZero];
self.anchorAvatarView.showNoVipLevel = NO;
self.anchorAvatarView.richWearImageView.hidden = YES;
self.anchorAvatarView.faceImageView.layer.cornerRadius = 27;
self.anchorAvatarView.faceImageView.layer.masksToBounds = YES;
self.anchorAvatarView.faceImageView.contentMode = UIViewContentModeScaleAspectFill;
[self.cardView addSubview:self.anchorAvatarView];
self.mainTitleLabel = [[CBAutoScrollLabel alloc] initWithFrame:CGRectZero];
self.mainTitleLabel.font = [UIFont fus_themeMediumFont:16];
self.mainTitleLabel.textColor = [UIColor colorWithHex:@"#2A2A2A"];
self.mainTitleLabel.textAlignment = NSTextAlignmentCenter;
self.mainTitleLabel.fadeLength = 7;
self.mainTitleLabel.scrollDirection = CBAutoScrollDirectionLeft;
self.mainTitleLabel.scrollSpeed = 30;
self.mainTitleLabel.pauseInterval = 1.5;
[self.mainTitleLabel observeApplicationNotifications];
[self.cardView addSubview:self.mainTitleLabel];
self.countdownLabel = [[UILabel alloc] initWithFrame:CGRectZero];
self.countdownLabel.font = [UIFont fus_themeFont:12];
self.countdownLabel.textColor = [UIColor colorWithHex:@"#9B9B9B"];
self.countdownLabel.textAlignment = NSTextAlignmentCenter;
self.countdownLabel.numberOfLines = 1;
[self.cardView addSubview:self.countdownLabel];
self.optionGroupView = [[FUSLiveShowTimeAudienceEntryTicketOptionGroupView alloc] initWithFrame:CGRectZero];
[self.cardView addSubview:self.optionGroupView];
self.enterButton = [UIButton buttonWithType:UIButtonTypeCustom];
self.enterButton.titleLabel.font = [UIFont fus_themeMediumFont:15];
[self.enterButton setTitleColor:[UIColor colorWithHex:@"#003333"] forState:UIControlStateNormal];
self.enterButton.backgroundColor = [UIColor colorWithRed:(45.0 / 255.0) green:(226.0 / 255.0) blue:(224.0 / 255.0) alpha:1.0];
self.enterButton.layer.cornerRadius = 22;
self.enterButton.layer.masksToBounds = YES;
[self.cardView addSubview:self.enterButton];
self.noticeButton = [UIButton buttonWithType:UIButtonTypeCustom];
[self.cardView addSubview:self.noticeButton];
[self.bgBtn mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
[self.closeBtn mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.mas_safeAreaLayoutGuideTop).offset(6);
make.right.equalTo(self).offset(-8);
make.width.height.mas_equalTo(44);
}];
[self.cardView mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self);
make.size.mas_equalTo(CGSizeMake(285, 360));
}];
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.cardView).offset(18);
make.left.equalTo(self.cardView).offset(20);
make.right.equalTo(self.cardView).offset(-20);
make.height.mas_equalTo(22);
}];
[self.anchorAvatarView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.titleLabel.mas_bottom).offset(14);
make.centerX.equalTo(self.cardView);
make.size.mas_equalTo(CGSizeMake(54, 54));
}];
[self.mainTitleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.anchorAvatarView.mas_bottom).offset(10);
make.left.equalTo(self.cardView).offset(20);
make.right.equalTo(self.cardView).offset(-20);
make.height.mas_equalTo(20);
}];
[self.countdownLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.mainTitleLabel.mas_bottom).offset(6);
make.left.equalTo(self.cardView).offset(20);
make.right.equalTo(self.cardView).offset(-20);
make.height.mas_equalTo(16);
}];
[self.optionGroupView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.countdownLabel.mas_bottom).offset(14);
make.left.equalTo(self.cardView).offset(20);
make.right.equalTo(self.cardView).offset(-20);
make.height.mas_equalTo(85);
}];
[self.enterButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.optionGroupView.mas_bottom).offset(16);
make.left.equalTo(self.cardView).offset(20);
make.right.equalTo(self.cardView).offset(-20);
make.height.mas_equalTo(44);
}];
[self.noticeButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.enterButton.mas_bottom).offset(10);
make.centerX.equalTo(self.cardView);
make.height.mas_equalTo(16);
make.bottom.lessThanOrEqualTo(self.cardView).offset(-12);
}];
}
- (void)fus_bindActions {
__weak typeof(self) weakSelf = self;
[self.bgBtn addTarget:self action:@selector(fus_onTapBackground) forControlEvents:UIControlEventTouchUpInside];
[self.closeBtn addTarget:self action:@selector(fus_onTapClose) forControlEvents:UIControlEventTouchUpInside];
[self.enterButton addTarget:self action:@selector(fus_onTapEnter) forControlEvents:UIControlEventTouchUpInside];
[self.noticeButton addTarget:self action:@selector(fus_onTapNotice) forControlEvents:UIControlEventTouchUpInside];
self.optionGroupView.selectHandler = ^(NSInteger index) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
/// 仅记录选择并回调给外部,业务侧可以基于 index 决定票数/价格/跳转逻辑
strongSelf.selectedOptionIndex = index;
[strongSelf.optionGroupView fus_setSelectedIndex:index];
if (strongSelf.actionHandler) {
strongSelf.actionHandler(FUSLiveShowTimeAudienceEntryMainViewActionSelectTicketOption, index);
}
};
}
- (void)fus_showAnimated:(BOOL)animated {
if (!animated) {
self.alpha = 1;
self.cardView.transform = CGAffineTransformIdentity;
return;
}
self.alpha = 0;
self.cardView.transform = CGAffineTransformMakeScale(0.98, 0.98);
__weak typeof(self) weakSelf = self;
[UIView animateWithDuration:0.25 animations:^{
weakSelf.alpha = 1;
weakSelf.cardView.transform = CGAffineTransformIdentity;
}];
}
- (void)fus_onTapBackground {
if (!self.dismissWhenTapBackground) {
return;
}
/// 点击背景属于“用户意图关闭”,是否真正关闭交由外部统一控制(便于做强引导/二次确认)
if (self.actionHandler) {
self.actionHandler(FUSLiveShowTimeAudienceEntryMainViewActionDismiss, self.selectedOptionIndex);
}
}
- (void)fus_onTapClose {
if (self.actionHandler) {
self.actionHandler(FUSLiveShowTimeAudienceEntryMainViewActionDismiss, self.selectedOptionIndex);
}
}
- (void)fus_onTapEnter {
if (self.actionHandler) {
self.actionHandler(FUSLiveShowTimeAudienceEntryMainViewActionEnter, self.selectedOptionIndex);
}
}
- (void)fus_onTapNotice {
if (self.actionHandler) {
self.actionHandler(FUSLiveShowTimeAudienceEntryMainViewActionNotice, self.selectedOptionIndex);
}
}
@end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment