Commit 350e7cac by suolong

限时表演UI

parent 186fb7b0
Showing with 541 additions and 29 deletions
{
"images" : [
{
"filename" : "live_room_runBK.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "live_room_runBK@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "live_room_runBK@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "live_room_ticket.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "live_room_ticket@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "live_room_ticket@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/// “限时表演”观众端:购票入口弹窗(从底部上滑展示)
@interface FUSLiveShowTimeTicketActionPopView : UIView
/// 弹窗入口类型(用于默认选中项/顶部标识态)
typedef NS_ENUM(NSInteger, FUSLiveShowTimeTicketActionEntryType) {
/// 从“购票选秀/购票支持”入口进入
FUSLiveShowTimeTicketActionEntryTypeBuy = 1,
/// 从“抢当MVP”入口进入
FUSLiveShowTimeTicketActionEntryTypeGrabMVP = 2,
};
/// 购票选项
typedef NS_ENUM(NSInteger, FUSLiveShowTimeTicketPurchaseOption) {
/// 选择 1 张票
FUSLiveShowTimeTicketPurchaseOptionOneTicket = 0,
/// 补全所有票
FUSLiveShowTimeTicketPurchaseOptionFillAllTickets = 1,
/// 购买“抢当 MVP 所需”的票数
FUSLiveShowTimeTicketPurchaseOptionMVPRequiredTickets = 2,
};
/// 在指定容器上展示弹窗
/// @param onView 承载弹窗的父视图
/// @param entryType 入口类型(用于默认选中项/顶部标识态)
+ (instancetype)fus_showOnView:(UIView *)onView entryType:(FUSLiveShowTimeTicketActionEntryType)entryType;
/// 当前入口类型(外部只读,便于埋点/判断)
@property (nonatomic, assign, readonly) FUSLiveShowTimeTicketActionEntryType entryType;
/// 当前选中的购票选项
@property (nonatomic, assign) FUSLiveShowTimeTicketPurchaseOption selectedOption;
/// 更新底部主按钮文案(例如“确认购买”“支持主播”)
@property (nonatomic, copy, nullable) NSString *mainActionTitle;
/// 点击底部主按钮回调(由外部决定具体业务行为)
/// @param option 当前选中的购票选项
@property (nonatomic, copy, nullable) void (^confirmHandler)(FUSLiveShowTimeTicketPurchaseOption option);
/// 更新“补全所有票/抢当MVP所需”的票数显示
/// @param remainingTicketCount 还差多少张票(<0 表示未知,不展示)
/// @param mvpNeedTicketCount 抢当MVP需要多少张票(<0 表示未知,不展示)
- (void)fus_updateRemainingTicketCount:(NSInteger)remainingTicketCount
mvpNeedTicketCount:(NSInteger)mvpNeedTicketCount;
/// 更新“票的贡献”列表数据
/// @param contributionList 元素建议包含:nickname/face/ticketCount(字段缺失会兜底空展示)
- (void)fus_updateContributionList:(NSArray<NSDictionary *> *)contributionList;
/// 更新顶部“已集票数”显示
/// @param currentTicketCount 已集票数(<0 表示未知)
/// @param targetTicketCount 目标票数(<=0 表示未知)
- (void)fus_updateCollectedTicketCount:(NSInteger)currentTicketCount
targetTicketCount:(NSInteger)targetTicketCount;
/// 更新阶段状态(用于展示“精彩限时表演中”等样式)
/// @param stageStatus 0:集票中 1:待表演 2:表演中 9999:已结束
- (void)fus_updateStageStatus:(NSInteger)stageStatus;
/// 更新右上角倒计时
/// @param remainingSeconds 剩余秒数;<0 表示不展示倒计时
- (void)fus_updateCountdownRemainingSeconds:(NSInteger)remainingSeconds;
/// 主动关闭弹窗(会带动画并移除)
- (void)fus_dismiss;
@end
NS_ASSUME_NONNULL_END
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/// “限时表演-票的贡献”列表 cell
@interface FUSLiveShowTimeTicketContributionCell : UITableViewCell
/// 刷新贡献数据
/// @param face 头像 URL(可为空)
/// @param nickname 昵称(可为空)
/// @param ticketCount 票数
/// @param isMVP 是否为 MVP(控制左侧 MVP 标签展示)
- (void)fus_setupWithFace:(NSString *)face
nickname:(NSString *)nickname
ticketCount:(NSInteger)ticketCount
isMVP:(BOOL)isMVP;
@end
NS_ASSUME_NONNULL_END
#import "FUSLiveShowTimeTicketContributionCell.h"
#import <FUSCommon/FUSCommon.h>
#import <FUSFoundation/FUSFoundation.h>
#import <Masonry/Masonry.h>
#import "FUSShowRoomCenterBunble.h"
@implementation FUSLiveShowTimeTicketContributionCell {
/// 头像(圆形裁切)
UIImageView *_faceImageView;
/// 昵称
UILabel *_nicknameLabel;
/// MVP 标签(仅 MVP 用户展示)
UILabel *_mvpTagLabel;
/// 票券图标
UIImageView *_ticketIconView;
/// 票数
UILabel *_countLabel;
/// 底部分割线
UIView *_dividerView;
/// MVP 标签宽度约束(用于 show/hide 时收缩宽度)
MASConstraint *_mvpWidthConstraint;
}
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (!self) {
return nil;
}
self.contentView.backgroundColor = UIColor.whiteColor;
self.backgroundColor = UIColor.whiteColor;
self.selectionStyle = UITableViewCellSelectionStyleNone;
_faceImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
_faceImageView.contentMode = UIViewContentModeScaleAspectFill;
_faceImageView.layer.cornerRadius = 16;
_faceImageView.layer.masksToBounds = YES;
[self.contentView addSubview:_faceImageView];
_nicknameLabel = [[UILabel alloc] initWithFrame:CGRectZero];
_nicknameLabel.font = [UIFont fus_themeFont:14];
_nicknameLabel.textColor = [UIColor colorWithHex:@"#1F1F1F"];
[self.contentView addSubview:_nicknameLabel];
_mvpTagLabel = [[UILabel alloc] initWithFrame:CGRectZero];
_mvpTagLabel.hidden = YES;
_mvpTagLabel.textAlignment = NSTextAlignmentCenter;
_mvpTagLabel.font = [UIFont fus_themeBoldFont:9];
_mvpTagLabel.textColor = UIColor.blackColor;
_mvpTagLabel.backgroundColor = [UIColor colorWithHex:@"#52DDE2"];
_mvpTagLabel.text = @"MVP";
_mvpTagLabel.layer.cornerRadius = 7;
_mvpTagLabel.layer.masksToBounds = YES;
[self.contentView addSubview:_mvpTagLabel];
_countLabel = [[UILabel alloc] initWithFrame:CGRectZero];
_countLabel.font = [UIFont fus_themeBoldFont:14];
_countLabel.textColor = [UIColor colorWithHex:@"#1F1F1F"];
_countLabel.textAlignment = NSTextAlignmentRight;
[self.contentView addSubview:_countLabel];
_ticketIconView = [[UIImageView alloc] initWithFrame:CGRectZero];
_ticketIconView.image = [FUSShowRoomCenterBunble imageNamed:@"live_room_ticket"];
_ticketIconView.contentMode = UIViewContentModeScaleAspectFit;
[self.contentView addSubview:_ticketIconView];
_dividerView = [[UIView alloc] initWithFrame:CGRectZero];
_dividerView.backgroundColor = [UIColor colorWithHex:@"#F0F1F3"];
[self.contentView addSubview:_dividerView];
[_faceImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.contentView).offset(16);
make.centerY.equalTo(self.contentView);
make.width.height.mas_equalTo(32);
}];
[_ticketIconView mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(self.contentView).offset(-16);
make.centerY.equalTo(self.contentView);
make.width.height.mas_equalTo(14);
}];
[_countLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(_ticketIconView.mas_left).offset(-6);
make.top.bottom.equalTo(self.contentView);
make.width.mas_equalTo(70);
}];
[_mvpTagLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(_faceImageView.mas_right).offset(10);
make.centerY.equalTo(self.contentView);
make.height.mas_equalTo(14);
_mvpWidthConstraint = make.width.mas_equalTo(0);
}];
[_nicknameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(_faceImageView.mas_right).offset(10);
make.right.equalTo(_countLabel.mas_left).offset(-10);
make.top.bottom.equalTo(self.contentView);
}];
[_dividerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.contentView).offset(16);
make.right.equalTo(self.contentView).offset(-16);
make.bottom.equalTo(self.contentView);
make.height.mas_equalTo(0.5);
}];
return self;
}
- (void)prepareForReuse {
[super prepareForReuse];
/// 复用时清理展示内容,避免旧数据闪现
_faceImageView.image = nil;
_nicknameLabel.text = @"";
_countLabel.text = @"";
_mvpTagLabel.hidden = YES;
}
- (void)fus_setupWithFace:(NSString *)face nickname:(NSString *)nickname ticketCount:(NSInteger)ticketCount isMVP:(BOOL)isMVP {
/// 头像优先加载网络图,兜底默认头像
if (![NSString isNull:face]) {
[_faceImageView setWebImageWithSubURLString:face placeholder:UIImage.fus_defaultIcon];
} else {
_faceImageView.image = UIImage.fus_defaultIcon;
}
/// 票数与 MVP 标签受服务端数据影响,统一做兜底,避免异常值导致 UI 抖动
_nicknameLabel.text = (nickname.length > 0 ? nickname : @"");
_countLabel.text = [NSString stringWithFormat:@"%zd", (NSInteger)MAX(0, ticketCount)];
_mvpTagLabel.hidden = !isMVP;
[_mvpWidthConstraint uninstall];
[_mvpTagLabel mas_updateConstraints:^(MASConstraintMaker *make) {
_mvpWidthConstraint = make.width.mas_equalTo(isMVP ? 34 : 0);
}];
[_nicknameLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(isMVP ? _mvpTagLabel.mas_right : _faceImageView.mas_right).offset(isMVP ? 6 : 10);
make.right.equalTo(_countLabel.mas_left).offset(-10);
make.top.bottom.equalTo(self.contentView);
}];
}
@end
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/// “限时表演-票的贡献”列表 View(内部为 UITableView,支持滚动)
@interface FUSLiveShowTimeTicketContributionListView : UIView
/// 票的贡献列表数据源(元素建议包含:nickname/face/ticketCount/isMVP)
@property (nonatomic, copy) NSArray<NSDictionary *> *contributionList;
@end
NS_ASSUME_NONNULL_END
#import "FUSLiveShowTimeTicketContributionListView.h"
#import <FUSCommon/FUSCommon.h>
#import <FUSFoundation/FUSFoundation.h>
#import <Masonry/Masonry.h>
#import "FUSLiveShowTimeTicketContributionCell.h"
@interface FUSLiveShowTimeTicketContributionListView () <UITableViewDelegate, UITableViewDataSource>
/// 承载贡献列表的 tableView(复用 cell,避免频繁创建视图)
@property (nonatomic, strong) UITableView *tableView;
@end
@implementation FUSLiveShowTimeTicketContributionListView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (!self) {
return nil;
}
/// 外部可能异步下发,先给一个空数组避免 nil 分支判断
_contributionList = @[];
self.backgroundColor = UIColor.whiteColor;
self.tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.tableView.backgroundColor = UIColor.whiteColor;
self.tableView.rowHeight = 52;
[self.tableView registerClass:FUSLiveShowTimeTicketContributionCell.class forCellReuseIdentifier:NSStringFromClass(FUSLiveShowTimeTicketContributionCell.class)];
[self addSubview:self.tableView];
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
return self;
}
- (void)setContributionList:(NSArray<NSDictionary *> *)contributionList {
/// copy 保证外部可变数组传入时不会被后续修改影响 UI 展示
_contributionList = [contributionList copy] ?: @[];
[self.tableView reloadData];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.contributionList.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
FUSLiveShowTimeTicketContributionCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(FUSLiveShowTimeTicketContributionCell.class) forIndexPath:indexPath];
NSDictionary *item = nil;
if (indexPath.row < self.contributionList.count) {
item = self.contributionList[indexPath.row];
}
id nicknameValue = item[@"nickname"];
NSString *nickname = ([nicknameValue isKindOfClass:NSString.class] ? (NSString *)nicknameValue : @"");
id faceValue = item[@"face"];
NSString *face = ([faceValue isKindOfClass:NSString.class] ? (NSString *)faceValue : @"");
NSInteger ticketCount = 0;
id countValue = item[@"ticketCount"];
if ([countValue isKindOfClass:NSNumber.class]) {
ticketCount = [(NSNumber *)countValue integerValue];
} else if ([countValue isKindOfClass:NSString.class]) {
ticketCount = [(NSString *)countValue integerValue];
}
BOOL isMVP = NO;
id isMVPValue = item[@"isMVP"];
if ([isMVPValue isKindOfClass:NSNumber.class]) {
isMVP = ([(NSNumber *)isMVPValue integerValue] == 1);
} else if ([isMVPValue isKindOfClass:NSString.class]) {
isMVP = ([(NSString *)isMVPValue integerValue] == 1);
}
[cell fus_setupWithFace:face nickname:nickname ticketCount:ticketCount isMVP:isMVP];
return cell;
}
@end
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/// “限时表演-活动须知”View(内部为 UITextView,支持长文滚动)
@interface FUSLiveShowTimeTicketNoticeView : UIView
/// 活动须知文本内容(内部使用 UITextView 支持长文滚动展示)
@property (nonatomic, copy) NSString *noticeText;
@end
NS_ASSUME_NONNULL_END
#import "FUSLiveShowTimeTicketNoticeView.h"
#import <FUSCommon/FUSCommon.h>
#import <FUSFoundation/FUSFoundation.h>
#import <Masonry/Masonry.h>
@interface FUSLiveShowTimeTicketNoticeView ()
/// 须知正文(长文可滚动,避免超出一屏)
@property (nonatomic, strong) UITextView *textView;
@end
@implementation FUSLiveShowTimeTicketNoticeView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (!self) {
return nil;
}
/// 外部可异步下发文本,先给默认值避免 nil
_noticeText = @"";
self.backgroundColor = UIColor.whiteColor;
self.textView = [[UITextView alloc] initWithFrame:CGRectZero];
self.textView.editable = NO;
self.textView.scrollEnabled = YES;
self.textView.showsVerticalScrollIndicator = NO;
self.textView.textContainerInset = UIEdgeInsetsZero;
self.textView.textContainer.lineFragmentPadding = 0;
self.textView.font = [UIFont fus_themeFont:14];
self.textView.textColor = [UIColor colorWithHex:@"#666666"];
self.textView.backgroundColor = UIColor.clearColor;
[self addSubview:self.textView];
[self.textView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self).offset(16);
make.left.equalTo(self).offset(16);
make.right.equalTo(self).offset(-16);
make.bottom.equalTo(self).offset(-16);
}];
return self;
}
- (void)setNoticeText:(NSString *)noticeText {
/// copy 保证外部可变字符串不会影响已渲染内容
_noticeText = [noticeText copy] ?: @"";
self.textView.text = _noticeText;
}
@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