feat: add new localization strings for login functionality

主要变更:
1. 新增多个与登录相关的本地化字符串,包括一键登录、手机号登录、用户服务协议等。
2. 更新了用户登录界面的提示信息,以提升用户体验。

此更新旨在增强应用的本地化支持,提供更好的用户交互体验。
This commit is contained in:
edwinQQQ
2025-10-17 15:54:28 +08:00
parent a18cbdc3e5
commit 0bb912bac9
309 changed files with 20 additions and 40874 deletions

View File

@@ -1,15 +0,0 @@
//
// AppDelegate.h
// YUMI
//
// Created by admin on 2023/3/9.
//
#import <UIKit/UIKit.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@end

View File

@@ -1,176 +0,0 @@
//
// AppDelegate.m
// YUMI
//
// Created by admin on 2023/3/9.
//
#import "AppDelegate.h"
#import "BaseNavigationController.h"
#import <AppTrackingTransparency/AppTrackingTransparency.h>
#import "YuMi-swift.h"
UIKIT_EXTERN NSString * const kOpenRoomNotification;
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIStoryboard *launchStoryboard = [UIStoryboard storyboardWithName:@"Launch Screen" bundle:nil];
UIViewController *launchScreenVC = [launchStoryboard instantiateInitialViewController];
self.window.rootViewController = launchScreenVC;
[self.window makeKeyAndVisible];
[self loadMainPage];
if (@available(iOS 15, *)) {
[[UITableView appearance] setSectionHeaderTopPadding:0];
}
return YES;
}
// MARK: - Helper Methods
/// 获取 keyWindowiOS 13+ 兼容)
- (UIWindow *)getKeyWindow {
// iOS 13+ 使用 connectedScenes 获取 window
if (@available(iOS 13.0, *)) {
for (UIWindowScene *scene in [UIApplication sharedApplication].connectedScenes) {
if (scene.activationState == UISceneActivationStateForegroundActive) {
for (UIWindow *window in scene.windows) {
if (window.isKeyWindow) {
return window;
}
}
// 如果没有 keyWindow返回第一个 window
return scene.windows.firstObject;
}
}
}
// iOS 13 以下,使用旧方法(已废弃但仍然可用)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return [UIApplication sharedApplication].keyWindow;
#pragma clang diagnostic pop
}
- (void)loadMainPage {
AccountModel *accountModel = [[AccountInfoStorage instance] getCurrentAccountInfo];
if (accountModel == nil ||
accountModel.uid == nil ||
accountModel.access_token == nil) {
[self toLoginPage];
}else{
[self toHomeTabbarPage];
// 延迟检查专属颜色(等待 window 初始化完成)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.8 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self checkAndShowSignatureColorGuide];
});
}
}
/// 检查并显示专属颜色引导页
- (void)checkAndShowSignatureColorGuide {
UIWindow *keyWindow = [self getKeyWindow];
if (!keyWindow) return;
BOOL hasSignatureColor = [EPEmotionColorStorage hasUserSignatureColor];
#if DEBUG
// Debug 环境:总是显示引导页
NSLog(@"[AppDelegate] Debug 模式:显示专属颜色引导页(已有颜色: %@", hasSignatureColor ? @"YES" : @"NO");
EPSignatureColorGuideView *guideView = [[EPSignatureColorGuideView alloc] init];
// 设置颜色确认回调
guideView.onColorConfirmed = ^(NSString *hexColor) {
[EPEmotionColorStorage saveUserSignatureColor:hexColor];
NSLog(@"[AppDelegate] 用户选择专属颜色: %@", hexColor);
};
// 如果已有颜色,设置 Skip 回调
if (hasSignatureColor) {
guideView.onSkipTapped = ^{
NSLog(@"[AppDelegate] 用户跳过专属颜色选择");
};
}
// 显示引导页,已有颜色时显示 Skip 按钮
[guideView showInWindow:keyWindow showSkipButton:hasSignatureColor];
#else
// Release 环境:仅在未设置专属颜色时显示
if (!hasSignatureColor) {
EPSignatureColorGuideView *guideView = [[EPSignatureColorGuideView alloc] init];
guideView.onColorConfirmed = ^(NSString *hexColor) {
[EPEmotionColorStorage saveUserSignatureColor:hexColor];
NSLog(@"[AppDelegate] 用户选择专属颜色: %@", hexColor);
};
[guideView showInWindow:keyWindow];
}
#endif
}
- (void)toLoginPage {
// 使用新的 Swift 登录页面
EPLoginViewController *lvc = [[EPLoginViewController alloc] init];
BaseNavigationController *navigationController =
[[BaseNavigationController alloc] initWithRootViewController:lvc];
navigationController.modalPresentationStyle = UIModalPresentationFullScreen;
self.window.rootViewController = navigationController;
}
- (void)toHomeTabbarPage {
EPTabBarController *epTabBar = [EPTabBarController create];
[epTabBar refreshTabBarWithIsLogin:YES];
UIWindow *window = [self getKeyWindow];
if (window) {
window.rootViewController = epTabBar;
[window makeKeyAndVisible];
}
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
[self getAdvertisingTrackingAuthority];
[[NSNotificationCenter defaultCenter]postNotificationName:@"kAppDidBecomeActive" object:nil];
}
- (void)getAdvertisingTrackingAuthority {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (@available(iOS 14, *)) {
ATTrackingManagerAuthorizationStatus status = ATTrackingManager.trackingAuthorizationStatus;
switch (status) {
case ATTrackingManagerAuthorizationStatusDenied:
break;
case ATTrackingManagerAuthorizationStatusAuthorized:
break;
case ATTrackingManagerAuthorizationStatusNotDetermined: {
// NSLog(@"用户未做选择或未弹窗IDFA");
//请求弹出用户授权框只会在程序运行是弹框1次除非卸载app重装通地图、相机等权限弹框一样
[ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
// NSLog(@"app追踪IDFA权限%lu",(unsigned long)status);
}];
}
break;
default:
break;
}
}
});
}
@end

View File

@@ -1,110 +0,0 @@
//
// APIConfig.swift
// YuMi
//
// Created by AI on 2025-10-09.
// Copyright © 2025 YuMi. All rights reserved.
//
import Foundation
/// API 域名配置类
/// 使用 XOR + Base64 双重混淆防止反编译
@objc class APIConfig: NSObject {
// MARK: - Private Properties
/// XOR 加密密钥
private static let xorKey: UInt8 = 77
/// RELEASE 环境域名(加密后)
/// 原始域名https://api.epartylive.com
private static let releaseEncodedParts: [String] = [
"JTk5PT53YmI=", // https:// (XOR 后 Base64)
"LD0kYw==", // api. (XOR 后 Base64)
"KD0sPzk0ISQ7KGMuIiA=", // epartylive.com (XOR 后 Base64)
]
// MARK: - Public Methods
/// 获取 API 基础域名
/// - Returns: 根据编译环境返回对应的域名
@objc static func baseURL() -> String {
#if DEBUG
// DEV 环境:临时使用硬编码(等 Bridging 修复后再改回 HttpRequestHelper
// TODO: 修复后改为 return HttpRequestHelper.getHostUrl()
return getDevBaseURL()
#else
// RELEASE 环境:使用动态生成的新域名
let url = decodeURL(from: releaseEncodedParts)
// 验证解密结果
if url.isEmpty || !url.hasPrefix("http") {
NSLog("[APIConfig] 警告:域名解密失败,使用备用域名")
return backupURL()
}
return url
#endif
}
/// 获取 DEV 环境域名(临时方案)
/// - Returns: DEV 域名
private static func getDevBaseURL() -> String {
// 从 UserDefaults 读取(原 HttpRequestHelper 的逻辑)
#if DEBUG
let isProduction = UserDefaults.standard.string(forKey: "kIsProductionEnvironment")
if isProduction == "YES" {
return "https://api.epartylive.com" // 正式环境
} else {
return "https://test-api.yourdomain.com" // 测试环境(请替换为实际测试域名)
}
#else
return "https://api.epartylive.com"
#endif
}
/// 备用域名(降级方案)
/// - Returns: 原域名(仅在解密失败时使用)
@objc static func backupURL() -> String {
return getDevBaseURL()
}
// MARK: - Private Methods
/// 解密域名
/// - Parameter parts: 加密后的域名片段数组
/// - Returns: 解密后的完整域名
private static func decodeURL(from parts: [String]) -> String {
let decoded = parts.compactMap { part -> String? in
guard let data = Data(base64Encoded: part) else {
NSLog("[APIConfig] Base64 解码失败: \(part)")
return nil
}
let xored = data.map { $0 ^ xorKey }
return String(bytes: xored, encoding: .utf8)
}
let result = decoded.joined()
#if DEBUG
NSLog("[APIConfig] 解密后的域名: \(result)")
#endif
return result
}
}
// MARK: - Debug Helper
#if DEBUG
extension APIConfig {
/// 测试方法:验证域名加密/解密是否正常
@objc static func testEncryption() {
print("=== APIConfig 加密测试 ===")
print("Release 域名: \(decodeURL(from: releaseEncodedParts))")
print("当前环境域名: \(baseURL())")
print("备用域名: \(backupURL())")
}
}
#endif

View File

@@ -1,79 +0,0 @@
//
// DJDKMIMOMColor.h
// YUMI
//
// Created by YUMI on 2021/9/9.
//
#import <Foundation/Foundation.h>
#define UIColorFromRGB(rgbValue) [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 green:((float)((rgbValue & 0xFF00) >> 8))/255.0 blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0]
#define UIColorRGBAlpha(rgbValue,a) [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 green:((float)((rgbValue & 0xFF00) >> 8))/255.0 blue:((float)(rgbValue & 0xFF))/255.0 alpha:(a)]
NS_ASSUME_NONNULL_BEGIN
@interface DJDKMIMOMColor : NSObject
/// 主题色0x9682FF
+ (UIColor *)appMainColor;
///强调色 #248CFE
+ (UIColor *)appEmphasizeColor;
///强调色1 0xBF36FF
+ (UIColor *)appEmphasizeColor1;
///强调色2 0xFB486A
+ (UIColor *)appEmphasizeColor2;
/* ------页面相关颜色 START------ */
/// view的背景色 0xF3F5FA
+ (UIColor *)appBackgroundColor;
/// cell的背景色 0xFFFFFF
+ (UIColor *)appCellBackgroundColor;
///正文颜色 0x333333
+ (UIColor *)mainTextColor;
/// 二级文字颜色 0x666666
+ (UIColor *)secondTextColor;
///三级文字的颜色 0x999999
+ (UIColor *)textThirdColor;
///分割线的颜色 0xE8E8E8
+ (UIColor *)dividerColor;
/* ------页面相关颜色 END------ */
/* ------Button 相关颜色 START------ */
/// button 可用 渐变色的开始 0xFFA936
+ (UIColor *)confirmButtonGradientStartColor;
/// button 可用 渐变色的中间 #9CB3FF
+ (UIColor *)confirmButtonGradientMiddleColor;
/// button 可用 渐变色的开始 0xFFCB47
+ (UIColor *)confirmButtonGradientEndColor;
/// 确定的按钮文字颜色 #FFFFFF
+ (UIColor *)confirmButtonTextColor;
/// 取消按钮 渐变色的开始 0xF7DDBF
+ (UIColor *)cancelButtonGradientStartColor;
/// 取消按钮 渐变色的结束 0xF7E8C4
+ (UIColor *)cancelButtonGradientEndColor;
/// 取消的按钮文字颜色 0xFFA936
+ (UIColor *)cancelButtonTextColor;
/// 取消按钮单一普通背景色 0xFFCE4E
+ (UIColor *)cancelButtonNormalBgColor;
/// 按钮不可点击背景色 0xD2D5D7
+ (UIColor *)disableButtonColor;
/// 按钮不可点击文字颜色 0xF9F9F9
+ (UIColor *)disableButtonTextColor;
/* ------Button 相关颜色 END------ */
/* ------弹窗相关颜色 START------ */
+ (UIColor *)alertBackgroundColor;
+ (UIColor *)alertTitleColor;
+ (UIColor *)alertMessageColor;
+ (UIColor *)actionSeparatorColor;
/* ------弹窗相关颜色 END------ */
///tabbar 没有点击的时候颜色 0x333333, 0.4
+ (UIColor *)tabbarNormalColor;
/// tabbar的View的color 0xFFFFFF
+ (UIColor *)tabbarViewColor;
+ (UIColor *)colorWithHexString:(NSString *)hexString;
+ (UIColor *)inputTextColor;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,177 +0,0 @@
//
// DJDKMIMOMColor.m
// YUMI
//
// Created by YUMI on 2021/9/9.
//
#import "DJDKMIMOMColor.h"
@implementation DJDKMIMOMColor
/// 主题色0x9682FF
+ (UIColor *)appMainColor {
return UIColorFromRGB(0x9682FF);
}
///强调色 #248CFE
+ (UIColor *)appEmphasizeColor {
return UIColorFromRGB(0x248CFE);
}
///强调色1 0xBF36FF
+ (UIColor *)appEmphasizeColor1 {
return UIColorFromRGB(0xBF36FF);
}
///强调色2 0xFB486A
+ (UIColor *)appEmphasizeColor2 {
return UIColorFromRGB(0xFB486A);
}
/* ------页面相关颜色 START------ */
/// view的背景色 0xF3F5FA
+ (UIColor *)appBackgroundColor {
return UIColorFromRGB(0xF3F5FA);
}
/// cell的背景色 0xFFFFFF
+ (UIColor *)appCellBackgroundColor {
return UIColorFromRGB(0xFFFFFF);
}
///正文颜色 0x333333
+ (UIColor *)mainTextColor {
return UIColorFromRGB(0x161958);
}
/// 二级文字颜色 0x666666
+ (UIColor *)secondTextColor {
return UIColorFromRGB(0x8A8CAB);
}
///三级文字的颜色 0x999999
+ (UIColor *)textThirdColor {
return UIColorFromRGB(0xBABBCD);
}
///分割线的颜色 0xE8E8E8
+ (UIColor *)dividerColor {
return UIColorFromRGB(0xE8E8E8);
}
/* ------页面相关颜色 END------ */
/* ------Button 相关颜色 START------ */
/// button 可用 渐变色的开始 0x3CAAFF
+ (UIColor *)confirmButtonGradientStartColor {
return UIColorFromRGB(0x13E2F5);
}
/// button 可用 渐变色的开始 0xB176FF
+ (UIColor *)confirmButtonGradientEndColor {
return UIColorFromRGB(0xCC66FF);
}
/// 确定的按钮文字颜色 #FFFFFF
+ (UIColor *)confirmButtonTextColor {
return UIColorFromRGB(0xFFFFFF);
}
/// 取消按钮 渐变色的开始 0xF7DDBF
+ (UIColor *)cancelButtonGradientStartColor {
return UIColorFromRGB(0xCEEFFD);
}
/// button 可用 渐变色的中间 #9CB3FF
+ (UIColor *)confirmButtonGradientMiddleColor {
return UIColorFromRGB(0xf9CB3FF);
}
/// 取消按钮 渐变色的结束 0xF7E8C4
+ (UIColor *)cancelButtonGradientEndColor {
return UIColorFromRGB(0xD2F4F4);
}
/// 取消的按钮文字颜色 0xFFA936
+ (UIColor *)cancelButtonTextColor {
return UIColorFromRGB(0x5FCCE4);
}
/// 取消按钮单一普通背景色 0xFFCE4E
+ (UIColor *)cancelButtonNormalBgColor {
return UIColorFromRGB(0xCEEFFD);
}
/// 按钮不可点击背景色 0xD2D5D7
+ (UIColor *)disableButtonColor {
return UIColorFromRGB(0xCEEFFD);
}
/// 按钮不可点击文字颜色 0xF9F9F9
+ (UIColor *)disableButtonTextColor {
return UIColorFromRGB(0xB3B3C3);
}
/* ------Button 相关颜色 END------ */
/* ------弹窗相关颜色 START------ */
+ (UIColor *)alertBackgroundColor {
return UIColorFromRGB(0xFFFFFF);
}
+ (UIColor *)alertTitleColor {
return UIColorFromRGB(0x333333);
}
+ (UIColor *)alertMessageColor {
return UIColorFromRGB(0x333333);
}
+ (UIColor *)actionSeparatorColor {
return UIColorFromRGB(0xF0F0F0);
}
/* ------弹窗相关颜色 END------ */
///tabbar 没有点击的时候颜色 0x333333, 0.4
+ (UIColor *)tabbarNormalColor {
return UIColorRGBAlpha(0x333333, 0.4);
}
/// tabbar的View的color 0xFFFFFF
+ (UIColor *)tabbarViewColor {
return UIColorFromRGB(0xFFFFFF);
}
+ (UIColor *)colorWithHexString: (NSString *) hexString {
if (hexString.length == 0) {
return [UIColor blackColor];
}
NSString *colorString = [[hexString stringByReplacingOccurrencesOfString: @"#" withString: @""] uppercaseString];
CGFloat alpha, red, blue, green;
switch ([colorString length]) {
case 3: // #RGB
alpha = 1.0f;
red = [self colorComponentFrom: colorString start: 0 length: 1];
green = [self colorComponentFrom: colorString start: 1 length: 1];
blue = [self colorComponentFrom: colorString start: 2 length: 1];
break;
case 4: // #ARGB
alpha = [self colorComponentFrom: colorString start: 0 length: 1];
red = [self colorComponentFrom: colorString start: 1 length: 1];
green = [self colorComponentFrom: colorString start: 2 length: 1];
blue = [self colorComponentFrom: colorString start: 3 length: 1];
break;
case 6: // #RRGGBB
alpha = 1.0f;
red = [self colorComponentFrom: colorString start: 0 length: 2];
green = [self colorComponentFrom: colorString start: 2 length: 2];
blue = [self colorComponentFrom: colorString start: 4 length: 2];
break;
case 8: // #AARRGGBB
alpha = [self colorComponentFrom: colorString start: 0 length: 2];
red = [self colorComponentFrom: colorString start: 2 length: 2];
green = [self colorComponentFrom: colorString start: 4 length: 2];
blue = [self colorComponentFrom: colorString start: 6 length: 2];
break;
default:
[NSException raise:@"Invalid color value" format: @"Color value %@ is invalid. It should be a hex value of the form #RBG, #ARGB, #RRGGBB, or #AARRGGBB", hexString];
break;
}
return [UIColor colorWithRed: red green: green blue: blue alpha: alpha];
}
+ (CGFloat) colorComponentFrom: (NSString *) string start: (NSUInteger) start length: (NSUInteger) length {
NSString *substring = [string substringWithRange: NSMakeRange(start, length)];
NSString *fullHex = length == 2 ? substring : [NSString stringWithFormat: @"%@%@", substring, substring];
unsigned hexComponent;
[[NSScanner scannerWithString: fullHex] scanHexInt: &hexComponent];
return hexComponent / 255.0;
}
///输入框的文本颜色 #1F1A4E
+ (UIColor *)inputTextColor {
return [self colorWithHexString:@"#1F1A4E"];
}
@end

View File

@@ -1,60 +0,0 @@
//
// TTActionSheetConfig.h
// AFNetworking
//
// Created by lee on 2019/5/23.
// action sheet item 配置
#import <UIKit/UIKit.h>
typedef enum : NSUInteger {
TTItemSelectNormal,
TTItemSelectHighLight,
} TTItemSelectType;
NS_ASSUME_NONNULL_BEGIN
typedef void(^TTActionSheetClickAction)(void);
@interface TTActionSheetConfig : NSObject
/** 标题 */
@property (nonatomic, copy) NSString *title;
/**
标题颜色
*/
@property (nonatomic, strong) UIColor *titleColor;
/** 是否选中 */
@property (nonatomic, assign) TTItemSelectType type;
/** 点击事件 */
@property (nonatomic, copy) TTActionSheetClickAction clickAction;
@property(nonatomic, assign) BOOL displayMoliCoin;
/**
构建 actionSheet item 实例
@param title 标题
@param clickAction 点击事件
@return item 实例
*/
+ (TTActionSheetConfig *)normalTitle:(NSString *)title
clickAction:(TTActionSheetClickAction)clickAction;
+ (TTActionSheetConfig *)normalTitle:(NSString *)title
selectColorType:(TTItemSelectType)type clickAction:(TTActionSheetClickAction)clickAction;
/// 构建实例
/// @param title 标题
/// @param textColor 颜色
/// @param handler 事件处理
+ (TTActionSheetConfig *)actionWithTitle:(NSString *)title
color:(UIColor *)textColor
handler:(TTActionSheetClickAction)handler;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,49 +0,0 @@
//
// TTActionSheetConfig.m
// AFNetworking
//
// Created by lee on 2019/5/23.
//
#import "TTActionSheetConfig.h"
#import "DJDKMIMOMColor.h"
@implementation TTActionSheetConfig
/**
构建 actionSheet item 实例
@param title 标题
@param clickAction 点击事件
@return item 实例
*/
+ (TTActionSheetConfig *)normalTitle:(NSString *)title clickAction:(TTActionSheetClickAction)clickAction {
return [self normalTitle:title selectColorType:TTItemSelectNormal clickAction:clickAction];
}
+ (TTActionSheetConfig *)normalTitle:(NSString *)title selectColorType:(TTItemSelectType)type clickAction:(TTActionSheetClickAction)clickAction {
UIColor *color = type == TTItemSelectHighLight ? [DJDKMIMOMColor alertTitleColor] : [DJDKMIMOMColor alertTitleColor];
TTActionSheetConfig *config = [self actionWithTitle:title color:color handler:clickAction];
config.type = type;
return config;
}
+ (TTActionSheetConfig *)actionWithTitle:(NSString *)title
color:(UIColor *)textColor
handler:(TTActionSheetClickAction)handler {
TTActionSheetConfig *config = [[TTActionSheetConfig alloc] init];
config.type = TTItemSelectNormal;
config.title = title;
config.titleColor = textColor;
config.clickAction = handler;
return config;
}
@end

View File

@@ -1,30 +0,0 @@
//
// TTAlertButtonConfig.h
// YM_TTChatViewKit
//
// Created by lee on 2019/5/21.
// Copyright © 2023 YUMI. All rights reserved.
// alert 按钮配置
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface TTAlertButtonConfig : NSObject
/** 按钮标题 */
@property (nonatomic, copy) NSString *title;
/** 按钮字体 */
@property (nonatomic, strong) UIFont *font;
/** 按钮字体颜色 */
@property (nonatomic, strong) UIColor *titleColor;
/** 背景色 */
@property (nonatomic, strong) UIColor *backgroundColor;
/** 背景图 */
@property (nonatomic, strong) UIImage *backgroundImage;
/** 圆角 */
@property (nonatomic, assign) CGFloat cornerRadius;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,13 +0,0 @@
//
// TTAlertButtonConfig.m
// YM_TTChatViewKit
//
// Created by lee on 2019/5/21.
// Copyright © 2023 YUMI. All rights reserved.
//
#import "TTAlertButtonConfig.h"
@implementation TTAlertButtonConfig
@end

View File

@@ -1,86 +0,0 @@
//
// TTAlertConfig.h
// YM_TTChatViewKit
//
// Created by lee on 2019/5/20.
// Copyright © 2023 YUMI. All rights reserved.
// alert 配置
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "TTAlertButtonConfig.h"
#import "TTAlertMessageAttributedConfig.h"
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, TTAlertActionStyle) {
TTAlertActionConfirmStyle = 0, // 只有确定按钮
TTAlertActionCancelStyle = 1, // 只有取消按钮
TTAlertActionBothStyle = 2, // 全部按钮
};
@interface TTAlertConfig : NSObject
// 按钮显示风格
@property (nonatomic, assign) TTAlertActionStyle actionStyle;
// 容器背景设置
@property (nonatomic, strong) UIColor *backgroundColor;
// 标题设置
@property (nonatomic, copy) NSString *title;
@property (nonatomic, strong) UIFont *titleFont;
@property (nonatomic, strong) UIColor *titleColor;
// 内容设置
@property (nonatomic, copy) NSString *message;
@property (nonatomic, strong) UIFont *messageFont;
@property (nonatomic, strong) UIColor *messageColor;
/** 内容的行间距,默认不设置,当值为 0 或负数时无效 */
@property (nonatomic, assign) CGFloat messageLineSpacing;
@property(nonatomic, assign) NSTextAlignment messageTextAlignment;
/** 内容富文本配置 */
@property (nonatomic, strong) NSArray<TTAlertMessageAttributedConfig *> *messageAttributedConfig;
///配置内容的富文本
@property (nonatomic,strong) NSMutableAttributedString *messageAttributed;
// 取消按钮配置
@property (nonatomic, strong) TTAlertButtonConfig *cancelButtonConfig;
// 确定按钮配置
@property (nonatomic, strong) TTAlertButtonConfig *confirmButtonConfig;
/**
背景蒙层的透明度
@Description 默认是 000000 黑色 0.3 alpha
*/
@property (nonatomic, assign) CGFloat maskBackgroundAlpha;
// 圆角
@property (nonatomic, assign) CGFloat cornerRadius;
// 点击蒙层是否消失默认YES
@property (nonatomic, assign) BOOL shouldDismissOnBackgroundTouch;
/**
点击确定取消按钮时禁止弹窗自动消失默认NO
@discussion 若值为 YES需要主动调用 [TTPopup dismiss] 消除弹窗
*/
@property (nonatomic, assign) BOOL disableAutoDismissWhenClickButton;
/**
重复弹窗过滤默认NO
@discussion 设置过滤时,队列中将不会出现相同过滤标识的弹窗
过滤标识通过 #<filterIdentifier> 设置
*/
@property (nonatomic, assign) BOOL shouldFilterPopup;
/**
过滤标识默认nil
*/
@property (nonatomic, copy) NSString *filterIdentifier;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,90 +0,0 @@
//
// TTAlertConfig.m
// YM_TTChatViewKit
//
// Created by lee on 2019/5/20.
// Copyright © 2023 YUMI. All rights reserved.
//
#import "TTAlertConfig.h"
#import "UIImage+Utils.h"
#import "DJDKMIMOMColor.h"
static CGFloat kAlertTitleFont = 18.f;
static CGFloat kAlertButtonFont = 15.f;
static CGFloat kAlertMessageFont = 15.f;
static CGFloat kAlertCornerRadius = 12.f;
static CGFloat kAlertBackgroundColorAlpha = 0.3;
static CGFloat kAlertMessageFontLineSpace = -1;
static CGFloat kAlertButtonCornerRadius = 8.f;
@implementation TTAlertConfig
- (instancetype)init {
self = [super init];
if (self) {
_backgroundColor = [DJDKMIMOMColor alertBackgroundColor];
//背景颜色
kAlertTitleFont = 16.f;
kAlertCornerRadius = 14.f;
kAlertButtonCornerRadius = 19.f;
_actionStyle = TTAlertActionBothStyle;
// title
_title = @"";// 标题
_titleFont = [UIFont fontWithName:@"PingFangSC-Medium" size:kAlertTitleFont];// 字体
_titleColor = [DJDKMIMOMColor alertTitleColor];// 颜色
// message
_message = @"";
_messageFont = [UIFont systemFontOfSize:kAlertMessageFont];// 内容
_messageColor = [DJDKMIMOMColor alertMessageColor];// 颜色
_messageLineSpacing = kAlertMessageFontLineSpace;// 字体行间距
_messageAttributedConfig = @[];// 自定义富文本样式
_messageTextAlignment = NSTextAlignmentCenter;
// cancel button
_cancelButtonConfig = [[TTAlertButtonConfig alloc] init];
_cancelButtonConfig.title = YMLocalizedString(@"XPRoomSettingInputView4");// 取消按钮
_cancelButtonConfig.font = [UIFont systemFontOfSize:kAlertButtonFont];// 按钮字体
_cancelButtonConfig.titleColor = [DJDKMIMOMColor cancelButtonTextColor];// 字体颜色
_cancelButtonConfig.backgroundImage = [UIImage gradientColorImageFromColors:@[[DJDKMIMOMColor cancelButtonGradientStartColor], [DJDKMIMOMColor cancelButtonGradientEndColor]] gradientType:GradientTypeLeftToRight imgSize:CGSizeMake(10, 10)];
_cancelButtonConfig.cornerRadius = kAlertButtonCornerRadius;// 按钮背景图
// confirm button
_confirmButtonConfig = [[TTAlertButtonConfig alloc] init];
_confirmButtonConfig.title = YMLocalizedString(@"TTAlertConfig0");
_confirmButtonConfig.font = [UIFont systemFontOfSize:kAlertButtonFont];
_confirmButtonConfig.titleColor = [DJDKMIMOMColor confirmButtonTextColor];
_confirmButtonConfig.backgroundImage = [UIImage gradientColorImageFromColors:@[UIColorFromRGB(0xE29030), UIColorFromRGB(0xFCC074)]
gradientType:GradientTypeLeftToRight
imgSize:CGSizeMake(10, 10)];
_confirmButtonConfig.cornerRadius = kAlertButtonCornerRadius;
// // 创建渐变图层
// CAGradientLayer *gradientLayer = [CAGradientLayer layer];
// gradientLayer.colors = @[(__bridge id)UIColorFromRGB(0xE29030).CGColor,
// (__bridge id)UIColorFromRGB(0xFCC074).CGColor];
// gradientLayer.startPoint = CGPointMake(0.0, 0.0); // 顶部中央
// gradientLayer.endPoint = CGPointMake(0.0, 1.0); // 底部中央
// gradientLayer.frame = CGRectMake(0, 0, 120, 38); // 设置渐变图层大小
//
// // 将渐变图层添加到按钮图层
// [_previewActionButton.layer insertSublayer:gradientLayer atIndex:0];
_cornerRadius = kAlertCornerRadius;// 默认圆角
_shouldDismissOnBackgroundTouch = YES;// 点击蒙层是否消失
// mask default 0.3 black
_maskBackgroundAlpha = kAlertBackgroundColorAlpha; // alert 背景色
_disableAutoDismissWhenClickButton = NO;
}
return self;
}
@end

View File

@@ -1,34 +0,0 @@
//
// TTAlertContentAttributedConfig.h
// YM_TTChatViewKit
//
// Created by lee on 2019/5/21.
// Copyright © 2023 YUMI. All rights reserved.
// alert 提示内容富文本配置
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface TTAlertMessageAttributedConfig : NSObject
/** 富文本字段需要特殊显示的文本 */
@property (nonatomic, copy) NSString *text;
/** 颜色 */
@property (nonatomic, strong) UIColor *color;
/** 字体 */
@property (nonatomic, strong) UIFont *font;
/**
目标文本指定位置,一旦设置则无视 text 的值
@discussion 内容文本中出现相同的目标文本时,可通过设置 range 精确指定位置
*/
@property (nonatomic, assign) NSRange range;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,13 +0,0 @@
//
// TTAlertContentAttributedConfig.m
// YM_TTChatViewKit
//
// Created by lee on 2019/5/21.
// Copyright © 2023 YUMI. All rights reserved.
//
#import "TTAlertMessageAttributedConfig.h"
@implementation TTAlertMessageAttributedConfig
@end

View File

@@ -1,38 +0,0 @@
//
// TTPopupConstants.h
// YM_TTChatViewKit
//
// Created by lvjunhang on 2019/5/21.
// Copyright © 2023 YUMI. All rights reserved.
//
#import <UIKit/UIKit.h>
/**
Popup 组件通用回调
*/
typedef void(^TTPopupCompletionHandler)(void);
/**
弹窗类型
- TTPopupStyleAlert: Alert
- TTPopupStyleActionSheet: ActionSheet
*/
typedef NS_ENUM(NSUInteger, TTPopupStyle) {
TTPopupStyleAlert = 0,
TTPopupStyleActionSheet
};
/**
弹窗优先级
- TTPopupPriorityNormal: 普通
- TTPopupPriorityHigh: 高
- TTPopupPriorityRequired: 必须
*/
typedef NS_ENUM(NSUInteger, TTPopupPriority) {
TTPopupPriorityNormal = 0,
TTPopupPriorityHigh,
TTPopupPriorityRequired
};

View File

@@ -1,24 +0,0 @@
//
// TTPopupManagerService.h
// YM_TTChatViewKit
//
// Created by lvjunhang on 2019/5/21.
// Copyright © 2023 YUMI. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "TTPopupManagerServiceProtocol.h"
NS_ASSUME_NONNULL_BEGIN
@interface TTPopupManagerService : NSObject<TTPopupManagerServiceProtocol>
/**
当前显示的弹窗服务
*/
@property (nonatomic, strong) id<TTPopupServiceProtocol> currentPopupService;
+ (instancetype)sharedInstance;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,200 +0,0 @@
//
// TTPopupManagerService.m
// YM_TTChatViewKit
//
// Created by lvjunhang on 2019/5/21.
// Copyright © 2023 YUMI. All rights reserved.
//
#import "TTPopupManagerService.h"
#import <FFPopup/FFPopup.h>
@interface TTPopupManagerService ()
@property (nonatomic, strong) NSMutableArray< id<TTPopupServiceProtocol> > *queue;
/**
当前是否正在显示状态
*/
@property (nonatomic, assign, getter=isShowingPopup) BOOL showingPopup;
@end
@implementation TTPopupManagerService
#pragma mark - Life Cycle
+ (instancetype)sharedInstance {
static TTPopupManagerService *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (instancetype)init {
if (self = [super init]) {
_queue = [NSMutableArray array];
}
return self;
}
#pragma mark - TTPopupManagerServiceProtocol
- (void)addPopupService:(id<TTPopupServiceProtocol>)service {
if (![service conformsToProtocol:@protocol(TTPopupServiceProtocol)]) {
return;
}
if ([_queue containsObject:service]) {
return;
}
NSInteger insertPosition = [self insertPositionForPopupService:service];
if (insertPosition == NSNotFound) {
return;
}
[_queue insertObject:service atIndex:insertPosition];
//当前没有弹窗显示且队列只有一个元素时,显示弹窗
if (_currentPopupService == nil && _queue.count == 1) {
[self showPopup];
}
}
- (void)removePopupService {
//防止当弹窗还未显示完成,在显示过程中频繁调用 dismiss
//使得 _currentPopupService 被清空,导致弹窗无法消失,从而假死现象
if (!self.isShowingPopup) {
return;
}
if (_currentPopupService == nil) {
return;
}
if (_queue.count > 0) {
[_queue removeObjectAtIndex:0];
}
[FFPopup dismissPopupForView:_currentPopupService.contentView animated:YES];
_currentPopupService = nil;
}
/**
点击蒙层时移除队列中的数据源
@discussion 注意无需调用 dismissPopupForView
*/
- (void)removeSourceWhenTouchMaskView {
if (_currentPopupService == nil) {
return;
}
if (_queue.count > 0) {
[_queue removeObjectAtIndex:0];
}
_currentPopupService = nil;
}
#pragma mark - Private Methods
/**
显示弹窗
*/
- (void)showPopup {
if (self.isShowingPopup) {
return;
}
if (_currentPopupService) {
return;
}
if (_queue.count == 0) {
return;
}
id<TTPopupServiceProtocol> popupService = _queue.firstObject;
if (![popupService conformsToProtocol:@protocol(TTPopupServiceProtocol)]) {
return;
}
_currentPopupService = popupService;
FFPopupHorizontalLayout horizontalLayout = FFPopupHorizontalLayout_Center;
FFPopupVerticalLayout verticalLayout = FFPopupVerticalLayout_Center;
FFPopupShowType showType = (FFPopupShowType)popupService.showType;
FFPopupDismissType dismissType = FFPopupDismissType_GrowOut;
if (popupService.style == TTPopupStyleActionSheet) {
verticalLayout = FFPopupVerticalLayout_Bottom;
showType = FFPopupShowType_SlideInFromBottom;
dismissType = FFPopupDismissType_SlideOutToBottom;
}
FFPopup *popup = [FFPopup popupWithContentView:popupService.contentView];
popup.showType = showType;
popup.dismissType = dismissType;
popup.maskType = FFPopupMaskType_Dimmed;
popup.dimmedMaskAlpha = popupService.maskBackgroundAlpha;
popup.shouldDismissOnBackgroundTouch = popupService.shouldDismissOnBackgroundTouch;
[popup showWithLayout:FFPopupLayoutMake(horizontalLayout, verticalLayout) duration:0.0];
__weak typeof(self) weakSelf = self;
// 不管是调用dismissPopupForView:animated: 还是‘点击蒙层消除’,最终都会走到这里
// 适合在此展示队列中下一个弹窗
// 通过 _currentPopupService 是否为空可以判断是哪种消除方式
popup.didFinishDismissingBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
BOOL isDismissOnBackgroundTouch = strongSelf.currentPopupService != nil;
if (isDismissOnBackgroundTouch) {
// ‘点击蒙层消除’时,在展现下一个弹窗前移除数据源
[self removeSourceWhenTouchMaskView];
}
if (popupService.didFinishDismissHandler) {
popupService.didFinishDismissHandler(isDismissOnBackgroundTouch);
}
// 弹窗消除结束,更新状态
strongSelf.showingPopup = NO;
// 显示下一个弹窗
[strongSelf showPopup];
};
popup.didFinishShowingBlock = ^{
// 开始弹窗,更新状态
self.showingPopup = YES;
if (popupService.didFinishShowingHandler) {
popupService.didFinishShowingHandler();
}
};
}
/**
弹窗将要插入队列的位置
@param service 弹窗服务实例
@return 队列的位置
*/
- (NSInteger)insertPositionForPopupService:(id<TTPopupServiceProtocol>)service {
__block NSInteger result = NSNotFound;
if (service == nil) {
return result;
}
if (_queue.count == 0) {
return 0;
}
// 设置重复弹窗过滤
if (service.shouldFilterPopup && service.filterIdentifier.length > 0) {
BOOL filterFlag = NO;
for (id<TTPopupServiceProtocol> serv in _queue) {
if ([serv.filterIdentifier isEqualToString:service.filterIdentifier]) {
filterFlag = YES;
break;
}
}
if (filterFlag) {
return result;
}
}
[_queue enumerateObjectsUsingBlock:^(id<TTPopupServiceProtocol> _Nonnull model, NSUInteger idx, BOOL * _Nonnull stop) {
// 找到队中比当前弹窗优先级低的位置
if (service.priority > model.priority) {
result = idx;
*stop = YES;
}
}];
// 如果没有合适的位置,则将新的弹窗放在队尾
result = MIN(result, _queue.count);
return result;
}
@end

View File

@@ -1,25 +0,0 @@
//
// TTPopupManagerServiceProtocol.h
// YM_TTChatViewKit
//
// Created by lvjunhang on 2019/5/21.
// Copyright © 2023 YUMI. All rights reserved.
//
#import "TTPopupServiceProtocol.h"
@protocol TTPopupManagerServiceProtocol <NSObject>
/**
添加一个弹窗
@param service 服从弹窗服务的实例
*/
- (void)addPopupService:(id<TTPopupServiceProtocol>)service;
/**
移除一个弹窗
*/
- (void)removePopupService;
@end

View File

@@ -1,22 +0,0 @@
//
// TTPopupService.h
// YM_TTChatViewKit
//
// Created by lvjunhang on 2019/5/21.
// Copyright © 2023 YUMI. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "TTPopupServiceProtocol.h"
// TTPopupConfig 是 TTPopupService 的别称
// 具体信息见 TTPopupService
#define TTPopupConfig TTPopupService
NS_ASSUME_NONNULL_BEGIN
@interface TTPopupService : NSObject<TTPopupServiceProtocol>
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,36 +0,0 @@
//
// TTPopupService.m
// YM_TTChatViewKit
//
// Created by lvjunhang on 2019/5/21.
// Copyright © 2023 YUMI. All rights reserved.
//
#import "TTPopupService.h"
@implementation TTPopupService
@synthesize style = _style;
@synthesize priority = _priority;
@synthesize contentView = _contentView;
@synthesize maskBackgroundAlpha = _maskBackgroundAlpha;
@synthesize shouldDismissOnBackgroundTouch = _shouldDismissOnBackgroundTouch;
@synthesize didFinishDismissHandler = _didFinishDismissHandler;
@synthesize didFinishShowingHandler = _didFinishShowingHandler;
@synthesize shouldFilterPopup = _shouldFilterPopup;
@synthesize filterIdentifier = _filterIdentifier;
@synthesize showType = _showType;
- (instancetype)init {
self = [super init];
if (self) {
_style = TTPopupStyleAlert;
_priority = TTPopupPriorityNormal;
_maskBackgroundAlpha = 0.5;
_shouldDismissOnBackgroundTouch = YES;
_shouldFilterPopup = NO;
_showType = TTPopupShowTypeDefault;
}
return self;
}
@end

View File

@@ -1,78 +0,0 @@
//
// TTPopupServiceProtocol.h
// YM_TTChatViewKit
//
// Created by lvjunhang on 2019/5/21.
// Copyright © 2023 YUMI. All rights reserved.
//
#import "TTPopupConstants.h"
typedef NS_ENUM(NSUInteger, TTPopupShowType) {
TTPopupShowType_FadeIn = 1,
TTPopupShowTypeDefault = 8,
};
@class UIView;
@protocol TTPopupServiceProtocol <NSObject>
/**
弹窗样式默认TTPopupStyleAlert
*/
@property (nonatomic, assign) TTPopupStyle style;
/**
弹窗优先级权重默认TTPopupPriorityNormal
@discussion 权重越高在弹窗队列的优先级越高,即优先弹出;相同权重按先来后到原则
*/
@property (nonatomic, assign) TTPopupPriority priority;
/**
自定义视图内容默认nil
@discussion 如果未配置,或 contentView 未继承自 UIView 及其子类,将忽略该弹窗
*/
@property (nonatomic, strong) UIView *contentView;
/**
背景蒙层透明度默认0x000000 0.3 alpha
@discussion 由于第三方原因,暂不支持蒙层颜色修改
*/
@property (nonatomic, assign) CGFloat maskBackgroundAlpha;
/**
点击蒙层是否消除弹窗默认YES
*/
@property (nonatomic, assign) BOOL shouldDismissOnBackgroundTouch;
/**
弹窗消失回调isDismissOnBackgroundTouch 区分是否点击蒙层触发
*/
@property (nonatomic, copy) void (^didFinishDismissHandler)(BOOL isDismissOnBackgroundTouch);
/**
弹窗显示成功回调
*/
@property (nonatomic, copy) void (^didFinishShowingHandler)(void);
/**
重复弹窗过滤默认NO
@discussion 设置过滤时,队列中将不会出现相同过滤标识的弹窗
过滤标识通过 #<filterIdentifier> 设置
*/
@property (nonatomic, assign) BOOL shouldFilterPopup;
/**
过滤标识默认nil
*/
@property (nonatomic, copy) NSString *filterIdentifier;
/**
显示动画类型, 默认是 default
*/
@property (nonatomic, assign) TTPopupShowType showType;
@end

View File

@@ -1,126 +0,0 @@
//
// TTPopup.h
// YM_TTChatViewKit
//
// Created by lvjunhang on 2019/5/22.
// Copyright © 2023 YUMI. All rights reserved.
// 弹窗工具类
#import <Foundation/Foundation.h>
#import "TTPopupConstants.h"
#import "TTAlertConfig.h"
#import "TTActionSheetConfig.h"
#import "TTPopupService.h"
NS_ASSUME_NONNULL_BEGIN
@class UIView;
@interface TTPopup : NSObject
#pragma mark Alert
/**
显示 alert 弹窗
@discussion 显示四个内容:默认标题‘提示’,提示内容,取消按钮,确认按钮
@param message 提示内容,不能为空(⊙o⊙)哦
@param confirmHandler 确认操作
@param cancelHandler 取消操作
*/
+ (void)alertWithMessage:(NSString *)message
confirmHandler:(TTPopupCompletionHandler)confirmHandler
cancelHandler:(TTPopupCompletionHandler)cancelHandler;
/**
显示 alert 弹窗
@discussion 显示四个内容:默认标题‘提示’,提示内容,取消按钮,确认按钮
@param config 完善的视图配置,为您变态的需求保驾护航
@param isShowBorder 是否显示边框
@param confirmHandler 确认操作
@param cancelHandler 取消操作
*/
+ (void)alertWithConfig:(TTAlertConfig *)config
showBorder:(BOOL)isShowBorder
confirmHandler:(TTPopupCompletionHandler)confirmHandler
cancelHandler:(TTPopupCompletionHandler)cancelHandler;
+ (void)alertWithMessage:(NSString *)message
config:(TTAlertConfig *)config
showBorder:(BOOL)isShowBorder
cancelHandler:(TTPopupCompletionHandler)cancelHandler
confirmHandler:(TTPopupCompletionHandler)confirmHandler;
/**
显示 alert 弹窗
@discussion 显示四个内容:标题,提示内容,取消按钮,确认按钮
@param config 完善的视图配置,为您变态的需求保驾护航
@param cancelHandler 取消操作
@param confirmHandler 确认操作
*/
+ (void)alertWithConfig:(TTAlertConfig *)config
confirmHandler:(TTPopupCompletionHandler)confirmHandler
cancelHandler:(TTPopupCompletionHandler)cancelHandler;
#pragma mark Action Sheet
/**
显示 action sheet 弹窗,自带贴心的取消按钮😊
@param items 配置列表
*/
+ (void)actionSheetWithItems:(NSArray<TTActionSheetConfig *> *)items;
/**
显示 action sheet 弹窗
@param items 配置列表
@param showCancelItem 是否显示取消按钮
*/
+ (void)actionSheetWithItems:(NSArray<TTActionSheetConfig *> *)items
showCancelItem:(BOOL)showCancelItem;
/**
显示 action sheet 弹窗
@param items 配置列表
@param cancelHandler 取消按钮回调
*/
+ (void)actionSheetWithItems:(NSArray<TTActionSheetConfig *> *)items
cancelHandler:(TTActionSheetClickAction)cancelHandler;
#pragma mark Popup
/**
显示自定义弹窗
@param customView 自定义 view
@param style 弹窗样式
*/
+ (void)popupView:(UIView *)customView
style:(TTPopupStyle)style;
/**
显示自定义弹窗
@param config 自定义弹窗配置
*/
+ (void)popupWithConfig:(TTPopupService *)config;
#pragma mark Dismiss
/**
消除当前弹窗
*/
+ (void)dismiss;
#pragma mark Query
/**
当前是否有显示弹窗
*/
+ (BOOL)hasShowPopup;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,243 +0,0 @@
//
// TTPopup.m
// YM_TTChatViewKit
//
// Created by lvjunhang on 2019/5/22.
// Copyright © 2023 YUMI. All rights reserved.
//
#import "TTPopup.h"
#import "TTAlertView.h"
#import "TTActionSheetView.h"
#import "TTPopupService.h"
#import "TTPopupManagerService.h"
static CGFloat const kActionSheetViewPadding = 15.f;
static CGFloat const kActionSheetViewCellHeight = 52.f;
static CGFloat const kActionSheetViewCancelViewHeight = 67.f;
static CGFloat const kActionSheetViewBottomPadding = 15.f;
static CGFloat const kMixHeight = 200.f;
static CGFloat const kMaxHeight = 450.f;
@implementation TTPopup
#pragma mark - Public Methods
#pragma mark Alert
+ (void)alertWithMessage:(NSString *)message
confirmHandler:(TTPopupCompletionHandler)confirmHandler
cancelHandler:(TTPopupCompletionHandler)cancelHandler {
[self alertWithMessage:message
config:nil
cancelHandler:cancelHandler
confirmHandler:confirmHandler];
}
+ (void)alertWithConfig:(TTAlertConfig *)config
confirmHandler:(TTPopupCompletionHandler)confirmHandler
cancelHandler:(TTPopupCompletionHandler)cancelHandler {
[self alertWithMessage:nil
config:config
cancelHandler:cancelHandler
confirmHandler:confirmHandler];
}
+ (void)alertWithMessage:(NSString *)message
config:(TTAlertConfig *)config
cancelHandler:(TTPopupCompletionHandler)cancelHandler
confirmHandler:(TTPopupCompletionHandler)confirmHandler {
if (!config) {
config = [[TTAlertConfig alloc] init];
config.title = YMLocalizedString(@"UserDetail_CP_Toast_0");
config.message = message;
}
if (config.message.length <= 0 && config.messageAttributed.length <=0 && config.messageAttributedConfig.count <=0) {
#if DEBUG
NSAssert(NO, @" message can not be nil, 弹窗文案不可以为空");
return;
#else
// 防止正式环境 crash
config.message = @" ";
#endif
}
CGFloat width = [UIScreen mainScreen].bounds.size.width - 40 * 2;
CGFloat height = ([self messageSize:config.message width:width].height + 160);
// 最小 200 最大 450
if (height < kMixHeight) {
height = kMixHeight;
} else if (height > KScreenHeight - kSafeAreaTopHeight - kSafeAreaBottomHeight) {
height = KScreenHeight - kSafeAreaTopHeight - kSafeAreaBottomHeight;
}
TTAlertView *contentView = [[TTAlertView alloc] initWithFrame:CGRectMake(0, 0, width, height)];
contentView.config = config;
contentView.isConfigBoard = NO;
contentView.cancelAction = cancelHandler;
contentView.confirmAction = confirmHandler;
if (!contentView.config.disableAutoDismissWhenClickButton) {
// 设置弹窗按钮自动消除
contentView.dismissAction = ^{
[TTPopup dismiss];
};
}
[self popupView:contentView style:TTPopupStyleAlert config:config];
}
+ (void)alertWithConfig:(TTAlertConfig *)config
showBorder:(BOOL)isShowBorder
confirmHandler:(TTPopupCompletionHandler)confirmHandler
cancelHandler:(TTPopupCompletionHandler)cancelHandler {
[self alertWithMessage:@""
config:config
showBorder:isShowBorder
cancelHandler:cancelHandler
confirmHandler:confirmHandler];
}
+ (void)alertWithMessage:(NSString *)message
config:(TTAlertConfig *)config
showBorder:(BOOL)isShowBorder
cancelHandler:(TTPopupCompletionHandler)cancelHandler
confirmHandler:(TTPopupCompletionHandler)confirmHandler {
if (!config) {
config = [[TTAlertConfig alloc] init];
config.message = message;
}
if (config.message.length <= 0) {
NSAssert(NO, @" message can not be nil, 弹窗文案不可以为空");
return;
}
CGFloat width = [UIScreen mainScreen].bounds.size.width - 40 * 2;
CGFloat height = ([self messageSize:config.message width:width].height + 160);
// 最小 200 最大 450
if (height < kMixHeight) {
height = kMixHeight;
} else if (height > kMaxHeight) {
height = kMaxHeight;
}
TTAlertView *contentView = [[TTAlertView alloc] initWithFrame:CGRectMake(0, 0, width, height)];
contentView.config = config;
contentView.cancelAction = cancelHandler;
contentView.confirmAction = confirmHandler;
contentView.isConfigBoard = isShowBorder;
if (!contentView.config.disableAutoDismissWhenClickButton) {
// 设置弹窗按钮自动消除
contentView.dismissAction = ^{
[TTPopup dismiss];
};
}
[self popupView:contentView style:TTPopupStyleAlert config:config];
}
#pragma mark Action Sheet
+ (void)actionSheetWithItems:(NSArray<TTActionSheetConfig *> *)items {
[TTPopup actionSheetWithItems:items showCancelItem:YES cancelHandler:nil];
}
+ (void)actionSheetWithItems:(NSArray<TTActionSheetConfig *> *)items
showCancelItem:(BOOL)showCancelItem {
[TTPopup actionSheetWithItems:items showCancelItem:showCancelItem cancelHandler:nil];
}
+ (void)actionSheetWithItems:(NSArray<TTActionSheetConfig *> *)items cancelHandler:(TTActionSheetClickAction)cancelHandler {
[TTPopup actionSheetWithItems:items showCancelItem:YES cancelHandler:cancelHandler];
}
+ (void)actionSheetWithItems:(NSArray<TTActionSheetConfig *> *)items
showCancelItem:(BOOL)showCancelItem
cancelHandler:(TTActionSheetClickAction)cancelHandler {
CGFloat width = [UIScreen mainScreen].bounds.size.width - kActionSheetViewPadding * 2;
CGFloat height = kActionSheetViewCellHeight * items.count + kActionSheetViewBottomPadding;
// 如果需要显示取消按钮则增加响应的高度
if (showCancelItem) {
// 按钮的高度和间距
height += kActionSheetViewCancelViewHeight;
}
if (@available(iOS 11.0, *)) {
// 如果是 iPhone X 系列(刘海屏幕系列) 底部则需要添加 34 的高度
height += [UIApplication sharedApplication].keyWindow.safeAreaInsets.bottom;
}
CGRect rect = CGRectMake(0, 0, width, height);
TTActionSheetView *sheetView = [[TTActionSheetView alloc] initWithFrame:rect
needCancel:showCancelItem
items:items];
sheetView.cancelAction = cancelHandler;
// 设置弹窗按钮自动消除
sheetView.dismissAction = ^{
[TTPopup dismiss];
};
[self popupView:sheetView style:TTPopupStyleActionSheet];
}
#pragma mark Popup
+ (void)popupView:(UIView *)customView
style:(TTPopupStyle)style {
TTPopupService *service = [[TTPopupService alloc] init];
service.style = style;
service.contentView = customView;
[self popupWithConfig:service];
}
+ (void)popupView:(UIView *)customView
style:(TTPopupStyle)style
config:(TTAlertConfig *)config {
TTPopupService *service = [[TTPopupService alloc] init];
service.style = style;
service.contentView = customView;
service.shouldDismissOnBackgroundTouch = config.shouldDismissOnBackgroundTouch;
service.maskBackgroundAlpha = config.maskBackgroundAlpha;
[self popupWithConfig:service];
}
+ (void)popupWithConfig:(TTPopupService *)config {
if (![config.contentView isKindOfClass:UIView.class]) {
NSAssert(NO, @"TTPopup customView should inherit from UIView.");
return;
}
[[TTPopupManagerService sharedInstance] addPopupService:config];
}
#pragma mark Dismiss
+ (void)dismiss {
[[TTPopupManagerService sharedInstance] removePopupService];
}
#pragma mark Query
/**
当前是否有显示弹窗
*/
+ (BOOL)hasShowPopup {
return [TTPopupManagerService sharedInstance].currentPopupService != nil;
}
#pragma mark - Privite
+ (CGSize)messageSize:(NSString *)text width:(CGFloat)width {
CGRect stringRect = [text boundingRectWithSize:CGSizeMake(width - 2 * 20, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:15.f]} context:nil];
return stringRect.size;
}
@end

View File

@@ -1,26 +0,0 @@
//
// TTActionSheetView.h
// YM_TTChatViewKit
//
// Created by lee on 2019/5/22.
// Copyright © 2023 YUMI. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "TTPopupConstants.h"
NS_ASSUME_NONNULL_BEGIN
@class TTActionSheetConfig;
@interface TTActionSheetView : UIView
@property (nonatomic, copy) TTPopupCompletionHandler cancelAction;
@property (nonatomic, copy) TTPopupCompletionHandler dismissAction;
- (instancetype)initWithFrame:(CGRect)frame
needCancel:(BOOL)needCancel
items:(NSArray<TTActionSheetConfig *> *)items;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,159 +0,0 @@
//
// TTActionSheetView.m
// YM_TTChatViewKit
//
// Created by lee on 2019/5/22.
// Copyright © 2023 YUMI. All rights reserved.
//
#import "TTActionSheetView.h"
#import "TTActionSheetConfig.h"
#import "DJDKMIMOMColor.h"
#import <Masonry/Masonry.h>
static CGFloat const kSheetViewCellHeight = 51.f;
static CGFloat const kSheetViewCornerRadius = 14.f;
static NSString *const kSheetViewCellConst = @"kSheetViewCellConst";
@interface TTActionSheetView ()<UITableViewDelegate, UITableViewDataSource>
/** sheetView 载体 */
@property (nonatomic, strong) UITableView *tableView;
/** 数据源 */
@property (nonatomic, strong) NSArray<TTActionSheetConfig *> *items;
/** 是否需要显示取消按钮 */
@property (nonatomic, assign) BOOL needCancel;
/** 取消按钮 */
@property (nonatomic, strong) UIButton *cancelButton;
@end
@implementation TTActionSheetView
#pragma mark -
#pragma mark lifeCycle
- (instancetype)initWithFrame:(CGRect)frame needCancel:(BOOL)needCancel items:(NSArray<TTActionSheetConfig *> *)items {
self = [super initWithFrame:frame];
if (self) {
_items = items;
_needCancel = needCancel;
[self initViews];
[self initConstraints];
}
return self;
}
- (void)initViews {
[self addSubview:self.tableView];
[self addSubview:self.cancelButton];
}
- (void)initConstraints {
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.trailing.top.mas_equalTo(self);
make.height.mas_equalTo(self.items.count * kSheetViewCellHeight);
}];
if (_needCancel) {
// 显示 cancel view
self.cancelButton.hidden = NO;
[self.cancelButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.trailing.mas_equalTo(self);
make.height.mas_equalTo(kSheetViewCellHeight);
make.top.mas_equalTo(self.tableView.mas_bottom).offset(15);
}];
}
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.items.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kSheetViewCellConst];
cell.backgroundColor = UIColor.clearColor;
cell.textLabel.textAlignment = NSTextAlignmentCenter;
cell.textLabel.text = _items[indexPath.row].title;
cell.textLabel.textColor = _items[indexPath.row].titleColor;
if ([_items[indexPath.row] displayMoliCoin]) {
NSTextAttachment *coinAttachment = [[NSTextAttachment alloc] init];
coinAttachment.image = kImage(@"moli_money_icon");
coinAttachment.bounds = CGRectMake(0, -3.5, 20, 20);
NSAttributedString *coinAttributedString = [NSAttributedString attributedStringWithAttachment:coinAttachment];
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithAttributedString:coinAttributedString];
NSAttributedString *titleAttributedString = [[NSAttributedString alloc] initWithString:_items[indexPath.row].title
attributes:@{
NSForegroundColorAttributeName: _items[indexPath.row].titleColor,
NSFontAttributeName: cell.textLabel.font
}];
[string insertAttributedString:titleAttributedString atIndex:0];
cell.textLabel.attributedText = string.copy;
UIImageView *questionMark = [[UIImageView alloc] initWithImage:kImage(@"question_mark")];
[cell.contentView addSubview:questionMark];
[questionMark mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.mas_equalTo(cell.textLabel);
make.trailing.mas_equalTo(-20);
make.size.mas_equalTo(CGSizeMake(22, 22));
}];
}
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
// 配置中的事件处理
TTActionSheetConfig *config = _items[indexPath.row];
!config.clickAction ?: config.clickAction();
!_dismissAction ?: _dismissAction();
}
- (void)onClickCancelButtonAction:(UIButton *)cancelButton {
!_cancelAction ?: _cancelAction();
!_dismissAction ?: _dismissAction();
}
- (UITableView *)tableView {
if (!_tableView) {
_tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.separatorColor = [DJDKMIMOMColor actionSeparatorColor];
_tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
_tableView.rowHeight = kSheetViewCellHeight;
_tableView.tableFooterView = [[UIView alloc] init];
_tableView.backgroundColor = [UIColor whiteColor];
_tableView.layer.cornerRadius = kSheetViewCornerRadius;
_tableView.layer.masksToBounds = YES;
_tableView.bounces = NO;
[_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kSheetViewCellConst];
}
return _tableView;
}
- (UIButton *)cancelButton {
if (!_cancelButton) {
_cancelButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_cancelButton setTitle:YMLocalizedString(@"TTActionSheetView0") forState:UIControlStateNormal];
[_cancelButton setBackgroundColor:UIColor.whiteColor];
[_cancelButton setTitleColor:[DJDKMIMOMColor alertMessageColor] forState:UIControlStateNormal];
[_cancelButton.titleLabel setFont:[UIFont systemFontOfSize:16]];
_cancelButton.layer.cornerRadius = kSheetViewCornerRadius;
_cancelButton.layer.masksToBounds = YES;
[_cancelButton addTarget:self action:@selector(onClickCancelButtonAction:) forControlEvents:UIControlEventTouchUpInside];
_cancelButton.hidden = YES;
}
return _cancelButton;
}
@end

View File

@@ -1,25 +0,0 @@
//
// TTAlertView.h
// YM_TTChatViewKit
//
// Created by lee on 2019/5/20.
// Copyright © 2023 YUMI. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "TTPopupConstants.h"
@class TTAlertConfig;
NS_ASSUME_NONNULL_BEGIN
@interface TTAlertView : UIView
@property (nonatomic, strong) TTAlertConfig *config;// 配置
@property (nonatomic, assign) BOOL isConfigBoard;// 是否配置边框
@property (nonatomic, copy) TTPopupCompletionHandler cancelAction;
@property (nonatomic, copy) TTPopupCompletionHandler confirmAction;
@property (nonatomic, copy) TTPopupCompletionHandler dismissAction;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,255 +0,0 @@
//
// TTAlertView.m
// YM_TTChatViewKit
//
// Created by lee on 2019/5/20.
// Copyright © 2023 YUMI. All rights reserved.
//
#import "TTAlertView.h"
#import "TTAlertConfig.h"
#import "DJDKMIMOMColor.h"
#import <Masonry/Masonry.h>
static CGFloat const kMargin = 25.f;
static CGFloat const kPadding = 20.f;
static CGFloat const kBtnHeight = 38.f;
@interface TTAlertView ()
@property (nonatomic, strong) UILabel *titleLabel; // 标题
@property (nonatomic, strong) UILabel *messageLabel; // 内容
@property (nonatomic, strong) UIButton *cancelButton; // 取消按钮
@property (nonatomic, strong) UIButton *confirmButton; // 确认按钮
@property (nonatomic, strong) UIStackView *stackView;
@end
@implementation TTAlertView
#pragma mark - lifeCyle
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self initViews];
[self initConstraints];
}
return self;
}
- (void)initViews {
[self addSubview:self.titleLabel];
[self addSubview:self.messageLabel];
[self addSubview:self.stackView];
[self.stackView addSubview:self.cancelButton];
[self.stackView addSubview:self.confirmButton];
}
- (void)initConstraints {
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(self);
make.top.mas_equalTo(kPadding);
make.leading.trailing.mas_equalTo(self).inset(kPadding);
}];
[self.messageLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.titleLabel.mas_bottom).offset(kMargin);
make.leading.trailing.mas_equalTo(self).inset(kPadding);
make.bottom.mas_equalTo(self).offset(-kBtnHeight * 2);
}];
[self.stackView mas_makeConstraints:^(MASConstraintMaker *make) {
make.bottom.mas_equalTo(-kPadding);
make.centerX.mas_equalTo(self);
}];
[self.cancelButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.height.mas_equalTo(kBtnHeight);
make.width.mas_equalTo(self.mas_width).multipliedBy(0.4);
}];
[self.confirmButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.height.mas_equalTo(kBtnHeight);
make.width.mas_equalTo(self.mas_width).multipliedBy(0.4);
}];
}
#pragma mark - Button Events
- (void)onClickConfirmButtonAction:(UIButton *)confirmButton {
!_confirmAction ?: _confirmAction();
!_dismissAction ?: _dismissAction();
}
- (void)onClickCancelButtonAction:(UIButton *)cancelButton {
!_cancelAction ?: _cancelAction();
!_dismissAction ?: _dismissAction();
}
#pragma mark - private method
/**
设置 messageLabel 需要显示的富文本效果
@param config 弹窗配置
@return 富文本内容
*/
- (NSMutableAttributedString *)messageAttributeString:(TTAlertConfig *)config {
NSMutableAttributedString *attString = [[NSMutableAttributedString alloc] initWithString:config.message];
if (config.messageLineSpacing > 0) { // 行间距
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.lineSpacing = config.messageLineSpacing;
paragraphStyle.alignment = config.messageTextAlignment;
[attString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, config.message.length)];
}
// 富文本显示效果
[config.messageAttributedConfig enumerateObjectsUsingBlock:^(TTAlertMessageAttributedConfig * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
// 遍历数组,添加展示富文本效果
if ([obj isKindOfClass:[TTAlertMessageAttributedConfig class]]) {
if (obj.text && obj.text.length > 0) {
NSRange range = [config.message rangeOfString:obj.text];
// 如果外部指定了 range 。就不用查找出的 range
if (obj.range.length != 0) {
if (obj.range.location + obj.range.length > config.message.length) {
NSAssert(NO, @"obj.range out of bounds");
return;
}
range = obj.range;
}
if (obj.font) { // 字体
[attString addAttribute:NSFontAttributeName value:obj.font range:range];
}
if (obj.color) { // 颜色
[attString addAttribute:NSForegroundColorAttributeName value:obj.color range:range];
}
}
}
}];
return attString;
}
#pragma mark - getter && setter
- (void)setConfig:(TTAlertConfig *)config {
_config = config;
// cornerRadius
if (config.cornerRadius > 0) {
self.layer.cornerRadius = config.cornerRadius;
self.layer.masksToBounds = YES;
}
//背景
self.backgroundColor = config.backgroundColor;
// title
_titleLabel.text = config.title;
_titleLabel.textColor = config.titleColor;
_titleLabel.font = config.titleFont;
_cancelButton.hidden = config.actionStyle == TTAlertActionConfirmStyle;
_confirmButton.hidden = config.actionStyle == TTAlertActionCancelStyle;
// cancel button
[_cancelButton setTitle:config.cancelButtonConfig.title forState:UIControlStateNormal];
[_cancelButton setTitleColor:config.cancelButtonConfig.titleColor forState:UIControlStateNormal];
[_cancelButton.titleLabel setFont:config.cancelButtonConfig.font];
[_cancelButton setBackgroundColor:config.cancelButtonConfig.backgroundColor];
[_cancelButton setBackgroundImage:config.cancelButtonConfig.backgroundImage forState:UIControlStateNormal];
if (config.cancelButtonConfig.cornerRadius > 0) {
_cancelButton.layer.cornerRadius = config.cancelButtonConfig.cornerRadius;
_cancelButton.layer.masksToBounds = YES;
}
// confirm button
[_confirmButton setTitle:config.confirmButtonConfig.title forState:UIControlStateNormal];
[_confirmButton setTitleColor:config.confirmButtonConfig.titleColor forState:UIControlStateNormal];
[_confirmButton.titleLabel setFont:config.confirmButtonConfig.font];
[_confirmButton setBackgroundColor:config.confirmButtonConfig.backgroundColor];
[_confirmButton setBackgroundImage:config.confirmButtonConfig.backgroundImage forState:UIControlStateNormal];
if (config.confirmButtonConfig.cornerRadius > 0) {
_confirmButton.layer.cornerRadius = config.confirmButtonConfig.cornerRadius;
_confirmButton.layer.masksToBounds = YES;
}
// message
_messageLabel.font = config.messageFont;
_messageLabel.textColor = config.messageColor;
if (config.messageAttributedConfig.count > 0) {
_messageLabel.attributedText = [self messageAttributeString:config];
_messageLabel.textAlignment = NSTextAlignmentCenter;
} else if(config.messageAttributed.length > 0) {
_messageLabel.attributedText = config.messageAttributed;
_messageLabel.textAlignment = NSTextAlignmentCenter;
} else {
_messageLabel.text = config.message;
_messageLabel.textAlignment = config.messageTextAlignment;
}
_cancelButton.layer.borderColor = [DJDKMIMOMColor dividerColor].CGColor;
_cancelButton.layer.borderWidth = 2.f;
_confirmButton.layer.borderColor = [DJDKMIMOMColor dividerColor].CGColor;
_confirmButton.layer.borderWidth = 2.f;
}
- (void)setIsConfigBoard:(BOOL)isConfigBoard {
_isConfigBoard = isConfigBoard;
if (isConfigBoard) {
//需要边框
_cancelButton.layer.borderColor = [DJDKMIMOMColor dividerColor].CGColor;
_cancelButton.layer.borderWidth = 2.f;
_confirmButton.layer.borderColor = [DJDKMIMOMColor dividerColor].CGColor;
_confirmButton.layer.borderWidth = 2.f;
}else {
//不需要边框
_cancelButton.layer.borderColor = [DJDKMIMOMColor dividerColor].CGColor;
_cancelButton.layer.borderWidth = 0;
_confirmButton.layer.borderColor = [DJDKMIMOMColor dividerColor].CGColor;
_confirmButton.layer.borderWidth = 0;
}
}
- (UILabel *)titleLabel {
if (!_titleLabel) {
_titleLabel = [[UILabel alloc] init];
_titleLabel.textAlignment = NSTextAlignmentCenter;
_titleLabel.numberOfLines = 0;
}
return _titleLabel ;
}
- (UILabel *)messageLabel {
if (!_messageLabel) {
_messageLabel = [[UILabel alloc] init];
_messageLabel.numberOfLines = 0;
_messageLabel.textAlignment = NSTextAlignmentCenter;
_messageLabel.minimumScaleFactor = 0.7;
_messageLabel.adjustsFontSizeToFitWidth = YES;
}
return _messageLabel;
}
- (UIButton *)cancelButton {
if (!_cancelButton) {
_cancelButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_cancelButton addTarget:self action:@selector(onClickCancelButtonAction:) forControlEvents:UIControlEventTouchUpInside];
}
return _cancelButton;
}
- (UIButton *)confirmButton {
if (!_confirmButton) {
_confirmButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_confirmButton addTarget:self action:@selector(onClickConfirmButtonAction:) forControlEvents:UIControlEventTouchUpInside];
}
return _confirmButton;
}
- (UIStackView *)stackView {
if (!_stackView) {
_stackView = [[UIStackView alloc] initWithArrangedSubviews:@[self.cancelButton, self.confirmButton]];
_stackView.distribution = UIStackViewDistributionFillEqually;
_stackView.alignment = UIStackViewAlignmentCenter;
_stackView.spacing = 16;
}
return _stackView;
}
@end

View File

@@ -1,20 +0,0 @@
//
// TTNewAlertView.h
// xplan-ios
//
// Created by duoban on 2023/1/9.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface TTNewAlertView : UIView
@property (nonatomic,copy) NSString *message;
@property (nonatomic, copy) TTPopupCompletionHandler confirmHandle;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,118 +0,0 @@
//
// TTNewAlertView.m
// xplan-ios
//
// Created by duoban on 2023/1/9.
//
#import "TTNewAlertView.h"
@interface TTNewAlertView()
@property (nonatomic,strong) UIView *bgView;
@property (nonatomic,strong) UILabel *messageView;
@property (nonatomic,strong) UIButton *confirmBtn;
@property (nonatomic,strong) UIButton *cancelBtn;
@end
@implementation TTNewAlertView
-(instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if(self){
[self initSubViews];
[self initSubViewConstraints];
}
return self;
}
#pragma mark - Private Method
- (void)initSubViews {
self.backgroundColor = [UIColor clearColor];
[self addSubview:self.bgView];
[self.bgView addSubview:self.messageView];
[self.bgView addSubview:self.confirmBtn];
[self.bgView addSubview:self.cancelBtn];
}
- (void)initSubViewConstraints {
[self.bgView mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(kGetScaleWidth(310));
make.height.mas_equalTo(kGetScaleWidth(149));
make.center.equalTo(self);
}];
[self.confirmBtn mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.height.mas_equalTo(kGetScaleWidth(110));
make.height.mas_equalTo(kGetScaleWidth(37));
make.leading.mas_equalTo(kGetScaleWidth(31));
make.bottom.mas_equalTo(-kGetScaleWidth(31));
}];
[self.cancelBtn mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.height.centerY.equalTo(self.confirmBtn);
make.trailing.mas_equalTo(-kGetScaleWidth(31));
}];
[self.messageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(kGetScaleWidth(25));
make.centerX.equalTo(self);
make.leading.trailing.equalTo(self).inset(kGetScaleWidth(10));
}];
}
-(void)setMessage:(NSString *)message{
_message = message;
_messageView.text = message;
}
-(void)confirmAction{
[TTPopup dismiss];
if(self.confirmHandle){
self.confirmHandle();
}
}
-(void)cancelAction{
[TTPopup dismiss];
}
#pragma mark -懒加载
- (UIView *)bgView{
if (!_bgView){
_bgView = [UIView new];
_bgView.backgroundColor = [UIColor whiteColor];
[_bgView setCornerWithLeftTopCorner:kGetScaleWidth(12) rightTopCorner:kGetScaleWidth(12) bottomLeftCorner:kGetScaleWidth(12) bottomRightCorner:kGetScaleWidth(12) size:CGSizeMake(kGetScaleWidth(310), kGetScaleWidth(149))];
}
return _bgView;
}
- (UILabel *)messageView{
if (!_messageView){
_messageView = [UILabel labelInitWithText:@"" font:kFontRegular(15) textColor:[DJDKMIMOMColor inputTextColor]];
_messageView.textAlignment = NSTextAlignmentCenter;
_messageView.numberOfLines = 0;
}
return _messageView;
}
-(UIButton *)confirmBtn{
if (!_confirmBtn){
_confirmBtn = [UIButton new];
[_confirmBtn setTitle:YMLocalizedString(@"TTAlertConfig0") forState:UIControlStateNormal];
_confirmBtn.backgroundColor = UIColorFromRGB(0xE6E6F0);
_confirmBtn.layer.cornerRadius = kGetScaleWidth(37)/2;
_confirmBtn.layer.masksToBounds = YES;
[_confirmBtn addTarget:self action:@selector(confirmAction) forControlEvents:UIControlEventTouchUpInside];
}
return _confirmBtn;
}
-(UIButton *)cancelBtn{
if (!_cancelBtn){
_cancelBtn = [UIButton new];
[_cancelBtn setTitle:YMLocalizedString(@"XPShareView7") forState:UIControlStateNormal];
UIImage *image = [UIImage gradientColorImageFromColors:@[[DJDKMIMOMColor confirmButtonGradientStartColor],[DJDKMIMOMColor confirmButtonGradientMiddleColor],[DJDKMIMOMColor confirmButtonGradientEndColor]] gradientType:GradientTypeLeftToRight imgSize:CGSizeMake(kGetScaleWidth(110), kGetScaleWidth(37))];
_cancelBtn.backgroundColor = [UIColor colorWithPatternImage:image];
_cancelBtn.layer.cornerRadius = kGetScaleWidth(37)/2;
_cancelBtn.layer.masksToBounds = YES;
[_cancelBtn addTarget:self action:@selector(cancelAction) forControlEvents:UIControlEventTouchUpInside];
}
return _cancelBtn;
}
@end

View File

@@ -1,65 +0,0 @@
//
// UIImage+Utils.h
// YYMobileFramework
//
// Created by wuwei on 14/6/20.
// Copyright (c) 2014年 YY Inc. All rights reserved.
//
#import <UIKit/UIKit.h>
typedef NS_ENUM(NSUInteger, GradientType) {
GradientTypeTopToBottom = 0,//从上到小
GradientTypeLeftToRight = 1,//从左到右
GradientTypeUpleftToLowright = 2,//左上到右下
GradientTypeUprightToLowleft = 3,//右上到左下
};
@interface UIImage (Utils)
- (UIImage *)grayscaleImage;
- (UIImage *)imageBlendInGray;
- (UIImage *)imageWithBlendMode:(CGBlendMode)blendMode;
+ (UIImage *)imageWithColor:(UIColor *)color;
+ (UIImage *)imageWithColor:(UIColor *)color size:(CGSize)size;
+ (UIImage *)fixOrientation:(UIImage *)aImage;
- (UIImage *)imageWithColor:(UIColor *)color;
- (UIImage *)setCornerWithRadius:(CGFloat)radius andSize:(CGSize)size;
//异步生成纯色圆角图片
- (void)imageWithSize:(CGSize)size radius:(CGFloat)radius backColor:(UIColor *)backColor completion:(void(^)(UIImage *image))completion;
/**
返回指定大小,颜色,渐变模式的渐变色图片
*/
+ (UIImage *)gradientColorImageFromColors:(NSArray<UIColor *>*)colors gradientType:(GradientType)gradientType imgSize:(CGSize)imgSize;
+ (UIImage *)waterImageWithImage:(UIImage *)image waterImage:(UIImage *)waterImage waterImageRect:(CGRect)rect;
+ (CGSize)sizeWithImageOriginSize:(CGSize)originSize
minSize:(CGSize)imageMinSize
maxSize:(CGSize)imageMaxSize;
///裁剪图片
- (UIImage *)cutImage:(CGSize)newSize;
- (UIImage *)cropRightAndBottomPixels:(NSUInteger)pixels;
-(UIImage *)compressWithMaxLength:(NSUInteger)maxLength;
- (UIImage *)roundedImageWithCornerRadius:(CGFloat)cornerRadius size:(CGSize)size;
+(UIImage *)getImageFromView:(UIView *)view;
+ (NSString *)getImageTypeWithImageData: (NSData *)data;
+(UIImage *)getLanguageImage:(NSString *)image;
+(NSString *)getLanguageText:(NSString *)image;
- (UIImage *)resizeTo:(CGSize)size;
- (UIImage *)imageByApplyingAlpha:(CGFloat)alpha;
@end

View File

@@ -1,592 +0,0 @@
//
// UIImage+Utils.m
// YYMobileFramework
//
// Created by wuwei on 14/6/20.
// Copyright (c) 2014年 YY Inc. All rights reserved.
//
#import "UIImage+Utils.h"
#import <ImageIO/ImageIO.h>
@implementation UIImage (Utils)
- (UIImage *)grayscaleImage
{
CGFloat width = self.size.width;
CGFloat height = self.size.height;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
CGContextRef context = CGBitmapContextCreate(nil,
width,
height,
8,
0,
colorSpace,
kCGImageAlphaNone);
// kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedLast);
if (context == NULL) {
return nil;
}
CGContextDrawImage(context, CGRectMake(0, 0, width, height), self.CGImage);
CGImageRef imageRef = CGBitmapContextCreateImage(context);
UIImage *grayscaleImage = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
return grayscaleImage;
}
- (UIImage *)imageBlendInGray {
UIGraphicsBeginImageContext(self.size);
CGRect bounds = CGRectMake(0, 0, self.size.width, self.size.height);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextFillRect(context, bounds);
[self drawInRect:bounds blendMode:kCGBlendModeLuminosity alpha:1.0f];
[self drawInRect:bounds blendMode:kCGBlendModeDestinationIn alpha:1.0f];
UIImage *newImg = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImg;
}
- (UIImage *)imageWithBlendMode:(CGBlendMode)blendMode {
UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0f);
CGRect bounds = CGRectMake(0, 0, self.size.width, self.size.height);
UIRectFill(bounds);
[self drawInRect:bounds blendMode:blendMode alpha:1.0f];
if (blendMode != kCGBlendModeDestinationIn) {
[self drawInRect:bounds blendMode:kCGBlendModeDestinationIn alpha:1.0f];
}
UIImage *newImg = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImg;
}
+ (UIImage *)imageWithColor:(UIColor *)color
{
return [self imageWithColor:color size:CGSizeMake(1, 1)];
}
+ (UIImage *)imageWithColor:(UIColor *)color size:(CGSize)size {
if (!color || size.width <= 0 || size.height <= 0) return nil;
CGRect rect = CGRectMake(0.0f, 0.0f, size.width + 1, size.height);
UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, color.CGColor);
CGContextFillRect(context, rect);
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
+ (UIImage *)fixOrientation:(UIImage *)aImage {
// No-op if the orientation is already correct
if (aImage.imageOrientation == UIImageOrientationUp)
return aImage;
// We need to calculate the proper transformation to make the image upright.
// We do it in 2 steps: Rotate if Left/Right/Down, and then flip if Mirrored.
CGAffineTransform transform = CGAffineTransformIdentity;
switch (aImage.imageOrientation) {
case UIImageOrientationDown:
case UIImageOrientationDownMirrored:
transform = CGAffineTransformTranslate(transform, aImage.size.width, aImage.size.height);
transform = CGAffineTransformRotate(transform, M_PI);
break;
case UIImageOrientationLeft:
case UIImageOrientationLeftMirrored:
transform = CGAffineTransformTranslate(transform, aImage.size.width, 0);
transform = CGAffineTransformRotate(transform, M_PI_2);
break;
case UIImageOrientationRight:
case UIImageOrientationRightMirrored:
transform = CGAffineTransformTranslate(transform, 0, aImage.size.height);
transform = CGAffineTransformRotate(transform, -M_PI_2);
break;
default:
break;
}
switch (aImage.imageOrientation) {
case UIImageOrientationUpMirrored:
case UIImageOrientationDownMirrored:
transform = CGAffineTransformTranslate(transform, aImage.size.width, 0);
transform = CGAffineTransformScale(transform, -1, 1);
break;
case UIImageOrientationLeftMirrored:
case UIImageOrientationRightMirrored:
transform = CGAffineTransformTranslate(transform, aImage.size.height, 0);
transform = CGAffineTransformScale(transform, -1, 1);
break;
default:
break;
}
// Now we draw the underlying CGImage into a new context, applying the transform
// calculated above.
CGContextRef ctx = CGBitmapContextCreate(NULL, aImage.size.width, aImage.size.height,
CGImageGetBitsPerComponent(aImage.CGImage), 0,
CGImageGetColorSpace(aImage.CGImage),
CGImageGetBitmapInfo(aImage.CGImage));
CGContextConcatCTM(ctx, transform);
switch (aImage.imageOrientation) {
case UIImageOrientationLeft:
case UIImageOrientationLeftMirrored:
case UIImageOrientationRight:
case UIImageOrientationRightMirrored:
// Grr...
CGContextDrawImage(ctx, CGRectMake(0,0,aImage.size.height,aImage.size.width), aImage.CGImage);
break;
default:
CGContextDrawImage(ctx, CGRectMake(0,0,aImage.size.width,aImage.size.height), aImage.CGImage);
break;
}
// And now we just create a new UIImage from the drawing context
CGImageRef cgimg = CGBitmapContextCreateImage(ctx);
UIImage *img = [UIImage imageWithCGImage:cgimg];
CGContextRelease(ctx);
CGImageRelease(cgimg);
return img;
}
- (UIImage *)imageWithColor:(UIColor *)color {
CGRect rect = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f);
UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [color CGColor]);
CGContextFillRect(context, rect);
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
//异步生成纯色圆角图片
- (void)imageWithSize:(CGSize)size radius:(CGFloat)radius backColor:(UIColor *)backColor completion:(void(^)(UIImage *image))completion {
// 异步绘制裁切
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 利用绘图建立上下文
UIGraphicsBeginImageContextWithOptions(size, true, 0);
CGRect rect = CGRectMake(0, 0, size.width, size.height);
// 填充颜色
[backColor setFill];
UIRectFill(rect);
// // 贝塞尔裁切
// UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius];
// [path addClip];
// [self drawInRect:rect];
// 获取结果
UIImage *resultImage = [UIGraphicsGetImageFromCurrentImageContext() circularImage];
// 关闭上下文
UIGraphicsEndImageContext();
// 主队列回调
dispatch_async(dispatch_get_main_queue(), ^{
completion(resultImage);
});
});
}
- (UIImage *)circularImage {
// 1. 开启图形上下文
UIGraphicsBeginImageContextWithOptions(self.size, NO, 0);
// 2. 描述路径
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, self.size.width, self.size.height) byRoundingCorners:UIRectCornerTopLeft|UIRectCornerTopRight | UIRectCornerBottomLeft | UIRectCornerBottomRight cornerRadii:CGSizeMake(self.size.width, self.size.height)];
// UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, self.size.width, self.size.height)];
// 3. 添加裁减区域
[path addClip];
// 4. 绘制图片
[self drawAtPoint:CGPointZero];
// 5. 从上下文获取图片
UIImage * image = UIGraphicsGetImageFromCurrentImageContext();
// 6. 关闭上下文
UIGraphicsEndImageContext();
// 7. 设置图片
return image;
}
+ (UIImage *)gradientColorImageFromColors:(NSArray<UIColor *> *)colors gradientType:(GradientType)gradientType imgSize:(CGSize)imgSize{
NSMutableArray *ar = [NSMutableArray array];
for(UIColor *c in colors) {
[ar addObject:(id)c.CGColor];
}
UIGraphicsBeginImageContextWithOptions(imgSize, YES, 1);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGColorSpaceRef colorSpace = CGColorGetColorSpace([[colors lastObject] CGColor]);
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (CFArrayRef)ar, NULL);
CGPoint start;
CGPoint end;
switch (gradientType) {
case GradientTypeTopToBottom:
start = CGPointMake(0.0, 0.0);
end = CGPointMake(0.0, imgSize.height);
break;
case GradientTypeLeftToRight:
start = CGPointMake(0.0, 0.0);
end = CGPointMake(imgSize.width, 0.0);
break;
case GradientTypeUpleftToLowright:
start = CGPointMake(0.0, 0.0);
end = CGPointMake(imgSize.width, imgSize.height);
break;
case GradientTypeUprightToLowleft:
start = CGPointMake(imgSize.width, 0.0);
end = CGPointMake(0.0, imgSize.height);
break;
default:
break;
}
CGContextDrawLinearGradient(context, gradient, start, end, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
CGGradientRelease(gradient);
CGContextRestoreGState(context);
CGColorSpaceRelease(colorSpace);
UIGraphicsEndImageContext();
return image;
}
- (UIImage *)setCornerWithRadius:(CGFloat)radius andSize:(CGSize)size {
//开启图形上下文
UIGraphicsBeginImageContext(size);
//绘制圆角矩形
CGRect rect = CGRectMake(0, 0, size.width, size.height);
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(radius, radius)];
//将Path添加到上下文中
CGContextAddPath(UIGraphicsGetCurrentContext(), path.CGPath);
//裁剪上下文
CGContextClip(UIGraphicsGetCurrentContext());
//将图片绘制到上下文中
[self drawInRect:rect];
//设置绘制模式
CGContextDrawPath(UIGraphicsGetCurrentContext(), kCGPathStroke);
//获取图片
UIImage *output = UIGraphicsGetImageFromCurrentImageContext();
//关闭上下文
UIGraphicsEndImageContext();
//返回裁剪好的图片
return output;
}
// 给图片添加图片水印
+ (UIImage *)waterImageWithImage:(UIImage *)image waterImage:(UIImage *)waterImage waterImageRect:(CGRect)rect
{
//1.获取图片
//2.开启上下文
UIGraphicsBeginImageContextWithOptions(image.size, NO, 0);
//3.绘制背景图片
[image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
//绘制水印图片到当前上下文
[waterImage drawInRect:rect];
//4.从上下文中获取新图片
UIImage * newImage = UIGraphicsGetImageFromCurrentImageContext();
//5.关闭图形上下文
UIGraphicsEndImageContext();
//返回图片
return newImage;
}
+ (CGSize)sizeWithImageOriginSize:(CGSize)originSize
minSize:(CGSize)imageMinSize
maxSize:(CGSize)imageMaxSiz {
CGSize size;
NSInteger imageWidth = originSize.width ,imageHeight = originSize.height;
NSInteger imageMinWidth = imageMinSize.width, imageMinHeight = imageMinSize.height;
NSInteger imageMaxWidth = imageMaxSiz.width, imageMaxHeight = imageMaxSiz.height;
if (imageWidth > imageHeight) //宽图
{
size.height = imageMinHeight; //高度取最小高度
size.width = imageWidth * imageMinHeight / imageHeight;
if (size.width > imageMaxWidth)
{
size.width = imageMaxWidth;
}
}
else if(imageWidth < imageHeight)//高图
{
size.width = imageMinWidth;
size.height = imageHeight *imageMinWidth / imageWidth;
if (size.height > imageMaxHeight){
size.height = imageMaxHeight;
}
}
else//方图
{
if (imageWidth > imageMaxWidth){
size.width = imageMaxWidth;
size.height = imageMaxHeight;
}else if(imageWidth > imageMinWidth){
size.width = imageWidth;
size.height = imageHeight;
}else{
size.width = imageMinWidth;
size.height = imageMinHeight;
}
}
return size;
}
- (UIImage *)cutImage:(CGSize)newSize{
CGFloat scale = newSize.height / self.size.height;
UIImage *scaleImage = [self originImage:self scaleToSize:CGSizeMake(self.size.width*scale, self.size.height*scale)];
//裁剪暂时有问题
return scaleImage;
}
- (UIImage *)resizeTo:(CGSize)size {
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:size];
return [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull context) {
[self drawInRect:CGRectMake(0, 0, size.width, size.height)];
}];
}
- (UIImage *)cropRightAndBottomPixels:(NSUInteger)pixels {
// 获取原图像的大小
CGSize originalSize = self.size;
// 计算新的裁剪后的图像大小
CGSize newSize = CGSizeMake(originalSize.width - pixels, originalSize.height - pixels);
// 开始图像上下文
UIGraphicsBeginImageContextWithOptions(newSize, NO, self.scale);
// 绘制裁剪后的图像到上下文
[self drawAtPoint:CGPointZero];
// 获取裁剪后的图像
UIImage *croppedImage = UIGraphicsGetImageFromCurrentImageContext();
// 结束图像上下文
UIGraphicsEndImageContext();
return croppedImage;
}
- (UIImage*) originImage:(UIImage *)image scaleToSize:(CGSize)size {
UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale);
[image drawInRect:CGRectMake(0, 0, size.width, size.height)];
UIImage* scaledImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return scaledImage;
}
-(UIImage *)compressWithMaxLength:(NSUInteger)maxLength{
// Compress by quality
CGFloat compression = 1;
NSData *data = UIImageJPEGRepresentation(self, compression);
if (data.length < maxLength) return self;
CGFloat max = 1;
CGFloat min = 0;
for (int i = 0; i < 6; ++i) {
compression = (max + min) / 2;
data = UIImageJPEGRepresentation(self, compression);
//NSLog(@"Compression = %.1f", compression);
//NSLog(@"In compressing quality loop, image size = %ld KB", data.length / 1024);
if (data.length < maxLength * 0.9) {
min = compression;
} else if (data.length > maxLength) {
max = compression;
} else {
break;
}
}
//NSLog(@"After compressing quality, image size = %ld KB", data.length / 1024);
if (data.length < maxLength) return self;
UIImage *resultImage = [UIImage imageWithData:data];
// Compress by size
NSUInteger lastDataLength = 0;
while (data.length > maxLength && data.length != lastDataLength) {
lastDataLength = data.length;
CGFloat ratio = (CGFloat)maxLength / data.length;
//NSLog(@"Ratio = %.1f", ratio);
CGSize size = CGSizeMake((NSUInteger)(resultImage.size.width * sqrtf(ratio)),
(NSUInteger)(resultImage.size.height * sqrtf(ratio))); // Use NSUInteger to prevent white blank
UIGraphicsBeginImageContext(size);
[resultImage drawInRect:CGRectMake(0, 0, size.width, size.height)];
resultImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
data = UIImageJPEGRepresentation(resultImage, compression);
//NSLog(@"In compressing size loop, image size = %ld KB", data.length / 1024);
}
if (data) {
return [UIImage imageWithData:data];;
} else {
return self;
}
}
- (UIImage *)roundedImageWithCornerRadius:(CGFloat)cornerRadius size:(CGSize)size{
UIGraphicsBeginImageContextWithOptions(size, NO, 1);
UIBezierPath *clippingPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, size.width, size.height) cornerRadius:cornerRadius];
[clippingPath addClip];
[self drawInRect:CGRectMake(0, 0, size.width, size.height)];
UIImage *roundedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return roundedImage;
}
+(UIImage *)getImageFromView:(UIView *)view {
// 1. 创建一个新的图像上下文大小与view的bounds相匹配
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, 0.0);
// 2. 获取当前图像上下文
CGContextRef context = UIGraphicsGetCurrentContext();
// 3. 将view的层级渲染到上下文中
[view.layer renderInContext:context];
// 4. 从上下文中获取生成的图片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// 5. 结束图像上下文
UIGraphicsEndImageContext();
// 6. 返回生成的图片
return image;
}
+ (NSString *)getImageTypeWithImageData:(NSData *)data {
uint8_t c;
[data getBytes:&c length:1];
switch (c) {
case 0xFF:
return @"jpeg";
case 0x89:
return @"png";
case 0x47:
return @"gif";
case 0x49:
case 0x4D:
return @"tiff";
case 0x52:
if ([data length] < 12) {
return nil;
}
NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
return @"webp";
}
return nil;
}
return nil;
}
+(UIImage *)getLanguageImage:(NSString *)image{
NSString *curImage = image;
NSString *language = [NSBundle getLanguageText];
if (isMSZH()) {
// 不處理
} else if (isMSTR()) {
image = [NSString stringWithFormat:@"%@_tr", image];
} else if (isMSRTL()) {
image = [NSString stringWithFormat:@"%@_ar", image];
} else {
image = [NSString stringWithFormat:@"%@_en", image];
}
// 尝试获取带语言后缀的图片
UIImage *getImage = kImage(image);
// 若图片不存在,尝试使用英语图片作为默认
if (getImage == nil) {
NSString *defaultImageName = [NSString stringWithFormat:@"%@_en", curImage];
getImage = kImage(defaultImageName) ?: kImage(curImage);
}
return getImage;
}
+(NSString *)getLanguageText:(NSString *)image{
NSString *curImage = image;
NSString *language = [NSBundle getLanguageText];
if ([language isEqualToString:@"en"]){
image = [NSString stringWithFormat:@"%@_en",image];
} else if ([language isEqualToString:@"ar"]){
image = [NSString stringWithFormat:@"%@_ar",image];
} else if ([language isEqualToString:@"tr"]) { // 土耳其语默认使用英语内容
image = [NSString stringWithFormat:@"%@_en",image];
}
if (kImage(image) == nil){
return curImage;
}
return image;
}
- (UIImage *)imageByApplyingAlpha:(CGFloat)alpha {
UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0f);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGRect area = CGRectMake(0, 0, self.size.width, self.size.height);
CGContextScaleCTM(ctx, 1, -1);
CGContextTranslateCTM(ctx, 0, -area.size.height);
CGContextSetBlendMode(ctx, kCGBlendModeMultiply);
CGContextSetAlpha(ctx, alpha);
CGContextDrawImage(ctx, area, self.CGImage);
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
@end

View File

@@ -1,22 +0,0 @@
//
// NetImageConfig.h
// YUMI
//
// Created by zu on 2021/11/25.
//
#import <Foundation/Foundation.h>
#import "UIImageConstant.h"
NS_ASSUME_NONNULL_BEGIN
@interface NetImageConfig : NSObject
@property (nonatomic, assign) BOOL autoLoad;
@property (nonatomic, assign) ImageType imageType;
@property (nonatomic, assign) CGFloat radius;
@property (nonatomic, strong) UIImage * placeHolder;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,21 +0,0 @@
//
// NetImageConfig.m
// YUMI
//
// Created by zu on 2021/11/25.
//
#import "NetImageConfig.h"
@implementation NetImageConfig
- (instancetype)init
{
self = [super init];
if (self) {
_autoLoad = YES;
}
return self;
}
@end

View File

@@ -1,45 +0,0 @@
//
// NetImageView.h
// YUMI
//
// Created by zu on 2021/11/2.
//
#import <UIKit/UIKit.h>
#import "UIImageConstant.h"
#import "NetImageConfig.h"
NS_ASSUME_NONNULL_BEGIN
typedef void(^LoadCompletion)(UIImage *_Nullable image, NSURL * url);
typedef void(^LoadFail)(NSError *error);
typedef NS_ENUM(NSInteger, NetImageState){
NetImageStateUnload = 1,
NetImageStateLoading,
NetImageStateLoaded,
};
@interface NetImageView : UIImageView
@property (nonatomic, assign, readonly) NetImageState state;
@property (nonatomic, copy) NSString* imageUrl;
@property (nonatomic, assign) NSInteger backgroundLightType; // 0: non; 1: gold; 2: gray; 3: purple
- (instancetype)initWithUrl:(NSString * _Nonnull)imageUrl;
- (instancetype)initWithConfig:(NetImageConfig * _Nullable)config;
- (instancetype)initWithUrl:(NSString * _Nonnull)imageUrl config:(NetImageConfig * _Nullable)config;
- (UIImage *)lightImage:(NSInteger)type;
- (void)loadImage:(LoadCompletion _Nullable)completion;
- (void)loadImageWithUrl:(NSString * _Nonnull)imageUrl completion:(LoadCompletion _Nullable)completion;
- (void)loadImageWithUrl:(NSString * _Nonnull)imageUrl completion:(LoadCompletion _Nullable)completion fail:(LoadFail _Nullable)fail;
- (void)updateConfigPlaceHolder:(UIImage *)image;
- (void)cancelLoadImage;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,151 +0,0 @@
//
// NetImageView.m
// YUMI
//
// Created by zu on 2021/11/2.
//
#import "NetImageView.h"
#import <UIImageView+WebCache.h>
#import <SDImageCache.h>
@interface NetImageView()
@property (nonatomic, assign, readwrite) NetImageState state;
@property (nonatomic, copy) NSString * innerConfigUrl;
@property (nonatomic, strong) NetImageConfig * config;
@property (nonatomic, strong) UIImageView *lightImageView;
@end
@implementation NetImageView
- (instancetype)initWithUrl:(NSString *)url {
return [self initWithUrl:url config:nil];
}
- (instancetype)initWithConfig:(NetImageConfig *)config {
return [self initWithUrl:@"" config:config];
}
- (instancetype)initWithUrl:(NSString *)url config:(NetImageConfig *)config {
self = [super init];
if (self) {
_state = NetImageStateUnload;
_config = config;
if (_config.autoLoad) {
[self setImageUrl:url];
} else {
[self initImageUrl:url];
}
}
return self;
}
- (void)cancelLoadImage {
[self sd_cancelCurrentImageLoad];
}
- (void)initImageUrl:(NSString *)imageUrl {
_imageUrl = imageUrl;
_innerConfigUrl = [UIImageConstant configUrl:_imageUrl type:self.config.imageType radius:self.config.radius];
}
- (void)setBackgroundLightType:(NSInteger)backgroundLightType {
_backgroundLightType = backgroundLightType;
if (!_lightImageView) {
_lightImageView = [[UIImageView alloc] init];
[self insertSubview:_lightImageView atIndex:0];
[_lightImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self);
}];
}
switch (_backgroundLightType) {
case 1:
_lightImageView.image = [UIImage imageNamed:@"room_pk_result_avatar_bg_yellow"];
break;
case 2:
_lightImageView.image = [UIImage imageNamed:@"room_pk_result_avatar_bg_gray"];
break;
case 3:
_lightImageView.image = [UIImage imageNamed:@"room_pk_result_avatar_bg_purple"];
break;
default:
_lightImageView.image = nil;
break;
}
}
- (UIImage *)lightImage:(NSInteger)type {
switch (type) {
case 1:
return [UIImage imageNamed:@"room_pk_result_avatar_bg_yellow"];
case 2:
return [UIImage imageNamed:@"room_pk_result_avatar_bg_gray"];
case 3:
return [UIImage imageNamed:@"room_pk_result_avatar_bg_purple"];
default:
return nil;
}
}
- (void)setImageUrl:(NSString *)imageUrl {
[self initImageUrl:imageUrl];
[self loadImage:nil fail:nil];
}
- (void)loadImage:(LoadCompletion)completion {
[self loadImage:completion fail:nil];
}
- (void)loadImageWithUrl:(NSString *)imageUrl completion:(LoadCompletion)completion {
[self initImageUrl:imageUrl];
[self loadImage:completion fail:nil];
}
- (void)loadImageWithUrl:(NSString * _Nonnull)imageUrl completion:(LoadCompletion _Nullable)completion fail:(LoadFail _Nullable)fail{
[self initImageUrl:imageUrl];
[self loadImage:completion fail:fail];
}
- (void)loadImage:(LoadCompletion _Nullable)completion fail:(LoadFail _Nullable)fail{
self.state = NetImageStateLoading;
@kWeakify(self);
[self sd_setImageWithURL:[NSURL URLWithString:self.innerConfigUrl]
placeholderImage:self.config.placeHolder
options:SDWebImageRetryFailed | SDWebImageQueryMemoryData | SDWebImageQueryDiskDataSync
completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
@kStrongify(self);
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
self.state = NetImageStateUnload;
if (fail){
fail(error);
}
} else {
self.image = image;
self.state = NetImageStateLoaded;
if (completion) {
completion(image, imageURL);
};
}
});
}];
}
- (NetImageConfig *)config {
if (!_config) {
_config = [[NetImageConfig alloc] init];
}
return _config;
}
- (void)updateConfigPlaceHolder:(UIImage *)image {
self.config.placeHolder = image;
if (self.state == NetImageStateUnload) {
self.image = image;
}
}
@end

View File

@@ -1,53 +0,0 @@
//
// UIImageViewConstant.h
// YUMI
//
// Created by YUMI on 2021/9/17.
// 存放一些 加载图片 需要做的裁剪的key
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIImageConstant : NSObject
#pragma mark - 图片相关的
UIKIT_EXTERN NSString * const kImageTypeRoomFace; //房间表情
UIKIT_EXTERN NSString * const kImageTypeRoomGift; //房间礼物
UIKIT_EXTERN NSString * const kImageTypeUserIcon; //用户头像60x60
UIKIT_EXTERN NSString * const kImageTypeUserLibaryDetail;//用户相册大图nil
UIKIT_EXTERN NSString * const kImageTypeCornerAvatar;//圆角图形会先把图形裁剪成正方形并且转换为png
UIKIT_EXTERN NSString * const kImageTypeUserInfoAlbum;//用户信息里面相册
UIKIT_EXTERN NSString * const kImageTypeUserCardLevel;///用户资料卡中 等级以高度20等比例缩放
UIKIT_EXTERN NSString * const kImageTypeMonentsPhoto;///动态中的图片
typedef NS_ENUM(NSUInteger, ImageType){
ImageTypeRoomFace = 1, //房间表情
ImageTypeRoomGift, //房间礼物
ImageTypeUserIcon, //用户头像60x60
ImageTypeUserLibaryDetail, //用户相册大图
ImageTypeCornerAvatar, //圆角图形会先把图形裁剪成正方形并且转换为png
ImageTypeUserInfoAlbum, ///用户信息里面相册
ImageTypeUserCardLevel, /// 用户资料卡中 等级以高度20等比例缩放
ImageTypeMonentsPhoto, ///动态中的图片
};
///展位图
/// 头像的默认占位图
+ (UIImage *)defaultAvatarPlaceholder;
///空白头像缺省图
+ (UIImage *)defaultEmptyAvatarPlaceholder;
/// 空白图的占位图
+ (UIImage *)defaultEmptyPlaceholder;
+ (UIImage *)defaultEmptyPlaceholder_UFO;
/// banner的占位图
+ (UIImage *)defaultBannerPlaceholder;
+ (NSString*)configUrl:(NSString*)url type:(ImageType)type;
+ (NSString*)configUrl:(NSString*)url radius:(CGFloat)radius;
+ (NSString*)configUrl:(NSString*)url type:(ImageType)type radius:(CGFloat)radius;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,119 +0,0 @@
//
// UIImageViewConstant.m
// YUMI
//
// Created by YUMI on 2021/9/17.
//
#import "UIImageConstant.h"
@implementation UIImageConstant
/// 房间表情
NSString * const kImageTypeRoomFace = @"";
/// 房间礼物
NSString * const kImageTypeRoomGift = @"";
/// 用户头像150x150
NSString * const kImageTypeUserIcon = @"imageMogr2/auto-orient/thumbnail/150x150";
/// 用户相册大图
NSString * const kImageTypeUserLibaryDetail = @"imageMogr2/auto-orient/thumbnail/300x300";
NSString * const kImageTypeCornerAvatar = @"imageMogr2/auto-orient/thumbnail/300x300/format/png";
/// 用户信息里面相册
NSString * const kImageTypeUserInfoAlbum = @"imageMogr2/auto-orient/blur/375x375";
/// 用户信息里面相册
NSString * const kImageTypeUserCardLevel = @"imageMogr2/thumbnail/x40";
/// 动态中的图片 400 * 400
NSString * const kImageTypeMonentsPhoto = @"imageMogr2/auto-orient/thumbnail/400x400";
/// 头像的默认占位图
+ (UIImage *)defaultAvatarPlaceholder {
return [UIImage imageNamed:@"common_avatar"];
}
///空白头像缺省图
+ (UIImage *)defaultEmptyAvatarPlaceholder {
return [UIImage imageNamed:@"common_avatar"];
}
/// 空白图的占位图
+ (UIImage *)defaultEmptyPlaceholder {
return [UIImage imageNamed:@"common_empty"];
}
/// banner的占位图
+ (UIImage *)defaultBannerPlaceholder {
return [UIImage imageNamed:@"common_banner"];
}
+ (UIImage *)defaultEmptyPlaceholder_UFO {
return [UIImage imageNamed:@"common_empty_UFO"];
}
+ (NSString *)configUrl:(NSString *)url type:(ImageType)type {
return [self configUrl:url type:type radius:0];
}
+ (NSString *)configUrl:(NSString *)url radius:(CGFloat)radius {
return [self configUrl:url type:-1 radius:radius];
}
+ (NSString *)configUrl:(NSString *)url type:(ImageType)type radius:(CGFloat)radius {
if (!url || url.length <= 0) return nil;
NSMutableString *urlString = [NSMutableString stringWithString:url];
NSString *configUrl = nil;
switch (type) {
case ImageTypeUserIcon:
configUrl = kImageTypeUserIcon;
break;
case ImageTypeCornerAvatar:
configUrl = kImageTypeCornerAvatar;
break;
case ImageTypeRoomFace:
configUrl = kImageTypeRoomFace;
break;
case ImageTypeUserLibaryDetail:
configUrl = kImageTypeUserLibaryDetail;
break;
case ImageTypeRoomGift:
configUrl = kImageTypeRoomGift;
break;
case ImageTypeUserInfoAlbum:
configUrl = kImageTypeUserInfoAlbum;
break;
case ImageTypeUserCardLevel:
configUrl = kImageTypeUserCardLevel;
break;
case ImageTypeMonentsPhoto:
configUrl = kImageTypeMonentsPhoto;
break;
default:
break;
}
if (configUrl) {
if ([url containsString:@"?"]) {
[urlString appendString:@"|"];
}else{
[urlString appendString:@"?"];
}
[urlString appendString:configUrl];
}
if (radius > 0) {
[urlString appendString:[NSString stringWithFormat:@"|roundPic/radius/%f", radius]];
}
return percentEscapeString(urlString);
}
NSString *percentEscapeString(NSString *string) {
// 创建一个包含所有不需要百分比编码的字符集
NSMutableCharacterSet *allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
// 手动删除你想要百分比编码的字符, 其余的非字符将会变成带 % 的转义符
[allowedCharacterSet removeCharactersInString:@"|"];
return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
}
@end

View File

@@ -1,67 +0,0 @@
//
// UIView+Corner.h
// YUMI
//
// Created by YUMI on 2022/6/15.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIView (Corner)
- (void)setCornerWithLeftTopCorner:(CGFloat)leftTop
rightTopCorner:(CGFloat)rigtTop
bottomLeftCorner:(CGFloat)bottemLeft
bottomRightCorner:(CGFloat)bottemRight
size:(CGSize)size;
///**
// * 设置视图的圆角半径,边框宽度和颜色
// *
// * @param radius 圆角半径
// * @param corners 圆角位置 (可以组合使用 UIRectCornerTopLeft、UIRectCornerTopRight、UIRectCornerBottomLeft、UIRectCornerBottomRight)
// * @param borderWidth 边框宽度
// * @param borderColor 边框颜色
// */
//- (void)setCornerRadius:(CGFloat)radius
// corners:(UIRectCorner)corners
// borderWidth:(CGFloat)borderWidth
// borderColor:(UIColor *)borderColor;
/**
* 直接设置视图的圆角半径,应用到所有角
*
* @param radius 圆角半径
*/
- (void)setCornerRadius:(CGFloat)radius;
/**
* 直接设置视图的圆角半径和指定角
*
* @param radius 圆角半径
* @param corners 指定需要圆角的位置 (UIRectCornerTopLeft, UIRectCornerTopRight, etc.)
*/
- (void)setCornerRadius:(CGFloat)radius corners:(UIRectCorner)corners;
- (void)setCornerRadius:(CGFloat)radius cornerMask:(CACornerMask)cornerMask;
/**
* 设置视图的指定圆角、圆角半径、边框宽度和边框颜色
*
* @param radius 圆角半径
* @param corners 需要圆角的角位置 (可以组合,例如 `kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner`)
* @param borderWidth 边框宽度
* @param borderColor 边框颜色
*/
- (void)setCornerRadius:(CGFloat)radius
corners:(CACornerMask)corners
borderWidth:(CGFloat)borderWidth
borderColor:(UIColor *)borderColor;
- (void)setAllCornerRadius:(CGFloat)radius
borderWidth:(CGFloat)borderWidth
borderColor:(UIColor *)borderColor;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,125 +0,0 @@
//
// UIView+Corner.m
// YUMI
//
// Created by YUMI on 2022/6/15.
//
#import "UIView+Corner.h"
@implementation UIView (Corner)
- (void)setCornerWithLeftTopCorner:(CGFloat)leftTop
rightTopCorner:(CGFloat)rigtTop
bottomLeftCorner:(CGFloat)bottemLeft
bottomRightCorner:(CGFloat)bottemRight
size:(CGSize)size {
CGFloat width = size.width;
CGFloat height = size.height;
UIBezierPath *maskPath = [UIBezierPath bezierPath];
maskPath.lineWidth = 1.0;
maskPath.lineCapStyle = kCGLineCapRound;
maskPath.lineJoinStyle = kCGLineJoinRound;
[maskPath moveToPoint:CGPointMake(bottemRight, height)]; //左下角
[maskPath addLineToPoint:CGPointMake(width - bottemRight, height)];
[maskPath addQuadCurveToPoint:CGPointMake(width, height- bottemRight) controlPoint:CGPointMake(width, height)]; //右下角的圆弧
[maskPath addLineToPoint:CGPointMake(width, rigtTop)]; //右边直线
[maskPath addQuadCurveToPoint:CGPointMake(width - rigtTop, 0) controlPoint:CGPointMake(width, 0)]; //右上角圆弧
[maskPath addLineToPoint:CGPointMake(leftTop, 0)]; //顶部直线
[maskPath addQuadCurveToPoint:CGPointMake(0, leftTop) controlPoint:CGPointMake(0, 0)]; //左上角圆弧
[maskPath addLineToPoint:CGPointMake(0, height - bottemLeft)]; //左边直线
[maskPath addQuadCurveToPoint:CGPointMake(bottemLeft, height) controlPoint:CGPointMake(0, height)]; //左下角圆弧
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = CGRectMake(0, 0, size.width, size.height);
maskLayer.path = maskPath.CGPath;
self.layer.mask = maskLayer;
}
//- (void)setCornerRadius:(CGFloat)radius
// corners:(UIRectCorner)corners
// borderWidth:(CGFloat)borderWidth
// borderColor:(UIColor *)borderColor {
//
// // 创建 UIBezierPath 并应用圆角
// UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:self.bounds
// byRoundingCorners:corners
// cornerRadii:CGSizeMake(radius, radius)];
//
// // 创建 CAShapeLayer 并设置 path
// CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
// maskLayer.path = path.CGPath;
// self.layer.mask = maskLayer;
//
// // 设置边框
// if (borderWidth > 0 && borderColor) {
// CAShapeLayer *borderLayer = [[CAShapeLayer alloc] init];
// borderLayer.path = path.CGPath;
// borderLayer.lineWidth = borderWidth;
// borderLayer.strokeColor = borderColor.CGColor;
// borderLayer.fillColor = UIColor.clearColor.CGColor;
// borderLayer.frame = self.bounds;
// [self.layer addSublayer:borderLayer];
// }
//}
- (void)setCornerRadius:(CGFloat)radius {
self.layer.cornerRadius = radius;
self.layer.masksToBounds = YES; // 确保视图内容不会超出边界
}
- (void)setCornerRadius:(CGFloat)radius corners:(UIRectCorner)corners {
if (corners == UIRectCornerAllCorners) {
[self setCornerRadius:radius];
} else {
// 如果是部分圆角,使用 `CAShapeLayer` 和 `UIBezierPath`,但仅在必要时使用
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:self.bounds
byRoundingCorners:corners
cornerRadii:CGSizeMake(radius, radius)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.path = path.CGPath;
self.layer.mask = maskLayer;
}
}
- (void)setCornerRadius:(CGFloat)radius cornerMask:(CACornerMask)cornerMask {
self.layer.maskedCorners = cornerMask;
self.layer.cornerRadius = radius;
self.layer.masksToBounds = YES;
}
- (void)setCornerRadius:(CGFloat)radius
corners:(CACornerMask)corners
borderWidth:(CGFloat)borderWidth
borderColor:(UIColor *)borderColor {
// 设置指定角的圆角
self.layer.cornerRadius = radius;
self.layer.maskedCorners = corners;
self.layer.masksToBounds = YES; // 确保内容不会超出边界
// 设置边框
self.layer.borderWidth = borderWidth;
self.layer.borderColor = borderColor.CGColor;
}
- (void)setAllCornerRadius:(CGFloat)radius
borderWidth:(CGFloat)borderWidth
borderColor:(UIColor *)borderColor {
// 设置指定角的圆角
self.layer.cornerRadius = radius;
self.layer.maskedCorners = UIRectCornerTopLeft | UIRectCornerTopRight | UIRectCornerBottomLeft | UIRectCornerBottomRight;
self.layer.masksToBounds = YES; // 确保内容不会超出边界
// 设置边框
self.layer.borderWidth = borderWidth;
self.layer.borderColor = borderColor.CGColor;
}
@end

View File

@@ -1,37 +0,0 @@
//
// UIView+GradientLayer.h
// YuMi
//
// Created by P on 2024/11/18.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIView (GradientLayer)
/// 为视图添加渐变背景
/// @param colors 渐变颜色数组 (NSArray<UIColor *> *)
/// @param startPoint 渐变起点 (CGPoint)
/// @param endPoint 渐变终点 (CGPoint)
/// @param cornerRadius 圆角半径 (CGFloat)
- (void)addGradientBackgroundWithColors:(NSArray<UIColor *> *)colors
startPoint:(CGPoint)startPoint
endPoint:(CGPoint)endPoint
cornerRadius:(CGFloat)cornerRadius;
/// 移除渐变背景
- (void)removeGradientBackground;
/// 更新渐变背景
/// @param colors 渐变颜色数组 (NSArray<UIColor *> *)
/// @param startPoint 渐变起点 (CGPoint)
/// @param endPoint 渐变终点 (CGPoint)
- (void)updateGradientBackgroundWithColors:(NSArray<UIColor *> *)colors
startPoint:(CGPoint)startPoint
endPoint:(CGPoint)endPoint;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,120 +0,0 @@
//
// UIView+GradientLayer.m
// YuMi
//
// Created by P on 2024/11/18.
//
#import "UIView+GradientLayer.h"
#import <QuartzCore/QuartzCore.h>
@implementation UIView (GradientLayer)
static NSString * const kGradientLayerName = @"GradientLayer";
static void *kGradientObserverKey = &kGradientObserverKey;
// 添加渐变背景
- (void)addGradientBackgroundWithColors:(NSArray<UIColor *> *)colors
startPoint:(CGPoint)startPoint
endPoint:(CGPoint)endPoint
cornerRadius:(CGFloat)cornerRadius {
// 确保移除已有的渐变背景和 KVO 监听
[self removeGradientBackground];
// 创建渐变图层
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.name = kGradientLayerName;
gradientLayer.colors = [self cgColorsFromUIColors:colors];
gradientLayer.startPoint = startPoint;
gradientLayer.endPoint = endPoint;
gradientLayer.cornerRadius = cornerRadius;
// 初次设置 frame
gradientLayer.frame = self.bounds;
[self.layer insertSublayer:gradientLayer atIndex:0];
// 添加 KVO 监听
[self setupLayoutObserverForGradientLayer:gradientLayer];
}
// 动态更新渐变背景
- (void)updateGradientBackgroundWithColors:(NSArray<UIColor *> *)colors
startPoint:(CGPoint)startPoint
endPoint:(CGPoint)endPoint {
CAGradientLayer *gradientLayer = [self gradientLayer];
if (gradientLayer) {
gradientLayer.colors = [self cgColorsFromUIColors:colors];
gradientLayer.startPoint = startPoint;
gradientLayer.endPoint = endPoint;
}
}
// 移除渐变背景和监听
- (void)removeGradientBackground {
// 移除渐变图层
CAGradientLayer *gradientLayer = [self gradientLayer];
if (gradientLayer) {
[gradientLayer removeFromSuperlayer];
}
// 移除 KVO 监听
if ([self hasAddedObserver]) {
[self removeObserver:self forKeyPath:@"bounds"];
[self setHasAddedObserver:NO];
}
}
// 获取渐变图层
- (CAGradientLayer *)gradientLayer {
for (CALayer *layer in self.layer.sublayers) {
if ([layer.name isEqualToString:kGradientLayerName] &&
[layer isKindOfClass:[CAGradientLayer class]]) {
return (CAGradientLayer *)layer;
}
}
return nil;
}
// 添加 KVO 监听
- (void)setupLayoutObserverForGradientLayer:(CAGradientLayer *)gradientLayer {
if (![self hasAddedObserver]) {
[self addObserver:self forKeyPath:@"bounds" options:NSKeyValueObservingOptionNew context:(__bridge void *)gradientLayer];
[self setHasAddedObserver:YES];
}
}
// KVO 监听回调
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
if ([keyPath isEqualToString:@"bounds"]) {
CAGradientLayer *gradientLayer = (__bridge CAGradientLayer *)context;
gradientLayer.frame = self.bounds; // 更新渐变图层的 frame
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
// 工具方法:转换颜色数组
- (NSArray *)cgColorsFromUIColors:(NSArray<UIColor *> *)colors {
NSMutableArray *cgColors = [NSMutableArray array];
for (UIColor *color in colors) {
[cgColors addObject:(id)color.CGColor];
}
return [cgColors copy];
}
// 工具方法:判断是否已注册 KVO
- (BOOL)hasAddedObserver {
return [objc_getAssociatedObject(self, kGradientObserverKey) boolValue];
}
// 工具方法:设置是否已注册 KVO
- (void)setHasAddedObserver:(BOOL)added {
objc_setAssociatedObject(self, kGradientObserverKey, @(added), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end

View File

@@ -1,108 +0,0 @@
/*
File: UIImage+ImageEffects.h
Abstract: This is a category of UIImage that adds methods to apply blur and tint effects to an image. This is the code youll want to look out to find out how to use vImage to efficiently calculate a blur.
Version: 1.0
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple
Inc. ("Apple") in consideration of your agreement to the following
terms, and your use, installation, modification or redistribution of
this Apple software constitutes acceptance of these terms. If you do
not agree with these terms, please do not use, install, modify or
redistribute this Apple software.
In consideration of your agreement to abide by the following terms, and
subject to these terms, Apple grants you a personal, non-exclusive
license, under Apple's copyrights in this original Apple software (the
"Apple Software"), to use, reproduce, modify and redistribute the Apple
Software, with or without modifications, in source and/or binary forms;
provided that if you redistribute the Apple Software in its entirety and
without modifications, you must retain this notice and the following
text and disclaimers in all such redistributions of the Apple Software.
Neither the name, trademarks, service marks or logos of Apple Inc. may
be used to endorse or promote products derived from the Apple Software
without specific prior written permission from Apple. Except as
expressly stated in this notice, no other rights or licenses, express or
implied, are granted by Apple herein, including but not limited to any
patent rights that may be infringed by your derivative works or by other
works in which the Apple Software may be incorporated.
The Apple Software is provided by Apple on an "AS IS" basis. APPLE
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
Copyright (C) 2013 Apple Inc. All Rights Reserved.
Copyright © 2013 Apple Inc. All rights reserved.
WWDC 2013 License
NOTE: This Apple Software was supplied by Apple as part of a WWDC 2013
Session. Please refer to the applicable WWDC 2013 Session for further
information.
IMPORTANT: This Apple software is supplied to you by Apple Inc.
("Apple") in consideration of your agreement to the following terms, and
your use, installation, modification or redistribution of this Apple
software constitutes acceptance of these terms. If you do not agree with
these terms, please do not use, install, modify or redistribute this
Apple software.
In consideration of your agreement to abide by the following terms, and
subject to these terms, Apple grants you a non-exclusive license, under
Apple's copyrights in this original Apple software (the "Apple
Software"), to use, reproduce, modify and redistribute the Apple
Software, with or without modifications, in source and/or binary forms;
provided that if you redistribute the Apple Software in its entirety and
without modifications, you must retain this notice and the following
text and disclaimers in all such redistributions of the Apple Software.
Neither the name, trademarks, service marks or logos of Apple Inc. may
be used to endorse or promote products derived from the Apple Software
without specific prior written permission from Apple. Except as
expressly stated in this notice, no other rights or licenses, express or
implied, are granted by Apple herein, including but not limited to any
patent rights that may be infringed by your derivative works or by other
works in which the Apple Software may be incorporated.
The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES
NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
EA1002
5/3/2013
*/
#import <UIKit/UIKit.h>
@interface UIImage (ImageEffects)
- (UIImage *)applyLightEffect;
- (UIImage *)applyExtraLightEffect;
- (UIImage *)applyDarkEffect;
- (UIImage *)applyTintEffectWithColor:(UIColor *)tintColor;
- (UIImage *)applyBlurWithRadius:(CGFloat)blurRadius tintColor:(UIColor *)tintColor saturationDeltaFactor:(CGFloat)saturationDeltaFactor maskImage:(UIImage *)maskImage;
///为图片增加毛玻璃,value模糊程度
+(UIImage *)setBlurImage:(UIImage *)image value:(CGFloat)value;
@end

View File

@@ -1,309 +0,0 @@
/*
File: UIImage+ImageEffects.m
Abstract: This is a category of UIImage that adds methods to apply blur and tint effects to an image. This is the code youll want to look out to find out how to use vImage to efficiently calculate a blur.
Version: 1.0
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple
Inc. ("Apple") in consideration of your agreement to the following
terms, and your use, installation, modification or redistribution of
this Apple software constitutes acceptance of these terms. If you do
not agree with these terms, please do not use, install, modify or
redistribute this Apple software.
In consideration of your agreement to abide by the following terms, and
subject to these terms, Apple grants you a personal, non-exclusive
license, under Apple's copyrights in this original Apple software (the
"Apple Software"), to use, reproduce, modify and redistribute the Apple
Software, with or without modifications, in source and/or binary forms;
provided that if you redistribute the Apple Software in its entirety and
without modifications, you must retain this notice and the following
text and disclaimers in all such redistributions of the Apple Software.
Neither the name, trademarks, service marks or logos of Apple Inc. may
be used to endorse or promote products derived from the Apple Software
without specific prior written permission from Apple. Except as
expressly stated in this notice, no other rights or licenses, express or
implied, are granted by Apple herein, including but not limited to any
patent rights that may be infringed by your derivative works or by other
works in which the Apple Software may be incorporated.
The Apple Software is provided by Apple on an "AS IS" basis. APPLE
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
Copyright (C) 2013 Apple Inc. All Rights Reserved.
Copyright © 2013 Apple Inc. All rights reserved.
WWDC 2013 License
NOTE: This Apple Software was supplied by Apple as part of a WWDC 2013
Session. Please refer to the applicable WWDC 2013 Session for further
information.
IMPORTANT: This Apple software is supplied to you by Apple Inc.
("Apple") in consideration of your agreement to the following terms, and
your use, installation, modification or redistribution of this Apple
software constitutes acceptance of these terms. If you do not agree with
these terms, please do not use, install, modify or redistribute this
Apple software.
In consideration of your agreement to abide by the following terms, and
subject to these terms, Apple grants you a non-exclusive license, under
Apple's copyrights in this original Apple software (the "Apple
Software"), to use, reproduce, modify and redistribute the Apple
Software, with or without modifications, in source and/or binary forms;
provided that if you redistribute the Apple Software in its entirety and
without modifications, you must retain this notice and the following
text and disclaimers in all such redistributions of the Apple Software.
Neither the name, trademarks, service marks or logos of Apple Inc. may
be used to endorse or promote products derived from the Apple Software
without specific prior written permission from Apple. Except as
expressly stated in this notice, no other rights or licenses, express or
implied, are granted by Apple herein, including but not limited to any
patent rights that may be infringed by your derivative works or by other
works in which the Apple Software may be incorporated.
The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES
NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
EA1002
5/3/2013
*/
#import "UIImage+ImageEffects.h"
#import <Accelerate/Accelerate.h>
#import <float.h>
@implementation UIImage (ImageEffects)
- (UIImage *)applyLightEffect
{
UIColor *tintColor = [UIColor colorWithWhite:1.0 alpha:0.3];
return [self applyBlurWithRadius:30 tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil];
}
- (UIImage *)applyExtraLightEffect
{
UIColor *tintColor = [UIColor colorWithWhite:0.97 alpha:0.82];
return [self applyBlurWithRadius:20 tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil];
}
- (UIImage *)applyDarkEffect
{
UIColor *tintColor = [UIColor colorWithWhite:0.11 alpha:0.73];
return [self applyBlurWithRadius:20 tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil];
}
- (UIImage *)applyTintEffectWithColor:(UIColor *)tintColor
{
const CGFloat EffectColorAlpha = 0.6;
UIColor *effectColor = tintColor;
int componentCount = CGColorGetNumberOfComponents(tintColor.CGColor);
if (componentCount == 2) {
CGFloat b;
if ([tintColor getWhite:&b alpha:NULL]) {
effectColor = [UIColor colorWithWhite:b alpha:EffectColorAlpha];
}
}
else {
CGFloat r, g, b;
if ([tintColor getRed:&r green:&g blue:&b alpha:NULL]) {
effectColor = [UIColor colorWithRed:r green:g blue:b alpha:EffectColorAlpha];
}
}
return [self applyBlurWithRadius:10 tintColor:effectColor saturationDeltaFactor:-1.0 maskImage:nil];
}
- (UIImage *)applyBlurWithRadius:(CGFloat)blurRadius tintColor:(UIColor *)tintColor saturationDeltaFactor:(CGFloat)saturationDeltaFactor maskImage:(UIImage *)maskImage
{
// Check pre-conditions.
if (self.size.width < 1 || self.size.height < 1) {
// NSLog (@"*** error: invalid size: (%.2f x %.2f). Both dimensions must be >= 1: %@", self.size.width, self.size.height, self);
return nil;
}
if (!self.CGImage) {
// NSLog (@"*** error: image must be backed by a CGImage: %@", self);
return nil;
}
if (maskImage && !maskImage.CGImage) {
// NSLog (@"*** error: maskImage must be backed by a CGImage: %@", maskImage);
return nil;
}
CGRect imageRect = { CGPointZero, self.size };
UIImage *effectImage = self;
BOOL hasBlur = blurRadius > __FLT_EPSILON__;
BOOL hasSaturationChange = fabs(saturationDeltaFactor - 1.) > __FLT_EPSILON__;
if (hasBlur || hasSaturationChange) {
UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]);
CGContextRef effectInContext = UIGraphicsGetCurrentContext();
CGContextScaleCTM(effectInContext, 1.0, -1.0);
CGContextTranslateCTM(effectInContext, 0, -self.size.height);
CGContextDrawImage(effectInContext, imageRect, self.CGImage);
vImage_Buffer effectInBuffer;
effectInBuffer.data = CGBitmapContextGetData(effectInContext);
effectInBuffer.width = CGBitmapContextGetWidth(effectInContext);
effectInBuffer.height = CGBitmapContextGetHeight(effectInContext);
effectInBuffer.rowBytes = CGBitmapContextGetBytesPerRow(effectInContext);
UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]);
CGContextRef effectOutContext = UIGraphicsGetCurrentContext();
vImage_Buffer effectOutBuffer;
effectOutBuffer.data = CGBitmapContextGetData(effectOutContext);
effectOutBuffer.width = CGBitmapContextGetWidth(effectOutContext);
effectOutBuffer.height = CGBitmapContextGetHeight(effectOutContext);
effectOutBuffer.rowBytes = CGBitmapContextGetBytesPerRow(effectOutContext);
if (hasBlur) {
// A description of how to compute the box kernel width from the Gaussian
// radius (aka standard deviation) appears in the SVG spec:
// http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement
//
// For larger values of 's' (s >= 2.0), an approximation can be used: Three
// successive box-blurs build a piece-wise quadratic convolution kernel, which
// approximates the Gaussian kernel to within roughly 3%.
//
// let d = floor(s * 3*sqrt(2*pi)/4 + 0.5)
//
// ... if d is odd, use three box-blurs of size 'd', centered on the output pixel.
//
CGFloat inputRadius = blurRadius * [[UIScreen mainScreen] scale];
NSUInteger radius = floor(inputRadius * 3. * sqrt(2 * M_PI) / 4 + 0.5);
if (radius % 2 != 1) {
radius += 1; // force radius to be odd so that the three box-blur methodology works.
}
vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend);
vImageBoxConvolve_ARGB8888(&effectOutBuffer, &effectInBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend);
vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend);
}
BOOL effectImageBuffersAreSwapped = NO;
if (hasSaturationChange) {
CGFloat s = saturationDeltaFactor;
CGFloat floatingPointSaturationMatrix[] = {
0.0722 + 0.9278 * s, 0.0722 - 0.0722 * s, 0.0722 - 0.0722 * s, 0,
0.7152 - 0.7152 * s, 0.7152 + 0.2848 * s, 0.7152 - 0.7152 * s, 0,
0.2126 - 0.2126 * s, 0.2126 - 0.2126 * s, 0.2126 + 0.7873 * s, 0,
0, 0, 0, 1,
};
const int32_t divisor = 256;
NSUInteger matrixSize = sizeof(floatingPointSaturationMatrix)/sizeof(floatingPointSaturationMatrix[0]);
int16_t saturationMatrix[matrixSize];
for (NSUInteger i = 0; i < matrixSize; ++i) {
saturationMatrix[i] = (int16_t)roundf(floatingPointSaturationMatrix[i] * divisor);
}
if (hasBlur) {
vImageMatrixMultiply_ARGB8888(&effectOutBuffer, &effectInBuffer, saturationMatrix, divisor, NULL, NULL, kvImageNoFlags);
effectImageBuffersAreSwapped = YES;
}
else {
vImageMatrixMultiply_ARGB8888(&effectInBuffer, &effectOutBuffer, saturationMatrix, divisor, NULL, NULL, kvImageNoFlags);
}
}
if (!effectImageBuffersAreSwapped)
effectImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if (effectImageBuffersAreSwapped)
effectImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
// Set up output context.
UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]);
CGContextRef outputContext = UIGraphicsGetCurrentContext();
CGContextScaleCTM(outputContext, 1.0, -1.0);
CGContextTranslateCTM(outputContext, 0, -self.size.height);
// Draw base image.
CGContextDrawImage(outputContext, imageRect, self.CGImage);
// Draw effect image.
if (hasBlur) {
CGContextSaveGState(outputContext);
if (maskImage) {
CGContextClipToMask(outputContext, imageRect, maskImage.CGImage);
}
CGContextDrawImage(outputContext, imageRect, effectImage.CGImage);
CGContextRestoreGState(outputContext);
}
// Add in color tint.
if (tintColor) {
CGContextSaveGState(outputContext);
CGContextSetFillColorWithColor(outputContext, tintColor.CGColor);
CGContextFillRect(outputContext, imageRect);
CGContextRestoreGState(outputContext);
}
// Output image is ready.
UIImage *outputImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return outputImage;
}
+(UIImage *)setBlurImage:(UIImage *)image value:(CGFloat)value{
CIContext *context = [CIContext contextWithOptions:nil];
CIImage * sourceImage = [CIImage imageWithCGImage:image.CGImage];//将图片转换成CIImage
///图片仿射滤镜
CIFilter * clamp = [CIFilter filterWithName:@"CIAffineClamp"];//设置绘制类型
[clamp setValue:sourceImage forKey:kCIInputImageKey];//设置要绘制的图片
CIImage *clampResult = [clamp valueForKey:kCIOutputImageKey];
///高斯模糊滤镜
CIFilter* gaussianBlur = [CIFilter filterWithName:@"CIGaussianBlur"];
[gaussianBlur setValue:clampResult forKey:kCIInputImageKey];
[gaussianBlur setValue:[NSNumber numberWithFloat:value] forKey:@"inputRadius"];//设置模糊值
CIImage * gaussianBlurResult = [gaussianBlur valueForKey:kCIOutputImageKey];
///转化获取图片
CGImageRef cgImage = [context createCGImage:gaussianBlurResult fromRect:[sourceImage extent]];
UIImage * resultImage = [UIImage imageWithCGImage:cgImage];
return resultImage;
}
@end

View File

@@ -1,31 +0,0 @@
//
// YMCurrentVCStackManager.h
// YMBaseUIKit
//
// Created by 卫明何 on 2018/8/9.
// Copyright © 2018年 YUIMI. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface XCCurrentVCStackManager : NSObject
+ (instancetype)shareManager;
/**
当前的导航控制器
@return 导航控制器
*/
- (UINavigationController *)currentNavigationController;
/**
当前最顶层控制器
@return 当前最顶层控制器
*/
- (UIViewController *)getCurrentVC;
@end

View File

@@ -1,123 +0,0 @@
//
// YMCurrentVCStackManager.m
// YMBaseUIKit
//
// Created by 卫明何 on 2018/8/9.
// Copyright © 2018年 YUIMI. All rights reserved.
//
#import "XCCurrentVCStackManager.h"
NSString * const kRoomChatPushViewKey = @"kRoomChatPushViewKey";
@implementation XCCurrentVCStackManager
+ (instancetype)shareManager {
static dispatch_once_t onceToken = 0;
static id instance;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (UIViewController *)getCurrentVC {
///兼容房间内私聊的
[[NSNotificationCenter defaultCenter] postNotificationName:kRoomChatPushViewKey object:nil];
UIViewController *rootViewController = [UIApplication sharedApplication].delegate.window.rootViewController;
UIViewController *currentVC = [self getCurrentVCFrom:rootViewController];
return currentVC;
}
- (UIViewController *)getCurrentVCFrom:(UIViewController *)rootVC {
UIViewController *currentVC;
if ([rootVC presentedViewController]) {
// 视图是被presented出来的
rootVC = [rootVC presentedViewController];
}
if ([rootVC isKindOfClass:[UITabBarController class]]) {
// 根视图为UITabBarController
currentVC = [self getCurrentVCFrom:[(UITabBarController *)rootVC selectedViewController]];
} else if ([rootVC isKindOfClass:[UINavigationController class]]) {
// 根视图为UINavigationController
currentVC = [self getCurrentVCFrom:[(UINavigationController *)rootVC visibleViewController]];
} else {
// 根视图为非导航类
currentVC = rootVC;
}
return currentVC;
}
- (UIViewController *)currentViewController {
UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
UIViewController *vc = keyWindow.rootViewController;
while (vc.presentedViewController) {
vc = vc.presentedViewController;
if ([vc isKindOfClass:[UINavigationController class]]) {
vc = [(UINavigationController *)vc visibleViewController];
}
else if ([vc isKindOfClass:[UITabBarController class]]) {
vc = [(UITabBarController *)vc selectedViewController];
}
}
return vc;
}
- (UINavigationController *)currentNavigationController {
return [self currentNC];
}
- (UINavigationController *)currentNC{
if (![[UIApplication sharedApplication].windows.lastObject isKindOfClass:[UIWindow class]]) {
NSAssert(0, @"未获取到导航控制器");
return nil;
}
UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
return [self getCurrentNCFrom:rootViewController];
}
//递归
- (UINavigationController *)getCurrentNCFrom:(UIViewController *)vc{
if ([vc isKindOfClass:NSClassFromString(@"MMDrawerController")]) {
vc = (UIViewController *)[vc valueForKey:@"centerViewController"];
}
if ([vc isKindOfClass:[UITabBarController class]]) {
UINavigationController *nc = ((UITabBarController *)vc).selectedViewController;
return [self getCurrentNCFrom:nc];
}
else if ([vc isKindOfClass:[UINavigationController class]]) {
if (((UINavigationController *)vc).presentedViewController) {
return [self getCurrentNCFrom:((UINavigationController *)vc).presentedViewController];
}
return [self getCurrentNCFrom:((UINavigationController *)vc).topViewController];
}
else if ([vc isKindOfClass:[UIViewController class]]) {
if (vc.presentedViewController) {
return [self getCurrentNCFrom:vc.presentedViewController];
}
else {
if (!vc.navigationController) {
if (vc.presentingViewController) {
[vc dismissViewControllerAnimated:NO completion:nil];
return [self getCurrentNCFrom:vc.presentingViewController];
} else {
NSAssert(0, @"未获取到导航控制器");
return nil;
}
} else {
return vc.navigationController;
}
}
}
else {
NSAssert(0, @"未获取到导航控制器");
return nil;
}
}
@end

View File

@@ -1,148 +0,0 @@
//
// YMHUDTool.h
// TTPlay
//
// Created by YM on 2022/5/15.
// Copyright © 2023 YUMI. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/**
showGIFLoading使用注意:
1.谁负责showLoading, 谁负责hideHUD
2.showLoading是指定了加载在那个View, hideHUD时请指定hide那个view的hud
*/
@interface XNDJTDDLoadingTool : NSObject
/**
隐藏HUD
*/
+ (void)hideHUD;
/**
隐藏HUD, 如果view为nil, 则默认隐藏主窗口的HUD
@param view view
*/
+ (void)hideHUDInView:(nullable UIView *)view;
+(void)hideOnlyView:(UIView *)view;
+(void)showOnlyView:(UIView *)view;
/**
显示成功message, 默认显示在窗口上, 2.5s后消失, 默认不拦截点击事件
@param message 文字
*/
+ (void)showSuccessWithMessage:(NSString *)message;
/**
显示成功message, 2.5s后消失, 默认不拦截点击事件
@param message 文字
@param view 显示在哪个view上
*/
+ (void)showSuccessWithMessage:(NSString *)message inView:(nullable UIView *)view;
/**
显示成功message
@param message 文字
@param view 显示在哪个view上
@param afterDelay 延迟消失时间
@param enabled 是否可以拦截事件 no:不拦截 yes:拦截
*/
+ (void)showSuccessWithMessage:(NSString *)message inView:(nullable UIView *)view delay:(NSTimeInterval)afterDelay enabled:(BOOL)enabled;
/**
显示错误message, 默认显示在窗口上, 2.5s后消失, 默认不拦截点击事件
@param message 文字
*/
+ (void)showErrorWithMessage:(NSString *)message;
/**
显示错误message, 2.5s后消失, 默认不拦截点击事件
@param message 文字
@param view 显示在哪个view上
*/
+ (void)showErrorWithMessage:(NSString *)message inView:(nullable UIView *)view;
/**
显示错误message
@param message 文字
@param view 显示在哪个view上
@param afterDelay 延迟消失时间
@param enabled 是否可以拦截事件 no:不拦截 yes:拦截
*/
+ (void)showErrorWithMessage:(NSString *)message inView:(nullable UIView *)view delay:(NSTimeInterval)afterDelay enabled:(BOOL)enabled;
/**
在窗口上显示自定义GIFLoading, 背景默认黑色0.35透明度, 默认拦截点击事件
*/
+ (void)showGIFLoading;
/**
在指定的view上显示自定义GIFLoading, 背景默认黑色0.35透明度, 默认拦截点击事件
@param view 显示在哪个view上
*/
+ (void)showGIFLoadingInView:(nullable UIView *)view;
/**
在指定的view上显示自定义GIFLoading
@param view 显示在哪个view上
@param bgColor 背景颜色, 遮盖
@param enabled 是否可以拦截事件 no:不拦截 yes:拦截
*/
+ (void)showGIFLoadingInView:(nullable UIView *)view bgColor:(nullable UIColor *)bgColor enabled:(BOOL)enabled;
/**
加载下一个个播房
*/
+ (void)showAnchorLoading;
+ (void)showAnchorLoading:(UIView *)view;
/**
在窗口上显示菊花
*/
+ (void)showLoading;
/**
在view上显示菊花
*/
+ (void)showLoadingInView:(nullable UIView *)view;
/**
在view上显示菊花
*/
+ (void)showLoadingInView:(nullable UIView *)view enabled:(BOOL)enabled;
/**
在窗口上显示菊花+文字
*/
+ (void)showLoadingWithMessage:(NSString *)message;
/**
在view上显示菊花+文字
*/
+ (void)showLoadingWithMessage:(NSString *)message inView:(nullable UIView *)view;
/**
在view上显示菊花+文字
*/
+ (void)showLoadingWithMessage:(NSString *)message inView:(nullable UIView *)view enabled:(BOOL)enabled;
+(void)showOnlyView:(UIView *)view enabled:(BOOL)enabled;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,391 +0,0 @@
//
// YMHUDTool.m
// TTPlay
//
// Created by YM on 2022/5/15.
// Copyright © 2023 YUMI. All rights reserved.
//
#import "XNDJTDDLoadingTool.h"
#import "GCDHelper.h"
#import <MBProgressHUD/MBProgressHUD.h>
#import "MvpViewController.h"
#define kDelayTime 2.5
@interface XNDJTDDLoadingTool ()
///
@property (class,nonatomic,copy) NSArray *animationImages;
@end
@implementation XNDJTDDLoadingTool
static NSArray * _animationImages = nil;
/**
隐藏HUD, 如果view为nil, 则默认隐藏主窗口的HUD
@param view view
*/
+ (void)hideHUDInView:(nullable UIView *)view {
dispatch_main_sync_safe(^{
if (view) {
MBProgressHUD *hud = [XNDJTDDLoadingTool HUDForView:view];
if (hud != nil && hud.mode != MBProgressHUDModeText) {
hud.removeFromSuperViewOnHide = YES;
[hud hideAnimated:NO];
}
MBProgressHUD *windowHud = [XNDJTDDLoadingTool HUDForView:kWindow];
if (windowHud != nil && windowHud.mode != MBProgressHUDModeText) {
windowHud.removeFromSuperViewOnHide = YES;
[windowHud hideAnimated:NO];
}
} else {
MBProgressHUD *windowHud = [XNDJTDDLoadingTool HUDForView:kWindow];
if (windowHud != nil && windowHud.mode != MBProgressHUDModeText) {
windowHud.removeFromSuperViewOnHide = YES;
[windowHud hideAnimated:NO];
}
}
});
}
+ (MBProgressHUD *)HUDForView:(UIView *)view {
NSEnumerator *subviewsEnum = [view.subviews reverseObjectEnumerator];
for (UIView *subview in subviewsEnum) {
if ([subview isKindOfClass:[MBProgressHUD class]]) {
MBProgressHUD *hud = (MBProgressHUD *)subview;
id hasFinished = [hud valueForKey:@"hasFinished"];
if (hasFinished != nil) {
if([hasFinished boolValue] == NO){
return hud;
}
}
}
}
return nil;
}
/**
隐藏HUD
*/
+ (void)hideHUD {
[self hideHUDInView:nil];
}
/**
显示成功message, 默认显示在窗口上, 2.5s后消失, 默认不拦截点击事件
@param message 文字
*/
+ (void)showSuccessWithMessage:(NSString *)message {
[self showSuccessWithMessage:message inView:[UIApplication sharedApplication].keyWindow];
}
/**
显示成功message, 2.5s后消失, 默认不拦截点击事件
@param message 文字
@param view 显示在哪个view上
*/
+ (void)showSuccessWithMessage:(NSString *)message inView:(nullable UIView *)view {
[self showSuccessWithMessage:message inView:view delay:kDelayTime enabled:NO];
}
/**
显示成功message
@param message 文字
@param view 显示在哪个view上
@param afterDelay 延迟消失时间
@param enabled 是否可以拦截事件 no:不拦截 yes:拦截
*/
+ (void)showSuccessWithMessage:(NSString *)message inView:(nullable UIView *)view delay:(NSTimeInterval)afterDelay enabled:(BOOL)enabled {
if (message.length == 0) { return; }
__block UIView *inView = view;
dispatch_main_sync_safe(^{
if (!inView) {
inView = [UIApplication sharedApplication].keyWindow;
}
[self hideHUDInView:view]; // 先隐藏
MBProgressHUD *hud = [self normalProgressHUD:view];
hud.userInteractionEnabled = enabled;
hud.mode = MBProgressHUDModeText;
hud.bezelView.style = MBProgressHUDBackgroundStyleSolidColor;
hud.margin = 8;
// 方框背景颜色
hud.bezelView.color = [[UIColor blackColor] colorWithAlphaComponent:0.8];
hud.label.text = message;
hud.label.numberOfLines = 0;
hud.label.textColor = [UIColor whiteColor];
hud.label.font = [UIFont systemFontOfSize:14];
[hud hideAnimated:YES afterDelay:afterDelay];
});
}
/**
显示错误message, 默认显示在窗口上, 2.5s后消失, 默认不拦截点击事件
@param message 文字
*/
+ (void)showErrorWithMessage:(NSString *)message {
[self showErrorWithMessage:message inView:[UIApplication sharedApplication].keyWindow];
}
/**
显示错误message, 2.5s后消失, 默认不拦截点击事件
@param message 文字
@param view 显示在哪个view上
*/
+ (void)showErrorWithMessage:(NSString *)message inView:(nullable UIView *)view {
[self showErrorWithMessage:message inView:view delay:kDelayTime enabled:NO];
}
/**
显示错误message
@param message 文字
@param view 显示在哪个view上
@param afterDelay 延迟消失时间
@param enabled 是否可以拦截事件 no:不拦截 yes:拦截
*/
+ (void)showErrorWithMessage:(NSString *)message inView:(nullable UIView *)view delay:(NSTimeInterval)afterDelay enabled:(BOOL)enabled {
if (message.length == 0) { return; }
if (!view) {
view = [UIApplication sharedApplication].keyWindow;
}
[self hideHUDInView:view]; // 先隐藏
dispatch_main_sync_safe(^{
MBProgressHUD *hud = [self normalProgressHUD:view];
hud.userInteractionEnabled = enabled;
hud.mode = MBProgressHUDModeText;
hud.bezelView.style = MBProgressHUDBackgroundStyleSolidColor;
hud.margin = 8;
// 方框背景颜色
hud.bezelView.color = [[UIColor blackColor] colorWithAlphaComponent:0.8];
hud.label.text = message;
hud.label.numberOfLines = 0;
hud.label.textColor = [UIColor whiteColor];
hud.label.font = [UIFont systemFontOfSize:14];
[hud hideAnimated:YES afterDelay:afterDelay];
});
}
/**
* 在窗口上显示菊花
*/
+ (void)showLoading {
[self showGIFLoadingInView:[UIApplication sharedApplication].keyWindow];
return;
[self showLoadingInView:[UIApplication sharedApplication].keyWindow];
}
/**
* 在view上显示菊花
*/
+ (void)showLoadingInView:(nullable UIView *)view {
[self showGIFLoadingInView:[UIApplication sharedApplication].keyWindow];
return;
[self showLoadingInView:view enabled:YES];
}
/**
* 在view上显示菊花
*/
+ (void)showLoadingInView:(nullable UIView *)view enabled:(BOOL)enabled {
[self showGIFLoadingInView:[UIApplication sharedApplication].keyWindow];
return;
[self showLoadingWithMessage:@"" inView:view enabled:enabled];
}
/**
* 在窗口上显示菊花+文字
*/
+ (void)showLoadingWithMessage:(NSString *)message {
[self showLoadingWithMessage:message inView:[UIApplication sharedApplication].keyWindow];
}
/**
* 在view上显示菊花+文字
*/
+ (void)showLoadingWithMessage:(NSString *)message inView:(nullable UIView *)view {
[self showLoadingWithMessage:message inView:view enabled:YES];
}
/**
* 在view上显示菊花+文字
*/
+ (void)showLoadingWithMessage:(NSString *)message inView:(nullable UIView *)view enabled:(BOOL)enabled {
if (!view) {
view = [UIApplication sharedApplication].keyWindow;
}
[self hideHUDInView:view];
dispatch_main_sync_safe(^{
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:view animated:YES];
hud.userInteractionEnabled = enabled;
hud.bezelView.color = [[UIColor whiteColor] colorWithAlphaComponent:0.8];
hud.removeFromSuperViewOnHide = YES;
if (message.length) {
hud.label.text = message;
hud.label.numberOfLines = 0;
hud.label.textColor = [UIColor blackColor];
hud.label.font = [UIFont systemFontOfSize:14];
}
});
}
/**
在窗口上显示自定义GIFLoading, 背景默认黑色0.35透明度, 默认拦截点击事件
*/
+ (void)showGIFLoading {
[self showGIFLoadingInView:[UIApplication sharedApplication].keyWindow];
}
/**
在指定的view上显示自定义GIFLoading, 背景默认黑色0.35透明度, 默认拦截点击事件
@param view 显示在哪个view上
*/
+ (void)showGIFLoadingInView:(nullable UIView *)view {
[self showGIFLoadingInView:view bgColor:[UIColor colorWithRed:0 green:0 blue:0 alpha:0.35] enabled:YES];
}
/**
在指定的view上显示自定义GIFLoading
@param view 显示在哪个view上
@param bgColor 背景颜色, 遮盖
@param enabled 是否可以拦截事件 no:不拦截 yes:拦截
*/
+ (void)showGIFLoadingInView:(nullable UIView *)view bgColor:(nullable UIColor *)bgColor enabled:(BOOL)enabled {
if (!view) {
view = [UIApplication sharedApplication].keyWindow;
}
[self hideHUDInView:view];
dispatch_main_sync_safe(^{
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:view animated:YES];
hud.bezelView.style = MBProgressHUDBackgroundStyleSolidColor;
hud.minSize = CGSizeMake(100, 150);
hud.userInteractionEnabled = NO;
hud.mode = MBProgressHUDModeCustomView;
[hud.bezelView addSubview:[self loadingView]];
hud.backgroundColor = bgColor;
hud.bezelView.color = [UIColor clearColor];
hud.removeFromSuperViewOnHide = YES;
});
}
+ (void)showAnchorLoading {
UIView *view = [UIApplication sharedApplication].delegate.window;
dispatch_main_sync_safe(^{
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:view animated:YES];
hud.bezelView.style = MBProgressHUDBackgroundStyleSolidColor;
hud.userInteractionEnabled = NO;
hud.mode = MBProgressHUDModeCustomView;
hud.minSize = CGSizeMake(100, 150);
[hud.bezelView addSubview:[self loadingView ]];
hud.bezelView.color = [UIColor clearColor];
hud.removeFromSuperViewOnHide = YES;
});
}
+ (void)showAnchorLoading:(UIView *)view {
dispatch_main_sync_safe(^{
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:view animated:YES];
hud.bezelView.style = MBProgressHUDBackgroundStyleSolidColor;
hud.userInteractionEnabled = NO;
hud.mode = MBProgressHUDModeCustomView;
hud.minSize = CGSizeMake(100, 150);
[hud.bezelView addSubview:[self loadingView ]];
hud.bezelView.color = [UIColor clearColor];
hud.removeFromSuperViewOnHide = YES;
});
}
#pragma mark - private
+ (MBProgressHUD *)normalProgressHUD:(UIView *)view {
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:view animated:YES];
hud.removeFromSuperViewOnHide = YES;
return hud;
}
+ (UIView *)loadingView {
UIView *bgView = [UIView new];
bgView.backgroundColor = [UIColor clearColor];
bgView.frame = CGRectMake(0, 0, 100, 150);
return bgView;
}
+(void)hideOnlyView:(UIView *)view{
dispatch_main_sync_safe(^{
if (view) {
UIView *getView;
if([view isKindOfClass:[MvpViewController class]]){
getView = ((MvpViewController *)view).view;
}else{
getView = view;
}
MBProgressHUD *hud = [XNDJTDDLoadingTool HUDForView:getView];
if (hud != nil && hud.mode != MBProgressHUDModeText) {
hud.removeFromSuperViewOnHide = YES;
[hud hideAnimated:NO];
}
}
});
}
+(void)showOnlyView:(UIView *)view{
if (!view) {
view = [UIApplication sharedApplication].keyWindow;
}
[self hideOnlyView:view];
dispatch_main_sync_safe(^{
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:view animated:YES];
hud.bezelView.style = MBProgressHUDBackgroundStyleSolidColor;
hud.minSize = CGSizeMake(100, 150);
hud.userInteractionEnabled = NO;
hud.mode = MBProgressHUDModeCustomView;
[hud.bezelView addSubview:[self loadingView]];
hud.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.35];
hud.bezelView.color = [UIColor clearColor];
hud.removeFromSuperViewOnHide = YES;
});
}
+(void)showOnlyView:(UIView *)view enabled:(BOOL)enabled{
if (!view) {
view = [UIApplication sharedApplication].keyWindow;
}
[self hideOnlyView:view];
dispatch_main_sync_safe(^{
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:view animated:YES];
hud.bezelView.style = MBProgressHUDBackgroundStyleSolidColor;
hud.minSize = CGSizeMake(100, 150);
hud.userInteractionEnabled = enabled;
hud.mode = MBProgressHUDModeCustomView;
[hud.bezelView addSubview:[self loadingView]];
hud.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.35];
hud.bezelView.color = [UIColor clearColor];
hud.removeFromSuperViewOnHide = YES;
});
}
@end

View File

@@ -1,163 +0,0 @@
//
// EPImageUploader.swift
// YuMi
//
// Created by AI on 2025-10-11.
//
import UIKit
import Foundation
/// 图片批量上传工具(纯 Swift 内部类,直接使用 QCloudCOSXML SDK
/// 不对外暴露,由 EPSDKManager 内部调用
class EPImageUploader {
init() {}
/// 批量上传图片(内部方法)
/// - Parameters:
/// - images: 要上传的图片数组
/// - bucket: QCloud bucket 名称
/// - customDomain: 自定义域名
/// - progress: 进度回调 (已上传数, 总数)
/// - success: 成功回调
/// - failure: 失败回调
func performBatchUpload(
_ images: [UIImage],
bucket: String,
customDomain: String,
progress: @escaping (Int, Int) -> Void,
success: @escaping ([[String: Any]]) -> Void,
failure: @escaping (String) -> Void
) {
let total = images.count
let queue = DispatchQueue(label: "com.yumi.imageupload", attributes: .concurrent)
let semaphore = DispatchSemaphore(value: 3) // 最多同时上传 3 张
var uploadedCount = 0
var resultList: [[String: Any]] = []
var hasError = false
let lock = NSLock()
for (_, image) in images.enumerated() {
queue.async {
semaphore.wait()
// 检查是否已经失败
lock.lock()
if hasError {
lock.unlock()
semaphore.signal()
return
}
lock.unlock()
// 压缩图片
guard let imageData = image.jpegData(compressionQuality: 0.5) else {
lock.lock()
hasError = true
lock.unlock()
semaphore.signal()
DispatchQueue.main.async {
failure(YMLocalizedString("error.image_compress_failed"))
}
return
}
// 获取图片格式
let format = UIImage.getImageType(withImageData: imageData) ?? "jpeg"
// 生成文件名
let uuid = NSString.createUUID()
let fileName = "image/\(uuid).\(format)"
// 直接使用 QCloud SDK 上传
let request = QCloudCOSXMLUploadObjectRequest<AnyObject>()
request.bucket = bucket
request.object = fileName
request.body = imageData as NSData
// 监听上传进度(可选)
request.sendProcessBlock = { bytesSent, totalBytesSent, totalBytesExpectedToSend in
// 单个文件的上传进度(当前不使用)
}
// 监听上传结果
request.finishBlock = { [weak self] result, error in
guard let self = self else {
semaphore.signal()
return
}
if let error = error {
// 上传失败
lock.lock()
if !hasError {
hasError = true
lock.unlock()
semaphore.signal()
DispatchQueue.main.async {
failure(error.localizedDescription)
}
} else {
lock.unlock()
semaphore.signal()
}
} else if let result = result as? QCloudUploadObjectResult {
// 上传成功
lock.lock()
if !hasError {
uploadedCount += 1
// 解析上传 URL参考 UploadFile.m line 217-223
let uploadedURL = self.parseUploadURL(result.location, customDomain: customDomain)
let imageInfo: [String: Any] = [
"resUrl": uploadedURL,
"width": image.size.width,
"height": image.size.height,
"format": format
]
resultList.append(imageInfo)
let currentUploaded = uploadedCount
lock.unlock()
// 进度回调
DispatchQueue.main.async {
progress(currentUploaded, total)
}
// 全部完成
if currentUploaded == total {
DispatchQueue.main.async {
success(resultList)
}
}
} else {
lock.unlock()
}
semaphore.signal()
} else {
semaphore.signal()
}
}
// 执行上传
QCloudCOSTransferMangerService.defaultCOSTransferManager().uploadObject(request)
}
}
}
/// 解析上传返回的 URL参考 UploadFile.m line 217-223
/// - Parameters:
/// - location: QCloud 返回的原始 URL
/// - customDomain: 自定义域名
/// - Returns: 解析后的 URL
private func parseUploadURL(_ location: String, customDomain: String) -> String {
let components = location.components(separatedBy: ".com/")
if components.count == 2 {
return "\(customDomain)/\(components[1])"
}
return location
}
}

View File

@@ -1,92 +0,0 @@
//
// EPProgressHUD.swift
// YuMi
//
// Created by AI on 2025-10-11.
//
import UIKit
import Foundation
/// 带进度的 Loading 组件(基于 MBProgressHUD
@objc class EPProgressHUD: NSObject {
private static var currentHUD: MBProgressHUD?
/// 获取当前活跃的 window兼容 iOS 13+
private static var keyWindow: UIWindow? {
if #available(iOS 13.0, *) {
return UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
.first { $0.isKeyWindow }
} else {
return UIApplication.shared.keyWindow
}
}
/// 显示上传进度
/// - Parameters:
/// - uploaded: 已上传数量
/// - total: 总数量
@objc static func showProgress(_ uploaded: Int, total: Int) {
DispatchQueue.main.async {
guard let window = keyWindow else { return }
if let hud = currentHUD {
// 更新现有 HUD
hud.label.text = String(format: YMLocalizedString("upload.progress_format"), uploaded, total)
hud.progress = Float(uploaded) / Float(total)
} else {
// 创建新 HUD
let hud = MBProgressHUD.showAdded(to: window, animated: true)
hud.mode = .determinateHorizontalBar
hud.label.text = String(format: YMLocalizedString("upload.progress_format"), uploaded, total)
hud.progress = Float(uploaded) / Float(total)
hud.removeFromSuperViewOnHide = true
currentHUD = hud
}
}
}
/// 显示错误提示
/// - Parameter message: 错误信息
@objc static func showError(_ message: String) {
DispatchQueue.main.async {
guard let window = keyWindow else { return }
let hud = MBProgressHUD.showAdded(to: window, animated: true)
hud.mode = .text
hud.label.text = message
hud.label.numberOfLines = 0
hud.removeFromSuperViewOnHide = true
hud.hide(animated: true, afterDelay: 2.0)
}
}
/// 显示成功提示
/// - Parameter message: 成功信息
@objc static func showSuccess(_ message: String) {
DispatchQueue.main.async {
guard let window = keyWindow else { return }
let hud = MBProgressHUD.showAdded(to: window, animated: true)
hud.mode = .text
hud.label.text = message
hud.label.numberOfLines = 0
hud.removeFromSuperViewOnHide = true
hud.hide(animated: true, afterDelay: 2.0)
}
}
/// 关闭 HUD
@objc static func dismiss() {
DispatchQueue.main.async {
guard let hud = currentHUD else { return }
hud.hide(animated: true)
currentHUD = nil
}
}
}

View File

@@ -1,56 +0,0 @@
//
// EPQCloudConfig.swift
// YuMi
//
// Created by AI on 2025-10-11.
//
import Foundation
/// QCloud 配置数据模型(对应 UploadFileModel
struct EPQCloudConfig {
let secretId: String
let secretKey: String
let sessionToken: String
let bucket: String
let region: String
let customDomain: String
let startTime: Int64
let expireTime: Int64
let appId: String
let accelerate: Int
/// 从 API 返回的 dictionary 初始化
/// API: GET tencent/cos/getToken
init?(dictionary: [String: Any]) {
// 必填字段检查
guard let secretId = dictionary["secretId"] as? String,
let secretKey = dictionary["secretKey"] as? String,
let sessionToken = dictionary["sessionToken"] as? String,
let bucket = dictionary["bucket"] as? String,
let region = dictionary["region"] as? String,
let customDomain = dictionary["customDomain"] as? String,
let appId = dictionary["appId"] as? String else {
return nil
}
self.secretId = secretId
self.secretKey = secretKey
self.sessionToken = sessionToken
self.bucket = bucket
self.region = region
self.customDomain = customDomain
self.appId = appId
// 可选字段使用默认值
self.startTime = (dictionary["startTime"] as? Int64) ?? 0
self.expireTime = (dictionary["expireTime"] as? Int64) ?? 0
self.accelerate = (dictionary["accelerate"] as? Int) ?? 0
}
/// 检查配置是否过期
var isExpired: Bool {
return Date().timeIntervalSince1970 > Double(expireTime)
}
}

View File

@@ -1,253 +0,0 @@
//
// EPSDKManager.swift
// YuMi
//
// Created by AI on 2025-10-11.
//
import Foundation
/// 第三方 SDK 统一管理器(单例)
/// 统一入口:对外提供所有 SDK 能力
/// 内部管理QCloud 初始化、配置、上传等
@objc class EPSDKManager: NSObject, QCloudSignatureProvider, QCloudCredentailFenceQueueDelegate {
// MARK: - Singleton
@objc static let shared = EPSDKManager()
// MARK: - Properties
// QCloud 配置缓存
private var qcloudConfig: EPQCloudConfig?
// QCloud 初始化状态
private var isQCloudInitializing = false
// QCloud 初始化回调队列
private var qcloudInitCallbacks: [(Bool, String?) -> Void] = []
// QCloud 凭证队列
private var credentialFenceQueue: QCloudCredentailFenceQueue?
// 线程安全锁
private let lock = NSLock()
// 内部图片上传器
private let uploader = EPImageUploader()
// MARK: - Initialization
private override init() {
super.init()
}
// MARK: - Public API (对外统一入口)
/// 批量上传图片(统一入口)
/// - Parameters:
/// - images: 要上传的图片数组
/// - progress: 进度回调 (已上传数, 总数)
/// - success: 成功回调,返回图片信息数组
/// - failure: 失败回调
@objc func uploadImages(
_ images: [UIImage],
progress: @escaping (Int, Int) -> Void,
success: @escaping ([[String: Any]]) -> Void,
failure: @escaping (String) -> Void
) {
guard !images.isEmpty else {
success([])
return
}
// 确保 QCloud 已就绪
ensureQCloudReady { [weak self] isReady, errorMsg in
guard let self = self, isReady else {
DispatchQueue.main.async {
failure(errorMsg ?? YMLocalizedString("error.qcloud_init_failed"))
}
return
}
// 委托给内部 uploader 执行
self.uploader.performBatchUpload(
images,
bucket: self.qcloudConfig?.bucket ?? "",
customDomain: self.qcloudConfig?.customDomain ?? "",
progress: progress,
success: success,
failure: failure
)
}
}
/// 检查 QCloud 是否已就绪
/// - Returns: true 表示已初始化且未过期
@objc func isQCloudReady() -> Bool {
lock.lock()
defer { lock.unlock() }
guard let config = qcloudConfig else {
return false
}
return !config.isExpired
}
// MARK: - Internal Methods
/// 确保 QCloud 已就绪(自动初始化)
private func ensureQCloudReady(completion: @escaping (Bool, String?) -> Void) {
if isQCloudReady() {
completion(true, nil)
return
}
// 未初始化或已过期,重新初始化
initializeQCloud(completion: completion)
}
/// 初始化 QCloud获取 Token 并配置 SDK
private func initializeQCloud(completion: @escaping (Bool, String?) -> Void) {
lock.lock()
// 如果正在初始化,加入回调队列
if isQCloudInitializing {
qcloudInitCallbacks.append(completion)
lock.unlock()
return
}
// 如果已初始化且未过期,直接返回
if let config = qcloudConfig, !config.isExpired {
lock.unlock()
completion(true, nil)
return
}
// 开始初始化
isQCloudInitializing = true
qcloudInitCallbacks.append(completion)
lock.unlock()
// 调用 API 获取 QCloud Token
// API: GET tencent/cos/getToken
Api.getQCloudInfo { [weak self] (data, code, msg) in
guard let self = self else { return }
self.lock.lock()
if code == 200,
let dict = data?.data as? [String: Any],
let config = EPQCloudConfig(dictionary: dict) {
// 保存配置
self.qcloudConfig = config
// 配置 QCloud SDK
self.configureQCloudSDK(with: config)
// 初始化完成
self.isQCloudInitializing = false
let callbacks = self.qcloudInitCallbacks
self.qcloudInitCallbacks.removeAll()
self.lock.unlock()
// 短暂延迟确保 SDK 配置完成
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
callbacks.forEach { $0(true, nil) }
}
} else {
// 初始化失败
self.isQCloudInitializing = false
let callbacks = self.qcloudInitCallbacks
self.qcloudInitCallbacks.removeAll()
self.lock.unlock()
let errorMsg = msg ?? YMLocalizedString("error.qcloud_config_failed")
DispatchQueue.main.async {
callbacks.forEach { $0(false, errorMsg) }
}
}
}
}
/// 配置 QCloud SDK参考 UploadFile.m line 42-64
private func configureQCloudSDK(with config: EPQCloudConfig) {
let configuration = QCloudServiceConfiguration()
configuration.appID = config.appId
let endpoint = QCloudCOSXMLEndPoint()
endpoint.regionName = config.region
endpoint.useHTTPS = true
// 全球加速(参考 UploadFile.m line 56-59
if config.accelerate == 1 {
endpoint.suffix = "cos.accelerate.myqcloud.com"
}
configuration.endpoint = endpoint
configuration.signatureProvider = self
// 注册 COS 服务
QCloudCOSXMLService.registerDefaultCOSXML(with: configuration)
QCloudCOSTransferMangerService.registerDefaultCOSTransferManger(with: configuration)
// 初始化凭证队列
credentialFenceQueue = QCloudCredentailFenceQueue()
credentialFenceQueue?.delegate = self
}
// MARK: - QCloudSignatureProvider Protocol
/// 提供签名(参考 UploadFile.m line 67-104
func signature(
with fields: QCloudSignatureFields,
request: QCloudBizHTTPRequest,
urlRequest: NSMutableURLRequest,
compelete: @escaping QCloudHTTPAuthentationContinueBlock
) {
guard let config = qcloudConfig else {
let error = NSError(domain: "com.yumi.qcloud", code: -1,
userInfo: [NSLocalizedDescriptionKey: YMLocalizedString("error.qcloud_config_not_initialized")])
compelete(nil, error)
return
}
let credential = QCloudCredential()
credential.secretID = config.secretId
credential.secretKey = config.secretKey
credential.token = config.sessionToken
credential.startDate = Date(timeIntervalSince1970: TimeInterval(config.startTime))
credential.expirationDate = Date(timeIntervalSince1970: TimeInterval(config.expireTime))
let creator = QCloudAuthentationV5Creator(credential: credential)
let signature = creator?.signature(forData: urlRequest)
compelete(signature, nil)
}
// MARK: - QCloudCredentailFenceQueueDelegate Protocol
/// 管理凭证(参考 UploadFile.m line 107-133
func fenceQueue(
_ queue: QCloudCredentailFenceQueue,
requestCreatorWithContinue continueBlock: @escaping QCloudCredentailFenceQueueContinue
) {
guard let config = qcloudConfig else {
let error = NSError(domain: "com.yumi.qcloud", code: -1,
userInfo: [NSLocalizedDescriptionKey: YMLocalizedString("error.qcloud_config_not_initialized")])
continueBlock(nil, error)
return
}
let credential = QCloudCredential()
credential.secretID = config.secretId
credential.secretKey = config.secretKey
credential.token = config.sessionToken
credential.startDate = Date(timeIntervalSince1970: TimeInterval(config.startTime))
credential.expirationDate = Date(timeIntervalSince1970: TimeInterval(config.expireTime))
let creator = QCloudAuthentationV5Creator(credential: credential)
continueBlock(creator, nil)
}
}

View File

@@ -1,696 +0,0 @@
//
// EPLoginTypesViewController.swift
// YuMi
//
// Created by AI on 2025-01-27.
//
import UIKit
class EPLoginTypesViewController: BaseViewController {
// MARK: - Properties
var displayType: EPLoginDisplayType = .id
private let loginService = EPLoginService()
private let backgroundImageView = UIImageView()
private let titleLabel = UILabel()
private let backButton = UIButton(type: .system)
private let firstInputView = EPLoginInputView()
private let secondInputView = EPLoginInputView()
private var thirdInputView: EPLoginInputView?
private let actionButton = UIButton(type: .system)
private var forgotPasswordButton: UIButton?
private var hasAddedGradient = false
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
configureForDisplayType()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: false)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// 添加渐变背景到 actionButton只添加一次
if !hasAddedGradient && actionButton.bounds.width > 0 {
actionButton.addGradientBackground(
with: [
EPLoginConfig.Colors.gradientStart,
EPLoginConfig.Colors.gradientEnd
],
start: CGPoint(x: 0, y: 0.5),
end: CGPoint(x: 1, y: 0.5),
cornerRadius: EPLoginConfig.Layout.uniformCornerRadius
)
hasAddedGradient = true
}
}
// MARK: - Setup
private func setupUI() {
setupBackground()
setupNavigationBar()
setupTitle()
setupInputViews()
setupActionButton()
}
private func setupBackground() {
view.addSubview(backgroundImageView)
backgroundImageView.translatesAutoresizingMaskIntoConstraints = false
backgroundImageView.image = kImage(EPLoginConfig.Images.background)
backgroundImageView.contentMode = .scaleAspectFill
backgroundImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
private func setupNavigationBar() {
view.addSubview(backButton)
backButton.translatesAutoresizingMaskIntoConstraints = false
backButton.setImage(UIImage(systemName: EPLoginConfig.Images.iconBack), for: .normal)
backButton.tintColor = EPLoginConfig.Colors.textLight
backButton.addTarget(self, action: #selector(handleBack), for: .touchUpInside)
backButton.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.compactHorizontalPadding)
make.top.equalTo(view.safeAreaLayoutGuide).offset(8)
make.size.equalTo(EPLoginConfig.Layout.backButtonSize)
}
}
private func setupTitle() {
view.addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.font = .systemFont(ofSize: EPLoginConfig.Layout.titleFontSize, weight: .bold)
titleLabel.textColor = EPLoginConfig.Colors.textLight
titleLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.centerY.equalTo(backButton) // 与返回按钮垂直居中对齐
}
}
private func setupInputViews() {
firstInputView.translatesAutoresizingMaskIntoConstraints = false
secondInputView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(firstInputView)
view.addSubview(secondInputView)
firstInputView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.uniformHorizontalPadding)
make.trailing.equalToSuperview().offset(-EPLoginConfig.Layout.uniformHorizontalPadding)
make.top.equalTo(titleLabel.snp.bottom).offset(EPLoginConfig.Layout.inputTitleSpacing)
make.height.equalTo(EPLoginConfig.Layout.uniformHeight)
}
secondInputView.snp.makeConstraints { make in
make.leading.trailing.equalTo(firstInputView)
make.top.equalTo(firstInputView.snp.bottom).offset(EPLoginConfig.Layout.inputVerticalSpacing)
make.height.equalTo(EPLoginConfig.Layout.uniformHeight)
}
}
private func setupActionButton() {
view.addSubview(actionButton)
actionButton.translatesAutoresizingMaskIntoConstraints = false
actionButton.setTitle("Login", for: .normal)
actionButton.setTitleColor(EPLoginConfig.Colors.textLight, for: .normal)
actionButton.layer.cornerRadius = EPLoginConfig.Layout.uniformCornerRadius
actionButton.titleLabel?.font = .systemFont(ofSize: EPLoginConfig.Layout.buttonFontSize, weight: .semibold)
actionButton.addTarget(self, action: #selector(handleAction), for: .touchUpInside)
// 初始状态:禁用按钮
actionButton.isEnabled = false
actionButton.alpha = 0.5
actionButton.snp.makeConstraints { make in
make.leading.trailing.equalTo(firstInputView)
make.top.equalTo(secondInputView.snp.bottom).offset(EPLoginConfig.Layout.buttonTopSpacing)
make.height.equalTo(EPLoginConfig.Layout.uniformHeight)
}
}
// MARK: - Configuration
private func configureForDisplayType() {
switch displayType {
case .id:
titleLabel.text = YMLocalizedString("1.0.37_text_26") // ID Login
firstInputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: false,
isSecure: false,
icon: "icon_login_id",
placeholder: "Please enter ID",
keyboardType: .numberPad // ID 使用数字键盘
))
firstInputView.onTextChanged = { [weak self] _ in
self?.checkActionButtonStatus()
}
secondInputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: false,
isSecure: true,
icon: "icon_login_id",
placeholder: "Please enter password",
keyboardType: .default // 密码使用默认键盘(需要字母+数字)
))
secondInputView.onTextChanged = { [weak self] _ in
self?.checkActionButtonStatus()
}
actionButton.setTitle("Login", for: .normal)
// 添加忘记密码按钮
setupForgotPasswordButton()
case .email:
titleLabel.text = YMLocalizedString("20.20.51_text_1") // Email Login
firstInputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: false,
isSecure: false,
icon: "envelope",
placeholder: "Please enter email",
keyboardType: .emailAddress // Email 使用邮箱键盘
))
firstInputView.onTextChanged = { [weak self] _ in
self?.checkActionButtonStatus()
}
secondInputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: true,
isSecure: false,
icon: "number",
placeholder: "Please enter verification code",
keyboardType: .numberPad // 验证码使用数字键盘
))
secondInputView.onTextChanged = { [weak self] _ in
self?.checkActionButtonStatus()
}
secondInputView.delegate = self
actionButton.setTitle("Login", for: .normal)
case .phone:
titleLabel.text = "Phone Login"
firstInputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: false,
isSecure: false,
icon: "phone",
placeholder: "Please enter phone",
keyboardType: .numberPad // 手机号使用数字键盘
))
firstInputView.onTextChanged = { [weak self] _ in
self?.checkActionButtonStatus()
}
secondInputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: true,
isSecure: false,
icon: "number",
placeholder: "Please enter verification code",
keyboardType: .numberPad // 验证码使用数字键盘
))
secondInputView.onTextChanged = { [weak self] _ in
self?.checkActionButtonStatus()
}
secondInputView.delegate = self
actionButton.setTitle("Login", for: .normal)
case .emailReset:
titleLabel.text = YMLocalizedString("20.20.51_text_20")
firstInputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: false,
isSecure: false,
icon: "envelope",
placeholder: "Please enter email",
keyboardType: .emailAddress // Email 使用邮箱键盘
))
firstInputView.onTextChanged = { [weak self] _ in
self?.checkActionButtonStatus()
}
secondInputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: true,
isSecure: false,
icon: "number",
placeholder: "Please enter verification code",
keyboardType: .numberPad // 验证码使用数字键盘
))
secondInputView.onTextChanged = { [weak self] _ in
self?.checkActionButtonStatus()
}
secondInputView.delegate = self
// 添加第三个输入框
setupThirdInputView()
actionButton.setTitle("Confirm", for: .normal)
case .phoneReset:
titleLabel.text = YMLocalizedString("20.20.51_text_20")
firstInputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: false,
isSecure: false,
icon: "phone",
placeholder: "Please enter phone",
keyboardType: .numberPad // 手机号使用数字键盘
))
firstInputView.onTextChanged = { [weak self] _ in
self?.checkActionButtonStatus()
}
secondInputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: true,
isSecure: false,
icon: "number",
placeholder: "Please enter verification code",
keyboardType: .numberPad // 验证码使用数字键盘
))
secondInputView.onTextChanged = { [weak self] _ in
self?.checkActionButtonStatus()
}
secondInputView.delegate = self
// 添加第三个输入框
setupThirdInputView()
actionButton.setTitle("Confirm", for: .normal)
}
}
private func setupForgotPasswordButton() {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Forgot Password?", for: .normal)
button.setTitleColor(EPLoginConfig.Colors.textLight, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: EPLoginConfig.Layout.smallFontSize)
button.addTarget(self, action: #selector(handleForgotPassword), for: .touchUpInside)
view.addSubview(button)
button.snp.makeConstraints { make in
make.trailing.equalTo(secondInputView)
make.top.equalTo(secondInputView.snp.bottom).offset(8)
}
forgotPasswordButton = button
}
private func setupThirdInputView() {
let inputView = EPLoginInputView()
inputView.translatesAutoresizingMaskIntoConstraints = false
inputView.configure(with: EPLoginInputConfig(
showAreaCode: false,
showCodeButton: false,
isSecure: true,
icon: EPLoginConfig.Images.iconLock,
placeholder: "6-16 Digits + English Letters",
keyboardType: .default // 密码使用默认键盘(需要字母+数字)
))
inputView.onTextChanged = { [weak self] _ in
self?.checkActionButtonStatus()
}
view.addSubview(inputView)
inputView.snp.makeConstraints { make in
make.leading.trailing.equalTo(firstInputView)
make.top.equalTo(secondInputView.snp.bottom).offset(EPLoginConfig.Layout.inputVerticalSpacing)
make.height.equalTo(EPLoginConfig.Layout.uniformHeight)
}
// 重新调整 actionButton 位置
actionButton.snp.remakeConstraints { make in
make.leading.trailing.equalTo(firstInputView)
make.top.equalTo(inputView.snp.bottom).offset(EPLoginConfig.Layout.buttonTopSpacing)
make.height.equalTo(EPLoginConfig.Layout.uniformHeight)
}
thirdInputView = inputView
}
// MARK: - Actions
@objc private func handleBack() {
navigationController?.popViewController(animated: true)
}
@objc private func handleAction() {
view.endEditing(true)
// 执行对应类型的操作
switch displayType {
case .id:
handleIDLogin()
case .email:
handleEmailLogin()
case .phone:
handlePhoneLogin()
case .emailReset:
handleEmailResetPassword()
case .phoneReset:
handlePhoneResetPassword()
}
}
@objc private func handleForgotPassword() {
let vc = EPLoginTypesViewController()
vc.displayType = .emailReset
navigationController?.pushViewController(vc, animated: true)
}
// MARK: - 登录逻辑
private func handleIDLogin() {
let id = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
let password = secondInputView.text
// 表单验证(简化,仅检查空值)
guard !id.isEmpty else {
showErrorToast(YMLocalizedString("LoginPresenter0"))
return
}
guard !password.isEmpty else {
showErrorToast(YMLocalizedString("LoginPresenter1"))
return
}
// 显示加载状态
showLoading(true)
loginService.loginWithID(id: id, password: password) { [weak self] (accountModel: AccountModel) in
DispatchQueue.main.async {
self?.showLoading(false)
print("[EPLogin] ID登录成功: \(accountModel.uid)")
self?.showSuccessToast(YMLocalizedString("XPLoginPhoneViewController1"))
EPLoginManager.jumpToHome(from: self!)
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showErrorToast(msg)
}
}
}
private func handleEmailLogin() {
let email = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
let code = secondInputView.text
// 表单验证(简化,仅检查空值)
guard !email.isEmpty else {
showErrorToast(YMLocalizedString("LoginPresenter0"))
return
}
guard !code.isEmpty else {
showErrorToast(YMLocalizedString("LoginPresenter1"))
return
}
showLoading(true)
loginService.loginWithEmail(email: email, code: code) { [weak self] (accountModel: AccountModel) in
DispatchQueue.main.async {
self?.showLoading(false)
print("[EPLogin] 邮箱登录成功: \(accountModel.uid)")
self?.showSuccessToast(YMLocalizedString("XPLoginPhoneViewController1"))
EPLoginManager.jumpToHome(from: self!)
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showErrorToast(msg)
}
}
}
private func handlePhoneLogin() {
let phone = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
let code = secondInputView.text
// 表单验证(简化,仅检查空值)
guard !phone.isEmpty else {
showErrorToast(YMLocalizedString("XPLoginPhoneViewController0"))
return
}
guard !code.isEmpty else {
showErrorToast(YMLocalizedString("LoginPresenter1"))
return
}
showLoading(true)
loginService.loginWithPhone(phone: phone, code: code, areaCode: "+86") { [weak self] (accountModel: AccountModel) in
DispatchQueue.main.async {
self?.showLoading(false)
print("[EPLogin] 手机登录成功: \(accountModel.uid)")
self?.showSuccessToast(YMLocalizedString("XPLoginPhoneViewController1"))
EPLoginManager.jumpToHome(from: self!)
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showErrorToast(msg)
}
}
}
private func handleEmailResetPassword() {
guard let thirdInput = thirdInputView else { return }
let email = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
let code = secondInputView.text
let newPassword = thirdInput.text
// 表单验证(简化,仅检查空值)
guard !email.isEmpty else {
showErrorToast(YMLocalizedString("LoginPresenter0"))
return
}
guard !code.isEmpty else {
showErrorToast(YMLocalizedString("LoginPresenter1"))
return
}
guard !newPassword.isEmpty else {
showErrorToast(YMLocalizedString("LoginPresenter1"))
return
}
showLoading(true)
loginService.resetEmailPassword(email: email, code: code, newPassword: newPassword) { [weak self] in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showSuccessToast(YMLocalizedString("XPForgetPwdViewController1"))
self?.navigationController?.popViewController(animated: true)
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showErrorToast(msg)
}
}
}
private func handlePhoneResetPassword() {
guard let thirdInput = thirdInputView else { return }
let phone = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
let code = secondInputView.text
let newPassword = thirdInput.text
// 表单验证(简化,仅检查空值)
guard !phone.isEmpty else {
showErrorToast(YMLocalizedString("XPLoginPhoneViewController0"))
return
}
guard !code.isEmpty else {
showErrorToast(YMLocalizedString("LoginPresenter1"))
return
}
guard !newPassword.isEmpty else {
showErrorToast(YMLocalizedString("LoginPresenter1"))
return
}
showLoading(true)
loginService.resetPhonePassword(phone: phone, code: code, areaCode: "+86", newPassword: newPassword) { [weak self] in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showSuccessToast(YMLocalizedString("XPForgetPwdViewController1"))
self?.navigationController?.popViewController(animated: true)
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.showLoading(false)
self?.showErrorToast(msg)
}
}
}
// MARK: - 验证码发送
private func sendEmailCode() {
let email = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
// 简化验证,仅检查空值
guard !email.isEmpty else {
secondInputView.stopCountdown()
return
}
let type = (displayType == .emailReset) ? 2 : 1 // 2=找回密码, 1=登录
loginService.sendEmailCode(email: email, type: type) { [weak self] in
DispatchQueue.main.async {
self?.secondInputView.startCountdown()
self?.secondInputView.displayKeyboard()
self?.showSuccessToast(YMLocalizedString("XPLoginPhoneViewController2"))
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.secondInputView.stopCountdown()
self?.showErrorToast(msg)
}
}
}
private func sendPhoneCode() {
let phone = firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)
// 简化验证,仅检查空值
guard !phone.isEmpty else {
showErrorToast(YMLocalizedString("XPLoginPhoneViewController0"))
secondInputView.stopCountdown()
return
}
// 检查是否需要人机验证
loadCaptchaWebView { [weak self] in
guard let self = self else { return }
let type = (self.displayType == .phoneReset) ? 2 : 1 // 2=找回密码, 1=登录
self.loginService.sendPhoneCode(phone: phone, areaCode: "+86", type: type) { [weak self] in
DispatchQueue.main.async {
self?.secondInputView.startCountdown()
self?.secondInputView.displayKeyboard()
self?.showSuccessToast(YMLocalizedString("XPLoginPhoneViewController2"))
}
} failure: { [weak self] (code: Int, msg: String) in
DispatchQueue.main.async {
self?.secondInputView.stopCountdown()
self?.showErrorToast(msg)
}
}
}
}
private func sendEmailResetCode() {
sendEmailCode() // 复用邮箱验证码逻辑
}
private func sendPhoneResetCode() {
sendPhoneCode() // 复用手机验证码逻辑
}
// MARK: - UI Helpers
private func showLoading(_ show: Bool) {
if show {
actionButton.isEnabled = false
actionButton.alpha = 0.5
actionButton.setTitle("Loading...", for: .normal)
} else {
switch displayType {
case .id, .email, .phone:
actionButton.setTitle("Login", for: .normal)
case .emailReset, .phoneReset:
actionButton.setTitle("Confirm", for: .normal)
}
checkActionButtonStatus()
}
}
/// 检查并更新按钮启用状态
private func checkActionButtonStatus() {
let isEnabled: Bool
switch displayType {
case .id:
let hasId = !firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
let hasPassword = !secondInputView.text.isEmpty
isEnabled = hasId && hasPassword
case .email, .phone:
let hasAccount = !firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
let hasCode = !secondInputView.text.isEmpty
isEnabled = hasAccount && hasCode
case .emailReset, .phoneReset:
let hasAccount = !firstInputView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
let hasCode = !secondInputView.text.isEmpty
let hasPassword = !(thirdInputView?.text.isEmpty ?? true)
isEnabled = hasAccount && hasCode && hasPassword
}
actionButton.isEnabled = isEnabled
actionButton.alpha = isEnabled ? 1.0 : 0.5
}
/// 加载人机验证 Captcha WebView
/// - Parameter completion: 验证成功后的回调
private func loadCaptchaWebView(completion: @escaping () -> Void) {
completion()
}
}
// MARK: - EPLoginInputViewDelegate
extension EPLoginTypesViewController: EPLoginInputViewDelegate {
func inputViewDidRequestCode(_ inputView: EPLoginInputView) {
if inputView == secondInputView {
if displayType == .email || displayType == .emailReset {
sendEmailCode()
} else if displayType == .phone || displayType == .phoneReset {
sendPhoneCode()
}
}
}
func inputViewDidSelectArea(_ inputView: EPLoginInputView) {
// 区号选择(暂不实现)
print("[EPLogin] Area selection - 占位Phase 2 实现")
}
}

View File

@@ -1,307 +0,0 @@
//
// EPLoginViewController.swift
// YuMi
//
// Created by AI on 2025-01-27.
//
import UIKit
@objc class EPLoginViewController: UIViewController {
// MARK: - Properties
private let backgroundImageView = UIImageView()
private let logoImageView = UIImageView()
private let epartiTitleLabel = UILabel()
private let idLoginButton = EPLoginButton()
private let emailLoginButton = EPLoginButton()
private let agreeCheckbox = UIButton(type: .custom)
private let policyLabel = EPPolicyLabel()
private let feedbackButton = UIButton(type: .custom)
#if DEBUG
private let debugButton = UIButton(type: .custom)
#endif
private let policySelectedKey = EPLoginConfig.Keys.policyAgreed
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
// 验证 DEBUG 编译条件
#if DEBUG
print("✅ [EPLogin] DEBUG 模式已激活")
#else
print("⚠️ [EPLogin] 当前为 Release 模式")
#endif
navigationController?.setNavigationBarHidden(true, animated: false)
setupUI()
loadPolicyStatus()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: false)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
}
// MARK: - Setup
private func setupUI() {
setupBackground()
setupLogo()
setupLoginButtons()
setupPolicyArea()
setupNavigationBar()
}
private func setupBackground() {
view.addSubview(backgroundImageView)
backgroundImageView.image = kImage(EPLoginConfig.Images.background)
backgroundImageView.contentMode = .scaleAspectFill
backgroundImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
private func setupLogo() {
view.addSubview(logoImageView)
logoImageView.image = kImage(EPLoginConfig.Images.loginBg)
logoImageView.snp.makeConstraints { make in
make.top.leading.trailing.equalTo(view)
make.height.equalTo(EPLoginConfig.Layout.logoHeight)
}
// E-PARTY 标题
view.addSubview(epartiTitleLabel)
epartiTitleLabel.text = "E-PARTY"
epartiTitleLabel.font = .systemFont(ofSize: EPLoginConfig.Layout.epartiTitleFontSize, weight: .bold)
epartiTitleLabel.textColor = EPLoginConfig.Colors.textLight
epartiTitleLabel.transform = CGAffineTransform(a: 1, b: 0, c: -0.2, d: 1, tx: 0, ty: 0) // 斜体效果
epartiTitleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.epartiTitleLeading)
make.bottom.equalTo(logoImageView.snp.bottom).offset(EPLoginConfig.Layout.epartiTitleBottomOffset)
}
}
private func setupLoginButtons() {
// 配置按钮
idLoginButton.configure(
icon: EPLoginConfig.Images.iconLoginId,
title: YMLocalizedString(EPLoginConfig.LocalizedKeys.idLogin)
)
idLoginButton.delegate = self
emailLoginButton.configure(
icon: EPLoginConfig.Images.iconLoginEmail,
title: YMLocalizedString(EPLoginConfig.LocalizedKeys.emailLogin)
)
emailLoginButton.delegate = self
// StackView 布局
let stackView = UIStackView(arrangedSubviews: [idLoginButton, emailLoginButton])
stackView.axis = .vertical
stackView.spacing = EPLoginConfig.Layout.loginButtonSpacing
stackView.distribution = .fillEqually
view.addSubview(stackView)
stackView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.loginButtonHorizontalPadding)
make.trailing.equalToSuperview().offset(-EPLoginConfig.Layout.loginButtonHorizontalPadding)
make.top.equalTo(logoImageView.snp.bottom)
}
idLoginButton.snp.makeConstraints { make in
make.height.equalTo(EPLoginConfig.Layout.loginButtonHeight)
}
emailLoginButton.snp.makeConstraints { make in
make.height.equalTo(EPLoginConfig.Layout.loginButtonHeight)
}
}
private func setupPolicyArea() {
view.addSubview(agreeCheckbox)
view.addSubview(policyLabel)
agreeCheckbox.setImage(kImage("login_privace_select"), for: .selected)
agreeCheckbox.setImage(kImage("login_privace_unselect"), for: .normal)
agreeCheckbox.addTarget(self, action: #selector(togglePolicyCheckbox), for: .touchUpInside)
policyLabel.onUserAgreementTapped = { [weak self] in
print("[EPLogin] User agreement tapped callback triggered")
let url = self?.getUserAgreementURL() ?? ""
print("[EPLogin] User agreement URL: \(url)")
self?.openPolicyInExternalBrowser(url)
}
policyLabel.onPrivacyPolicyTapped = { [weak self] in
print("[EPLogin] Privacy policy tapped callback triggered")
let url = self?.getPrivacyPolicyURL() ?? ""
print("[EPLogin] Privacy policy URL: \(url)")
self?.openPolicyInExternalBrowser(url)
}
agreeCheckbox.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.horizontalPadding)
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-30)
make.size.equalTo(EPLoginConfig.Layout.checkboxSize)
}
policyLabel.snp.makeConstraints { make in
make.leading.equalTo(agreeCheckbox.snp.trailing).offset(8)
make.trailing.equalToSuperview().offset(-EPLoginConfig.Layout.horizontalPadding)
make.centerY.equalTo(agreeCheckbox)
}
}
private func setupNavigationBar() {
#if DEBUG
view.addSubview(feedbackButton)
feedbackButton.setTitle(YMLocalizedString(EPLoginConfig.LocalizedKeys.feedback), for: .normal)
feedbackButton.titleLabel?.font = .systemFont(ofSize: EPLoginConfig.Layout.smallFontSize)
feedbackButton.backgroundColor = EPLoginConfig.Colors.backgroundTransparent
feedbackButton.layer.cornerRadius = EPLoginConfig.Layout.feedbackButtonCornerRadius
feedbackButton.addTarget(self, action: #selector(handleFeedback), for: .touchUpInside)
feedbackButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-EPLoginConfig.Layout.compactHorizontalPadding)
make.top.equalTo(view.safeAreaLayoutGuide).offset(8)
make.height.equalTo(EPLoginConfig.Layout.feedbackButtonHeight)
}
view.addSubview(debugButton)
debugButton.setTitle("切换环境", for: .normal)
debugButton.setTitleColor(.blue, for: .normal)
debugButton.addTarget(self, action: #selector(handleDebug), for: .touchUpInside)
debugButton.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.compactHorizontalPadding)
make.top.equalTo(view.safeAreaLayoutGuide).offset(8)
}
#endif // DEBUG
}
// MARK: - Actions
private func handleIDLogin() {
let vc = EPLoginTypesViewController()
vc.displayType = .id
navigationController?.pushViewController(vc, animated: true)
}
private func handleEmailLogin() {
let vc = EPLoginTypesViewController()
vc.displayType = .email
navigationController?.pushViewController(vc, animated: true)
}
@objc private func togglePolicyCheckbox() {
agreeCheckbox.isSelected.toggle()
UserDefaults.standard.set(agreeCheckbox.isSelected, forKey: policySelectedKey)
}
@objc private func handleFeedback() {
print("[EPLogin] Feedback - 占位Phase 2 实现")
}
#if DEBUG
@objc private func handleDebug() {
print("[EPLogin] Debug - 占位Phase 2 实现")
}
#endif
private func openPolicyInExternalBrowser(_ urlString: String) {
print("[EPLogin] Original URL: \(urlString)")
// 如果不是完整 URL拼接域名参考 XPWebViewController.m 第 697-698 行)
var fullUrl = urlString
if !urlString.hasPrefix("http") && !urlString.hasPrefix("https") {
let hostUrl = HttpRequestHelper.getHostUrl()
fullUrl = "\(hostUrl)/\(urlString)"
print("[EPLogin] Added host URL, full URL: \(fullUrl)")
}
print("[EPLogin] Opening URL in external browser: \(fullUrl)")
guard let url = URL(string: fullUrl) else {
print("[EPLogin] ❌ Invalid URL: \(fullUrl)")
return
}
print("[EPLogin] URL object created: \(url)")
// 在外部浏览器中打开
if UIApplication.shared.canOpenURL(url) {
print("[EPLogin] ✅ Can open URL, attempting to open...")
UIApplication.shared.open(url, options: [:]) { success in
print("[EPLogin] Open external browser: \(success ? "✅ Success" : "❌ Failed")")
}
} else {
print("[EPLogin] ❌ Cannot open URL: \(fullUrl)")
}
}
// MARK: - Helpers
private func loadPolicyStatus() {
agreeCheckbox.isSelected = UserDefaults.standard.bool(forKey: policySelectedKey)
// 默认勾选
if !UserDefaults.standard.bool(forKey: EPLoginConfig.Keys.hasLaunchedBefore) {
agreeCheckbox.isSelected = true
UserDefaults.standard.set(true, forKey: policySelectedKey)
UserDefaults.standard.set(true, forKey: EPLoginConfig.Keys.hasLaunchedBefore)
}
}
/// 获取用户协议 URL
private func getUserAgreementURL() -> String {
// kUserProtocalURL 对应枚举值 4
let url = URLWithType(URLType(rawValue: 4)!) as String
print("[EPLogin] User agreement URL from URLWithType: \(url)")
return url
}
/// 获取隐私政策 URL
private func getPrivacyPolicyURL() -> String {
// kPrivacyURL 对应枚举值 0
let url = URLWithType(URLType(rawValue: 0)!) as String
print("[EPLogin] Privacy policy URL from URLWithType: \(url)")
return url
}
private func checkPolicyAgreed() -> Bool {
if !agreeCheckbox.isSelected {
// Phase 2: 显示提示
print("[EPLogin] Please agree to policy first")
return false
}
return true
}
}
// MARK: - EPLoginButtonDelegate
extension EPLoginViewController: EPLoginButtonDelegate {
func loginButtonDidTap(_ button: EPLoginButton) {
guard checkPolicyAgreed() else { return }
if button == idLoginButton {
handleIDLogin()
} else if button == emailLoginButton {
handleEmailLogin()
}
}
}

View File

@@ -1,33 +0,0 @@
//
// EPLoginBridge.swift
// YuMi
//
// Created by AI on 2025-01-27.
// 桥接 Objective-C 宏到 Swift
//
import UIKit
/// 桥接 kImage 宏
func kImage(_ name: String) -> UIImage? {
return UIImage(named: name)
}
/// 桥接 YMLocalizedString 宏
func YMLocalizedString(_ key: String) -> String {
return Bundle.ymLocalizedString(forKey: key)
}
/// 桥接 URLType 枚举常量
extension URLType {
static var captchaSwitch: URLType {
return URLType(rawValue: 113)! // kCaptchaSwitchPath
}
}
/// DES 加密辅助函数
func encryptDES(_ plainText: String) -> String {
// 直接使用加密密钥(与 ObjC 版本保持一致)
let key = "1ea53d260ecf11e7b56e00163e046a26"
return DESEncrypt.encryptUseDES(plainText, key: key) ?? plainText
}

View File

@@ -1,305 +0,0 @@
//
// EPLoginConfig.swift
// YuMi
//
// Created by AI on 2025-01-27.
// 统一配置文件 - 消除硬编码
//
import UIKit
/// 登录模块统一配置
struct EPLoginConfig {
// MARK: - Layout 布局尺寸
struct Layout {
/// 标准按钮宽度
static let buttonWidth: CGFloat = 294
/// 标准按钮高度
static let buttonHeight: CGFloat = 46
/// 登录按钮高度
static let loginButtonHeight: CGFloat = 56
/// 登录按钮间距
static let loginButtonSpacing: CGFloat = 24
/// 登录按钮左右边距
static let loginButtonHorizontalPadding: CGFloat = 30
/// 输入框/按钮统一高度
static let uniformHeight: CGFloat = 56
/// 输入框/按钮统一左右边距
static let uniformHorizontalPadding: CGFloat = 29
/// 输入框/按钮统一圆角
static let uniformCornerRadius: CGFloat = 28
/// 标准圆角半径(按钮/输入框)
static let cornerRadius: CGFloat = 23
/// Logo 尺寸
static let logoHeight: CGFloat = 400
/// Logo 距离顶部的距离
static let logoTopOffset: CGFloat = 80
/// E-PARTY 标题字号
static let epartiTitleFontSize: CGFloat = 56
/// E-PARTY 标题距离 view leading
static let epartiTitleLeading: CGFloat = 40
/// E-PARTY 标题距离 logoImage bottom 的偏移(负值表示向上)
static let epartiTitleBottomOffset: CGFloat = -30
/// 输入框之间的垂直间距
static let inputVerticalSpacing: CGFloat = 16
/// 输入框距离标题的距离
static let inputTitleSpacing: CGFloat = 60
/// 按钮距离输入框的距离
static let buttonTopSpacing: CGFloat = 40
/// 页面左右边距
static let horizontalPadding: CGFloat = 40
/// 紧凑左右边距
static let compactHorizontalPadding: CGFloat = 16
/// 标题字体大小
static let titleFontSize: CGFloat = 28
/// 按钮字体大小
static let buttonFontSize: CGFloat = 16
/// 输入框字体大小
static let inputFontSize: CGFloat = 14
/// 小字体大小(提示文字等)
static let smallFontSize: CGFloat = 12
/// 图标尺寸
static let iconSize: CGFloat = 24
/// 登录按钮图标尺寸
static let loginButtonIconSize: CGFloat = 30
/// 登录按钮图标左边距(距离白色背景)
static let loginButtonIconLeading: CGFloat = 33
/// 图标左边距
static let iconLeading: CGFloat = 15
/// 图标与文字间距
static let iconTextSpacing: CGFloat = 12
/// Checkbox 尺寸
static let checkboxSize: CGFloat = 18
/// 返回按钮尺寸
static let backButtonSize: CGFloat = 44
/// Feedback 按钮高度
static let feedbackButtonHeight: CGFloat = 22
static let feedbackButtonCornerRadius: CGFloat = 10.5
/// 输入框高度
static let inputHeight: CGFloat = 56
/// 输入框圆角
static let inputCornerRadius: CGFloat = 28
/// 输入框左右内边距
static let inputHorizontalPadding: CGFloat = 24
/// 输入框 icon 尺寸
static let inputIconSize: CGFloat = 20
/// 输入框边框宽度
static let inputBorderWidth: CGFloat = 1
/// 验证码按钮宽度
static let codeButtonWidth: CGFloat = 102
/// 验证码按钮高度
static let codeButtonHeight: CGFloat = 38
}
// MARK: - Colors 颜色主题
struct Colors {
/// 主题色(按钮背景)
static let primary = UIColor.systemPurple
/// 背景色
static let background = UIColor.white
static let backgroundTransparent = UIColor.white.withAlphaComponent(0.5)
/// 文字颜色
static let text = UIColor.darkText
static let textSecondary = UIColor.darkGray
static let textLight = UIColor.white
/// 图标颜色
static let icon = UIColor.darkGray
static let iconDisabled = UIColor.gray
/// 输入框颜色
static let inputBackground = UIColor.white.withAlphaComponent(0.1)
static let inputText = UIColor(red: 0x1F/255.0, green: 0x1B/255.0, blue: 0x4F/255.0, alpha: 1.0)
static let inputBorder = UIColor.white
static let inputBorderFocused = UIColor.systemPurple
/// 渐变色Login/Confirm按钮
static let gradientStart = UIColor(red: 0xF8/255.0, green: 0x54/255.0, blue: 0xFC/255.0, alpha: 1.0) // #F854FC
static let gradientEnd = UIColor(red: 0x50/255.0, green: 0x0F/255.0, blue: 0xFF/255.0, alpha: 1.0) // #500FFF
/// 验证码按钮颜色
static let codeButtonBackground = UIColor(red: 0x91/255.0, green: 0x68/255.0, blue: 0xFA/255.0, alpha: 1.0)
/// 按钮状态颜色
static let buttonEnabled = UIColor.systemPurple
static let buttonDisabled = UIColor.lightGray
/// 错误提示色
static let error = UIColor.systemRed
static let success = UIColor.systemGreen
/// 链接颜色
static let link = UIColor.black
static let linkUnderline = UIColor.black
}
// MARK: - Animation 动画配置
struct Animation {
/// 标准动画时长
static let duration: TimeInterval = 0.3
/// 短动画时长
static let shortDuration: TimeInterval = 0.15
/// 长动画时长
static let longDuration: TimeInterval = 0.5
/// 弹簧动画阻尼
static let springDamping: CGFloat = 0.75
/// 弹簧动画初速度
static let springVelocity: CGFloat = 0.5
/// 按钮点击缩放比例
static let buttonPressScale: CGFloat = 0.95
/// 错误抖动距离
static let shakeOffset: CGFloat = 10
/// 错误抖动次数
static let shakeCount: Int = 3
}
// MARK: - Validation 验证规则
struct Validation {
/// 密码最小长度
static let passwordMinLength = 6
/// 密码最大长度
static let passwordMaxLength = 16
/// 验证码长度
static let codeLength = 6
/// 手机号最小长度
static let phoneMinLength = 10
/// 手机号最大长度
static let phoneMaxLength = 15
/// 邮箱正则表达式
static let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
/// 手机号正则表达式
static let phoneRegex = "^[0-9]{10,15}$"
}
// MARK: - Timing 时间配置
struct Timing {
/// 验证码倒计时秒数
static let codeCountdownSeconds = 60
/// Toast 显示时长
static let toastDuration: TimeInterval = 2.0
/// 加载超时时间
static let requestTimeout: TimeInterval = 30.0
}
// MARK: - API 接口配置
struct API {
/// Client Secret
static let clientSecret = "uyzjdhds"
/// Client ID
static let clientId = "erban-client"
/// Grant Type
static let grantType = "password"
/// 版本号
static let version = "1"
/// 验证码类型:登录
static let codeTypeLogin = 1
/// 验证码类型:找回密码
static let codeTypeReset = 2
}
// MARK: - UserDefaults Keys
struct Keys {
/// 隐私协议已同意
static let policyAgreed = "HadAgreePrivacy"
/// 首次启动标识
static let hasLaunchedBefore = "HasLaunchedBefore"
}
// MARK: - Images 图片资源名称
struct Images {
/// 背景图
static let background = "vc_bg"
/// Logo 背景图
static let loginBg = "login_bg"
/// 登录按钮图标 - ID
static let iconLoginId = "icon_login_id"
/// 登录按钮图标 - Email
static let iconLoginEmail = "icon_login_email"
/// 图标 - 用户
static let iconPerson = "person.circle"
static let iconPersonFill = "person"
/// 图标 - 邮箱
static let iconEmail = "envelope.circle"
static let iconEmailFill = "envelope"
/// 图标 - 手机
static let iconPhone = "phone.circle"
static let iconPhoneFill = "phone"
/// 图标 - Apple
static let iconApple = "apple.logo"
/// 图标 - 锁
static let iconLock = "lock"
/// 图标 - 数字
static let iconNumber = "number"
/// 密码可见性图标
static let iconPasswordSee = "icon_password_see"
static let iconPasswordUnsee = "icon_password_unsee"
/// 图标 - 返回
static let iconBack = "chevron.left"
/// 图标 - 眼睛(隐藏)
static let iconEyeSlash = "eye.slash"
/// 图标 - 眼睛(显示)
static let iconEye = "eye"
/// Checkbox - 未选中
static let checkboxEmpty = "circle"
/// Checkbox - 已选中
static let checkboxFilled = "checkmark.circle"
}
// MARK: - Localized Strings Keys
struct LocalizedKeys {
/// ID 登录
static let idLogin = "1.0.37_text_26"
/// 邮箱登录
static let emailLogin = "20.20.51_text_1"
/// 隐私协议完整文本
static let policyFullText = "XPLoginViewController6"
/// 用户协议
static let userAgreement = "XPLoginViewController7"
/// 隐私政策
static let privacyPolicy = "XPLoginViewController9"
/// 反馈
static let feedback = "XPMineFeedbackViewController0"
}
}

View File

@@ -1,52 +0,0 @@
//
// EPLoginState.swift
// YuMi
//
// Created by AI on 2025-01-27.
//
import Foundation
/// 登录显示类型枚举
enum EPLoginDisplayType {
case id // ID + 密码
case email // 邮箱 + 验证码
case phone // 手机号 + 验证码
case emailReset // 邮箱找回密码
case phoneReset // 手机号找回密码
}
/// 登录状态验证器Phase 2 实现)
class EPLoginValidator {
/// 密码强度验证6-16位必须包含字母+数字
func validatePassword(_ password: String) -> Bool {
guard password.count >= 6 && password.count <= 16 else { return false }
let hasLetter = password.rangeOfCharacter(from: .letters) != nil
let hasDigit = password.rangeOfCharacter(from: .decimalDigits) != nil
return hasLetter && hasDigit
}
/// 邮箱格式验证
func validateEmail(_ email: String) -> Bool {
let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
return emailPredicate.evaluate(with: email)
}
/// 验证码格式验证6位数字
func validateCode(_ code: String) -> Bool {
guard code.count == 6 else { return false }
return code.allSatisfy { $0.isNumber }
}
/// 手机号格式验证(简单验证)
func validatePhone(_ phone: String) -> Bool {
let phoneRegex = "^[0-9]{10,15}$"
let phonePredicate = NSPredicate(format: "SELF MATCHES %@", phoneRegex)
return phonePredicate.evaluate(with: phone)
}
}

View File

@@ -1,149 +0,0 @@
//
// EPLoginManager.swift
// YuMi
//
// Created by AI on 2025-01-27.
//
import UIKit
/// 登录管理器Swift 版本)
/// 替代 PILoginManager处理登录成功后的路由和初始化
@objc class EPLoginManager: NSObject {
// MARK: - Login Success Navigation
/// 登录成功后跳转首页
/// - Parameter viewController: 当前视图控制器
static func jumpToHome(from viewController: UIViewController) {
// 1. 获取当前账号信息
guard let accountModel = AccountInfoStorage.instance().getCurrentAccountInfo() else {
print("[EPLoginManager] 账号信息不完整,无法继续")
return
}
let accessToken = accountModel.access_token
guard !accessToken.isEmpty else {
print("[EPLoginManager] access_token 为空,无法继续")
return
}
// 2. 请求 ticket
let loginService = EPLoginService()
loginService.requestTicket(accessToken: accessToken) { ticket in
// 3. 保存 ticket
AccountInfoStorage.instance().saveTicket(ticket)
// 4. 切换到 EPTabBarController
DispatchQueue.main.async {
let epTabBar = EPTabBarController.create()
epTabBar.refreshTabBarWithIsLogin(true)
// 设置为根控制器
if let window = getKeyWindow() {
window.rootViewController = epTabBar
window.makeKeyAndVisible()
// 延迟检查专属颜色(登录成功后引导)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
Self.checkAndShowSignatureColorGuide(in: window)
}
}
print("[EPLoginManager] 登录成功,已切换到 EPTabBarController")
}
} failure: { code, msg in
print("[EPLoginManager] 请求 Ticket 失败: \(code) - \(msg)")
// Ticket 请求失败,仍然跳转到首页(保持原有行为)
DispatchQueue.main.async {
let epTabBar = EPTabBarController.create()
epTabBar.refreshTabBarWithIsLogin(true)
if let window = getKeyWindow() {
window.rootViewController = epTabBar
window.makeKeyAndVisible()
// 延迟检查专属颜色(登录成功后引导)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
Self.checkAndShowSignatureColorGuide(in: window)
}
}
print("[EPLoginManager] Ticket 请求失败,仍跳转到首页")
}
}
}
/// Apple Login 接口占位(不实现)
/// - Parameter viewController: 当前视图控制器
static func loginWithApple(from viewController: UIViewController) {
print("[EPLoginManager] Apple Login - 占位Phase 2 实现")
// 占位,打印 log
}
// MARK: - Helper Methods
/// 获取 keyWindowiOS 13+ 兼容)
private static func getKeyWindow() -> UIWindow? {
if #available(iOS 13.0, *) {
for windowScene in UIApplication.shared.connectedScenes {
if let windowScene = windowScene as? UIWindowScene,
windowScene.activationState == .foregroundActive {
for window in windowScene.windows {
if window.isKeyWindow {
return window
}
}
// 如果没有 keyWindow返回第一个 window
return windowScene.windows.first
}
}
} else {
// iOS 13 以下,使用旧方法(已废弃但仍然可用)
return UIApplication.shared.keyWindow
}
return nil
}
/// 检查并显示专属颜色引导页
private static func checkAndShowSignatureColorGuide(in window: UIWindow) {
let hasSignatureColor = EPEmotionColorStorage.hasUserSignatureColor()
// #if DEBUG
print("[EPLoginManager] Debug 模式:显示专属颜色引导页(已有颜色: \(hasSignatureColor)")
let guideView = EPSignatureColorGuideView()
// 设置颜色确认回调
guideView.onColorConfirmed = { (hexColor: String) in
EPEmotionColorStorage.saveUserSignatureColor(hexColor)
print("[EPLoginManager] 用户选择专属颜色: \(hexColor)")
}
// 如果已有颜色,设置 Skip 回调
if hasSignatureColor {
guideView.onSkipTapped = {
print("[EPLoginManager] 用户跳过专属颜色选择")
}
}
// 显示引导页,已有颜色时显示 Skip 按钮
guideView.show(in: window, showSkipButton: hasSignatureColor)
// #else
// // Release 环境:仅在未设置专属颜色时显示
// if !hasSignatureColor {
// let guideView = EPSignatureColorGuideView()
// guideView.onColorConfirmed = { (hexColor: String) in
// EPEmotionColorStorage.saveUserSignatureColor(hexColor)
// }
// guideView.show(in: window)
// }
// #endif
}
}

View File

@@ -1,303 +0,0 @@
//
// EPLoginService.swift
// YuMi
//
// Created by AI on 2025-01-27.
//
import Foundation
/// 登录服务封装Swift 现代化版本)
/// 统一封装所有登录相关 API完全替代 OC 版本的 LoginPresenter
@objc class EPLoginService: NSObject {
// MARK: - Constants
private let clientSecret = EPLoginConfig.API.clientSecret
private let clientId = EPLoginConfig.API.clientId
private let version = EPLoginConfig.API.version
// MARK: - Private Helper Methods
/// 解析并保存 AccountModel
/// - Parameters:
/// - data: API 返回的数据
/// - code: 状态码
/// - completion: 成功回调
/// - failure: 失败回调
private func parseAndSaveAccount(data: BaseModel?,
code: Int64,
completion: @escaping (AccountModel) -> Void,
failure: @escaping (Int, String) -> Void) {
if code == 200 {
if let accountDict = data?.data as? NSDictionary,
let accountModel = AccountModel.mj_object(withKeyValues: accountDict) {
// 保存账号信息
AccountInfoStorage.instance().saveAccountInfo(accountModel)
completion(accountModel)
} else {
failure(Int(code), YMLocalizedString("error.account_parse_failed"))
}
} else {
failure(Int(code), YMLocalizedString("error.operation_failed"))
}
}
// MARK: - Request Ticket
/// 请求 Ticket登录成功后调用
/// - Parameters:
/// - accessToken: 访问令牌
/// - completion: 成功回调 (ticket)
/// - failure: 失败回调 (错误码, 错误信息)
@objc func requestTicket(accessToken: String,
completion: @escaping (String) -> Void,
failure: @escaping (Int, String) -> Void) {
Api.requestTicket({ (data, code, msg) in
if code == 200, let dict = data?.data as? NSDictionary {
if let tickets = dict["tickets"] as? NSArray,
let firstTicket = tickets.firstObject as? NSDictionary,
let ticket = firstTicket["ticket"] as? String {
completion(ticket)
} else {
failure(Int(code), YMLocalizedString("error.ticket_parse_failed"))
}
} else {
failure(Int(code), msg ?? YMLocalizedString("error.request_ticket_failed"))
}
}, access_token: accessToken, issue_type: "multi")
}
// MARK: - Send Verification Code
/// 发送邮箱验证码
/// - Parameters:
/// - email: 邮箱地址
/// - type: 类型 (1=登录, 2=找回密码)
/// - completion: 成功回调
/// - failure: 失败回调
@objc func sendEmailCode(email: String,
type: Int,
completion: @escaping () -> Void,
failure: @escaping (Int, String) -> Void) {
// 🔐 DES 加密邮箱
let encryptedEmail = encryptDES(email)
Api.emailGetCode({ (data, code, msg) in
if code == 200 {
completion()
} else {
failure(Int(code), msg ?? YMLocalizedString("error.send_email_code_failed"))
}
}, emailAddress: encryptedEmail, type: NSNumber(value: type))
}
/// 发送手机验证码
/// - Parameters:
/// - phone: 手机号
/// - areaCode: 区号
/// - type: 类型 (1=登录, 2=找回密码)
/// - completion: 成功回调
/// - failure: 失败回调
@objc func sendPhoneCode(phone: String,
areaCode: String,
type: Int,
completion: @escaping () -> Void,
failure: @escaping (Int, String) -> Void) {
// 🔐 DES 加密手机号
let encryptedPhone = encryptDES(phone)
Api.phoneSmsCode({ (data, code, msg) in
if code == 200 {
completion()
} else {
failure(Int(code), msg ?? YMLocalizedString("error.send_phone_code_failed"))
}
}, mobile: encryptedPhone, type: String(type), phoneAreaCode: areaCode)
}
// MARK: - Login Methods
/// ID + 密码登录
/// - Parameters:
/// - id: 用户 ID
/// - password: 密码
/// - completion: 成功回调 (AccountModel)
/// - failure: 失败回调
@objc func loginWithID(id: String,
password: String,
completion: @escaping (AccountModel) -> Void,
failure: @escaping (Int, String) -> Void) {
// 🔐 DES 加密 ID 和密码
let encryptedId = encryptDES(id)
let encryptedPassword = encryptDES(password)
Api.login(password: { [weak self] (data, code, msg) in
self?.parseAndSaveAccount(
data: data,
code: Int64(code),
completion: completion,
failure: { errorCode, _ in
failure(errorCode, msg ?? YMLocalizedString("error.login_failed"))
})
},
phone: encryptedId,
password: encryptedPassword,
client_secret: clientSecret,
version: version,
client_id: clientId,
grant_type: "password")
}
/// 邮箱 + 验证码登录
/// - Parameters:
/// - email: 邮箱地址
/// - code: 验证码
/// - completion: 成功回调 (AccountModel)
/// - failure: 失败回调
@objc func loginWithEmail(email: String,
code: String,
completion: @escaping (AccountModel) -> Void,
failure: @escaping (Int, String) -> Void) {
// 🔐 DES 加密邮箱
let encryptedEmail = encryptDES(email)
Api.login(code: { [weak self] (data, code, msg) in
self?.parseAndSaveAccount(
data: data,
code: Int64(code),
completion: completion,
failure: { errorCode, _ in
failure(errorCode, msg ?? YMLocalizedString("error.login_failed"))
})
},
email: encryptedEmail,
code: code,
client_secret: clientSecret,
version: version,
client_id: clientId,
grant_type: "email")
}
/// 手机号 + 验证码登录
/// - Parameters:
/// - phone: 手机号
/// - code: 验证码
/// - areaCode: 区号
/// - completion: 成功回调 (AccountModel)
/// - failure: 失败回调
@objc func loginWithPhone(phone: String,
code: String,
areaCode: String,
completion: @escaping (AccountModel) -> Void,
failure: @escaping (Int, String) -> Void) {
// 🔐 DES 加密手机号
let encryptedPhone = encryptDES(phone)
Api.login(code: { [weak self] (data, code, msg) in
self?.parseAndSaveAccount(
data: data,
code: Int64(code),
completion: completion,
failure: { errorCode, _ in
failure(errorCode, msg ?? YMLocalizedString("error.login_failed"))
})
},
phone: encryptedPhone,
code: code,
client_secret: clientSecret,
version: version,
client_id: clientId,
grant_type: "password",
phoneAreaCode: areaCode)
}
// MARK: - Reset Password
/// 邮箱重置密码
/// - Parameters:
/// - email: 邮箱地址
/// - code: 验证码
/// - newPassword: 新密码
/// - completion: 成功回调
/// - failure: 失败回调
@objc func resetEmailPassword(email: String,
code: String,
newPassword: String,
completion: @escaping () -> Void,
failure: @escaping (Int, String) -> Void) {
// 🔐 DES 加密邮箱和新密码
let encryptedEmail = encryptDES(email)
let encryptedPassword = encryptDES(newPassword)
Api.resetPassword(email: { (data, code, msg) in
if code == 200 {
completion()
} else {
failure(Int(code), msg ?? YMLocalizedString("error.reset_password_failed"))
}
}, email: encryptedEmail, newPwd: encryptedPassword, code: code)
}
/// 手机号重置密码
/// - Parameters:
/// - phone: 手机号
/// - code: 验证码
/// - areaCode: 区号
/// - newPassword: 新密码
/// - completion: 成功回调
/// - failure: 失败回调
@objc func resetPhonePassword(phone: String,
code: String,
areaCode: String,
newPassword: String,
completion: @escaping () -> Void,
failure: @escaping (Int, String) -> Void) {
// 🔐 DES 加密手机号和新密码
let encryptedPhone = encryptDES(phone)
let encryptedPassword = encryptDES(newPassword)
Api.resetPassword(phone: { (data, code, msg) in
if code == 200 {
completion()
} else {
failure(Int(code), msg ?? YMLocalizedString("error.reset_password_failed"))
}
}, phone: encryptedPhone, newPwd: encryptedPassword, smsCode: code, phoneAreaCode: areaCode)
}
// MARK: - Phone Quick Login (保留接口)
/// 手机快速登录(保留接口但 UI 暂不暴露)
/// - Parameters:
/// - accessToken: 访问令牌
/// - token: 令牌
/// - completion: 成功回调 (AccountModel)
/// - failure: 失败回调
@objc func phoneQuickLogin(accessToken: String,
token: String,
completion: @escaping (AccountModel) -> Void,
failure: @escaping (Int, String) -> Void) {
Api.phoneQuickLogin({ [weak self] (data, code, msg) in
self?.parseAndSaveAccount(
data: data,
code: Int64(code),
completion: completion,
failure: { errorCode, _ in
failure(errorCode, msg ?? YMLocalizedString("error.quick_login_failed"))
})
},
accessToken: accessToken,
token: token)
}
}

View File

@@ -1,131 +0,0 @@
//
// EPLoginButton.swift
// YuMi
//
// Created by AI on 2025-01-27.
// 登录按钮组件 - 使用 StackView 实现 icon 左侧固定 + title 居中
//
import UIKit
import SnapKit
/// 登录按钮点击代理
protocol EPLoginButtonDelegate: AnyObject {
func loginButtonDidTap(_ button: EPLoginButton)
}
/// 登录按钮组件
class EPLoginButton: UIControl {
// MARK: - Properties
weak var delegate: EPLoginButtonDelegate?
private let stackView = UIStackView()
private let iconImageView = UIImageView()
private let titleLabel = UILabel()
private let leftSpacer = UIView()
private let rightSpacer = UIView()
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Setup
private func setupUI() {
backgroundColor = EPLoginConfig.Colors.background
layer.cornerRadius = EPLoginConfig.Layout.cornerRadius
// StackView 配置
stackView.axis = .horizontal
stackView.alignment = .center
stackView.distribution = .fill
stackView.spacing = 0
stackView.isUserInteractionEnabled = false
addSubview(stackView)
// Icon
iconImageView.contentMode = .scaleAspectFit
// Title
titleLabel.font = .systemFont(ofSize: EPLoginConfig.Layout.inputFontSize, weight: .semibold)
titleLabel.textColor = EPLoginConfig.Colors.text
titleLabel.textAlignment = .center
// Spacers - 让 title 居中
leftSpacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
rightSpacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
// 布局顺序: [Leading 33] + [Icon] + [Flexible Spacer] + [Title] + [Flexible Spacer] + [Trailing 33]
let leadingPadding = UIView()
let trailingPadding = UIView()
stackView.addArrangedSubview(leadingPadding)
stackView.addArrangedSubview(iconImageView)
stackView.addArrangedSubview(leftSpacer)
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(rightSpacer)
stackView.addArrangedSubview(trailingPadding)
// 约束
stackView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
leadingPadding.snp.makeConstraints { make in
make.width.equalTo(EPLoginConfig.Layout.loginButtonIconLeading)
}
iconImageView.snp.makeConstraints { make in
make.size.equalTo(EPLoginConfig.Layout.loginButtonIconSize)
}
trailingPadding.snp.makeConstraints { make in
make.width.equalTo(EPLoginConfig.Layout.loginButtonIconLeading)
}
// 设置 leftSpacer 和 rightSpacer 宽度相等,实现 title 居中
leftSpacer.snp.makeConstraints { make in
make.width.equalTo(rightSpacer)
}
// 添加点击事件
addTarget(self, action: #selector(handleTap), for: .touchUpInside)
}
// MARK: - Configuration
/// 配置按钮
/// - Parameters:
/// - icon: 图标名称
/// - title: 标题文字
func configure(icon: String, title: String) {
iconImageView.image = kImage(icon)
titleLabel.text = title
}
// MARK: - Actions
@objc private func handleTap() {
delegate?.loginButtonDidTap(self)
}
// MARK: - Touch Feedback
override var isHighlighted: Bool {
didSet {
UIView.animate(withDuration: 0.1) {
self.alpha = self.isHighlighted ? 0.7 : 1.0
}
}
}
}

View File

@@ -1,322 +0,0 @@
//
// EPLoginInputView.swift
// YuMi
//
// Created by AI on 2025-01-27.
// 登录输入框组件 - 支持区号、验证码、密码切换等完整功能
//
import UIKit
import SnapKit
/// 输入框配置
struct EPLoginInputConfig {
var showAreaCode: Bool = false
var showCodeButton: Bool = false
var isSecure: Bool = false
var icon: String?
var placeholder: String
var keyboardType: UIKeyboardType = .default
}
/// 输入框代理
protocol EPLoginInputViewDelegate: AnyObject {
func inputViewDidRequestCode(_ inputView: EPLoginInputView)
func inputViewDidSelectArea(_ inputView: EPLoginInputView)
}
/// 登录输入框组件
class EPLoginInputView: UIView {
// MARK: - Properties
weak var delegate: EPLoginInputViewDelegate?
/// 输入内容变化回调
var onTextChanged: ((String) -> Void)?
private let stackView = UIStackView()
// 区号区域
private let areaStackView = UIStackView()
private let areaCodeButton = UIButton(type: .custom)
private let areaArrowImageView = UIImageView()
private let areaTapButton = UIButton(type: .custom)
// 输入框
private let inputTextField = UITextField()
private let iconImageView = UIImageView()
// 眼睛按钮(密码可见性切换)
private let eyeButton = UIButton(type: .custom)
// 验证码按钮
private let codeButton = UIButton(type: .custom)
// 倒计时
private var timer: DispatchSourceTimer?
private var countdownSeconds = 60
private var isCountingDown = false
// 配置
private var config: EPLoginInputConfig?
/// 获取输入内容
var text: String {
return inputTextField.text ?? ""
}
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
stopCountdown()
}
// MARK: - Setup
private func setupUI() {
backgroundColor = EPLoginConfig.Colors.inputBackground
layer.cornerRadius = EPLoginConfig.Layout.inputCornerRadius
layer.borderWidth = EPLoginConfig.Layout.inputBorderWidth
layer.borderColor = EPLoginConfig.Colors.inputBorder.cgColor
// Main StackView
stackView.axis = .horizontal
stackView.alignment = .center
stackView.distribution = .fill
stackView.spacing = 8
stackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
setupAreaCodeView()
setupInputTextField()
setupEyeButton()
setupCodeButton()
stackView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(EPLoginConfig.Layout.inputHorizontalPadding)
make.trailing.equalToSuperview().offset(-EPLoginConfig.Layout.inputHorizontalPadding)
make.top.bottom.equalToSuperview()
}
// 默认隐藏所有可选组件
areaStackView.isHidden = true
eyeButton.isHidden = true
codeButton.isHidden = true
iconImageView.isHidden = true
}
private func setupAreaCodeView() {
// 区号 StackView
areaStackView.axis = .horizontal
areaStackView.alignment = .center
areaStackView.distribution = .fill
areaStackView.spacing = 8
areaStackView.translatesAutoresizingMaskIntoConstraints = false
// 区号按钮
areaCodeButton.setTitle("+86", for: .normal)
areaCodeButton.setTitleColor(EPLoginConfig.Colors.inputText, for: .normal)
areaCodeButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
areaCodeButton.isUserInteractionEnabled = false
areaCodeButton.translatesAutoresizingMaskIntoConstraints = false
// 箭头图标
areaArrowImageView.image = kImage("login_area_arrow")
areaArrowImageView.contentMode = .scaleAspectFit
areaArrowImageView.isUserInteractionEnabled = false
areaArrowImageView.translatesAutoresizingMaskIntoConstraints = false
// 点击区域按钮
areaTapButton.translatesAutoresizingMaskIntoConstraints = false
areaTapButton.addTarget(self, action: #selector(handleAreaTap), for: .touchUpInside)
areaStackView.addSubview(areaTapButton)
areaStackView.addArrangedSubview(areaCodeButton)
areaStackView.addArrangedSubview(areaArrowImageView)
stackView.addArrangedSubview(areaStackView)
areaTapButton.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
areaCodeButton.snp.makeConstraints { make in
make.width.lessThanOrEqualTo(60)
}
areaArrowImageView.snp.makeConstraints { make in
make.width.equalTo(12)
make.height.equalTo(8)
}
}
private func setupInputTextField() {
// Icon (可选)
iconImageView.contentMode = .scaleAspectFit
iconImageView.tintColor = EPLoginConfig.Colors.icon
iconImageView.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(iconImageView)
iconImageView.snp.makeConstraints { make in
make.size.equalTo(EPLoginConfig.Layout.inputIconSize)
}
// TextField
inputTextField.textColor = EPLoginConfig.Colors.textLight
inputTextField.font = .systemFont(ofSize: 14)
inputTextField.tintColor = EPLoginConfig.Colors.textLight
inputTextField.translatesAutoresizingMaskIntoConstraints = false
inputTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
stackView.addArrangedSubview(inputTextField)
}
@objc private func textFieldDidChange() {
onTextChanged?(inputTextField.text ?? "")
}
private func setupEyeButton() {
eyeButton.translatesAutoresizingMaskIntoConstraints = false
eyeButton.setImage(kImage(EPLoginConfig.Images.iconPasswordUnsee), for: .normal)
eyeButton.setImage(kImage(EPLoginConfig.Images.iconPasswordSee), for: .selected)
eyeButton.addTarget(self, action: #selector(handleEyeTap), for: .touchUpInside)
stackView.addArrangedSubview(eyeButton)
eyeButton.snp.makeConstraints { make in
make.size.equalTo(24)
}
}
private func setupCodeButton() {
codeButton.translatesAutoresizingMaskIntoConstraints = false
codeButton.setTitle(YMLocalizedString("XPLoginInputView0"), for: .normal)
codeButton.setTitleColor(.white, for: .normal)
codeButton.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
codeButton.titleLabel?.textAlignment = .center
codeButton.titleLabel?.numberOfLines = 2
codeButton.layer.cornerRadius = EPLoginConfig.Layout.codeButtonHeight / 2
codeButton.backgroundColor = EPLoginConfig.Colors.codeButtonBackground
codeButton.addTarget(self, action: #selector(handleCodeTap), for: .touchUpInside)
stackView.addArrangedSubview(codeButton)
codeButton.snp.makeConstraints { make in
make.width.equalTo(EPLoginConfig.Layout.codeButtonWidth)
make.height.equalTo(EPLoginConfig.Layout.codeButtonHeight)
}
}
// MARK: - Configuration
/// 配置输入框
func configure(with config: EPLoginInputConfig) {
self.config = config
// 区号
areaStackView.isHidden = !config.showAreaCode
// Icon - 默认隐藏,不再使用
iconImageView.isHidden = true
// Placeholder60% 白色)
inputTextField.attributedPlaceholder = NSAttributedString(
string: config.placeholder,
attributes: [NSAttributedString.Key.foregroundColor: UIColor.white.withAlphaComponent(0.6)]
)
// 键盘类型
inputTextField.keyboardType = config.keyboardType
// 密码模式
inputTextField.isSecureTextEntry = config.isSecure
eyeButton.isHidden = !config.isSecure
// 验证码按钮
codeButton.isHidden = !config.showCodeButton
}
/// 设置区号
func setAreaCode(_ code: String) {
areaCodeButton.setTitle(code, for: .normal)
}
/// 清空输入
func clearInput() {
inputTextField.text = ""
}
/// 弹出键盘(自动聚焦输入框)
func displayKeyboard() {
inputTextField.becomeFirstResponder()
}
// MARK: - Actions
@objc private func handleAreaTap() {
delegate?.inputViewDidSelectArea(self)
}
@objc private func handleEyeTap() {
eyeButton.isSelected.toggle()
inputTextField.isSecureTextEntry = !eyeButton.isSelected
}
@objc private func handleCodeTap() {
guard !isCountingDown else { return }
delegate?.inputViewDidRequestCode(self)
}
// MARK: - Countdown
/// 开始倒计时
func startCountdown() {
guard !isCountingDown else { return }
isCountingDown = true
countdownSeconds = 60
codeButton.isEnabled = false
codeButton.backgroundColor = EPLoginConfig.Colors.iconDisabled
let queue = DispatchQueue.main
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now(), repeating: 1.0)
timer.setEventHandler { [weak self] in
guard let self = self else { return }
self.countdownSeconds -= 1
if self.countdownSeconds <= 0 {
self.stopCountdown()
self.codeButton.setTitle(YMLocalizedString("XPLoginInputView1"), for: .normal)
} else {
self.codeButton.setTitle("\(self.countdownSeconds)s", for: .normal)
}
}
timer.resume()
self.timer = timer
}
/// 停止倒计时
func stopCountdown() {
guard let timer = timer else { return }
timer.cancel()
self.timer = nil
isCountingDown = false
codeButton.isEnabled = true
codeButton.backgroundColor = EPLoginConfig.Colors.codeButtonBackground
codeButton.setTitle(YMLocalizedString("XPLoginInputView0"), for: .normal)
}
}

View File

@@ -1,151 +0,0 @@
//
// EPPolicyLabel.swift
// YuMi
//
// Created by AI on 2025-01-27.
//
import UIKit
class EPPolicyLabel: UILabel {
// MARK: - Properties
var onUserAgreementTapped: (() -> Void)?
var onPrivacyPolicyTapped: (() -> Void)?
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
// MARK: - Setup
private func setup() {
numberOfLines = 0
isUserInteractionEnabled = true
// 使用 YMLocalizedString 获取文案
let fullText = YMLocalizedString("XPLoginViewController6")
let userAgreementText = YMLocalizedString("XPLoginViewController7")
let privacyPolicyText = YMLocalizedString("XPLoginViewController9")
let attributedString = NSMutableAttributedString(string: fullText)
attributedString.addAttribute(NSAttributedString.Key.foregroundColor,
value: UIColor.darkGray,
range: NSRange(location: 0, length: fullText.count))
attributedString.addAttribute(NSAttributedString.Key.font,
value: UIFont.systemFont(ofSize: 12),
range: NSRange(location: 0, length: fullText.count))
// 高亮用户协议(蓝色)
if let userRange = fullText.range(of: userAgreementText) {
let nsRange = NSRange(userRange, in: fullText)
attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.systemBlue, range: nsRange)
attributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: nsRange)
}
// 高亮隐私政策(蓝色)
if let privacyRange = fullText.range(of: privacyPolicyText) {
let nsRange = NSRange(privacyRange, in: fullText)
attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.systemBlue, range: nsRange)
attributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: nsRange)
}
attributedText = attributedString
// 添加点击手势
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
addGestureRecognizer(tapGesture)
}
// MARK: - Actions
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
guard let attributedText = self.attributedText else {
print("[EPPolicyLabel] No attributed text")
return
}
let text = attributedText.string
let userAgreementText = YMLocalizedString("XPLoginViewController7")
let privacyPolicyText = YMLocalizedString("XPLoginViewController9")
print("[EPPolicyLabel] Tap detected, text: \(text)")
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: bounds.size)
let textStorage = NSTextStorage(attributedString: attributedText)
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
textContainer.lineFragmentPadding = 0
textContainer.maximumNumberOfLines = numberOfLines
textContainer.lineBreakMode = lineBreakMode
let locationOfTouchInLabel = gesture.location(in: self)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
// 根据 textAlignment 计算偏移
var textContainerOffset = CGPoint.zero
switch textAlignment {
case .left, .natural, .justified:
textContainerOffset = CGPoint(x: 0, y: (bounds.height - textBoundingBox.height) / 2)
case .center:
textContainerOffset = CGPoint(x: (bounds.width - textBoundingBox.width) / 2,
y: (bounds.height - textBoundingBox.height) / 2)
case .right:
textContainerOffset = CGPoint(x: bounds.width - textBoundingBox.width,
y: (bounds.height - textBoundingBox.height) / 2)
@unknown default:
textContainerOffset = CGPoint(x: 0, y: (bounds.height - textBoundingBox.height) / 2)
}
let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x,
y: locationOfTouchInLabel.y - textContainerOffset.y)
// 确保点击在文本区域内
guard textBoundingBox.contains(locationOfTouchInTextContainer) else {
print("[EPPolicyLabel] Tap outside text bounds")
return
}
let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer,
in: textContainer,
fractionOfDistanceBetweenInsertionPoints: nil)
print("[EPPolicyLabel] Character index: \(indexOfCharacter)")
// 检查点击位置
if let userRange = text.range(of: userAgreementText) {
let nsRange = NSRange(userRange, in: text)
print("[EPPolicyLabel] User agreement range: \(nsRange)")
if NSLocationInRange(indexOfCharacter, nsRange) {
print("[EPPolicyLabel] User agreement tapped!")
onUserAgreementTapped?()
return
}
}
if let privacyRange = text.range(of: privacyPolicyText) {
let nsRange = NSRange(privacyRange, in: text)
print("[EPPolicyLabel] Privacy policy range: \(nsRange)")
if NSLocationInRange(indexOfCharacter, nsRange) {
print("[EPPolicyLabel] Privacy policy tapped!")
onPrivacyPolicyTapped?()
return
}
}
print("[EPPolicyLabel] No link tapped")
}
}

View File

@@ -1,162 +0,0 @@
//
// EPAboutUsViewController.swift
// YuMi
//
// Created by AI on 2025-01-28.
//
import UIKit
import SnapKit
/// About Us 页面
/// 展示应用图标和版本信息
class EPAboutUsViewController: BaseViewController {
// MARK: - UI Components
private lazy var appIconImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.layer.cornerRadius = 20
imageView.layer.masksToBounds = true
// 获取应用图标
if let iconName = Bundle.main.object(forInfoDictionaryKey: "CFBundleIconName") as? String {
imageView.image = UIImage(named: iconName)
} else if let icons = Bundle.main.object(forInfoDictionaryKey: "CFBundleIcons") as? [String: Any],
let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any],
let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String],
let lastIcon = iconFiles.last {
imageView.image = UIImage(named: lastIcon)
} else {
// 使用占位图标
imageView.image = UIImage(named: "pi_app_logo_new_bg")
}
return imageView
}()
private lazy var appNameLabel: UILabel = {
let label = UILabel()
label.text = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String
?? "YuMi"
label.textColor = .white
label.font = .systemFont(ofSize: 24, weight: .bold)
label.textAlignment = .center
return label
}()
private lazy var versionLabel: UILabel = {
let label = UILabel()
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0.0"
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "1"
label.text = "Version \(version) (\(build))"
label.textColor = UIColor.white.withAlphaComponent(0.7)
label.font = .systemFont(ofSize: 16)
label.textAlignment = .center
return label
}()
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupNavigationBar()
setupUI()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(false, animated: animated)
}
// MARK: - Setup
private func setupNavigationBar() {
title = YMLocalizedString("EPEditSetting.AboutUs")
// 配置导航栏外观iOS 13+
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = UIColor(hex: "#0C0527")
appearance.titleTextAttributes = [
.foregroundColor: UIColor.white,
.font: UIFont.systemFont(ofSize: 18, weight: .medium)
]
appearance.shadowColor = .clear // 移除底部分割线
navigationController?.navigationBar.standardAppearance = appearance
navigationController?.navigationBar.scrollEdgeAppearance = appearance
navigationController?.navigationBar.compactAppearance = appearance
navigationController?.navigationBar.tintColor = .white // 返回按钮颜色
// 隐藏返回按钮文字,只保留箭头
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
private func setupUI() {
view.backgroundColor = UIColor(hex: "#0C0527")
// 创建容器视图
let containerView = UIView()
view.addSubview(containerView)
// 添加 UI 组件到容器
containerView.addSubview(appIconImageView)
containerView.addSubview(appNameLabel)
containerView.addSubview(versionLabel)
// 布局容器(垂直居中)
containerView.snp.makeConstraints { make in
make.centerY.equalTo(view).offset(-50) // 稍微偏上
make.leading.trailing.equalTo(view).inset(40)
}
// 应用图标
appIconImageView.snp.makeConstraints { make in
make.top.equalTo(containerView)
make.centerX.equalTo(containerView)
make.size.equalTo(100)
}
// 应用名称
appNameLabel.snp.makeConstraints { make in
make.top.equalTo(appIconImageView.snp.bottom).offset(24)
make.leading.trailing.equalTo(containerView)
}
// 版本号
versionLabel.snp.makeConstraints { make in
make.top.equalTo(appNameLabel.snp.bottom).offset(12)
make.leading.trailing.equalTo(containerView)
}
}
}
// MARK: - UIColor Extension
private extension UIColor {
convenience init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (1, 1, 1, 0)
}
self.init(
red: CGFloat(r) / 255,
green: CGFloat(g) / 255,
blue: CGFloat(b) / 255,
alpha: CGFloat(a) / 255
)
}
}

View File

@@ -1,850 +0,0 @@
//
// EPEditSettingViewController.swift
// YuMi
//
// Created by AI on 2025-01-27.
//
import UIKit
import Photos
import SnapKit
import WebKit
/// 设置编辑页面
/// 支持头像更新、昵称修改和退出登录功能
class EPEditSettingViewController: BaseViewController {
// MARK: - UI Components
private lazy var profileImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = 60 // 120/2 = 60
imageView.layer.masksToBounds = true
imageView.backgroundColor = .systemGray5
imageView.isUserInteractionEnabled = true
return imageView
}()
private lazy var cameraIconView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(named: "icon_setting_camear")
imageView.backgroundColor = UIColor(hex: "#0C0527")
imageView.layer.cornerRadius = 15 // 30/2 = 15
imageView.layer.masksToBounds = true
return imageView
}()
private lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.backgroundColor = UIColor(hex: "#0C0527")
tableView.separatorStyle = .none
tableView.delegate = self
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "SettingCell")
tableView.isScrollEnabled = true // 启用内部滚动
return tableView
}()
private lazy var logoutButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle(YMLocalizedString("EPEditSetting.Logout"), for: .normal)
button.setTitleColor(.white, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold)
button.layer.cornerRadius = 25
button.addTarget(self, action: #selector(logoutButtonTapped), for: .touchUpInside)
return button
}()
// MARK: - Data
private var settingItems: [SettingItem] = []
private var userInfo: UserInfoModel?
private var apiHelper: EPMineAPIHelper = EPMineAPIHelper()
private var hasAddedGradient = false
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupNavigationBar()
setupUI()
setupData()
loadUserInfo()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(false, animated: animated)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// 恢复父页面的导航栏配置(透明)
restoreParentNavigationBarStyle()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// 添加渐变背景到 Logout 按钮(只添加一次)
if !hasAddedGradient && logoutButton.bounds.width > 0 {
logoutButton.addGradientBackground(
with: [
UIColor(red: 0xF8/255.0, green: 0x54/255.0, blue: 0xFC/255.0, alpha: 1.0), // #F854FC
UIColor(red: 0x50/255.0, green: 0x0F/255.0, blue: 0xFF/255.0, alpha: 1.0) // #500FFF
],
start: CGPoint(x: 0, y: 0.5),
end: CGPoint(x: 1, y: 0.5),
cornerRadius: 25
)
hasAddedGradient = true
}
}
// MARK: - Setup
private func setupNavigationBar() {
title = YMLocalizedString("EPEditSetting.Title")
// 配置导航栏外观iOS 13+
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = UIColor(hex: "#0C0527")
appearance.titleTextAttributes = [
.foregroundColor: UIColor.white,
.font: UIFont.systemFont(ofSize: 18, weight: .medium)
]
appearance.shadowColor = .clear // 移除底部分割线
navigationController?.navigationBar.standardAppearance = appearance
navigationController?.navigationBar.scrollEdgeAppearance = appearance
navigationController?.navigationBar.compactAppearance = appearance
navigationController?.navigationBar.tintColor = .white // 返回按钮颜色
// 隐藏返回按钮文字,只保留箭头
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
// 如果是从上一页 push 进来的,也要修改上一页的 backButtonTitle
navigationController?.navigationBar.topItem?.backBarButtonItem = UIBarButtonItem(
title: "",
style: .plain,
target: nil,
action: nil
)
}
private func restoreParentNavigationBarStyle() {
// 恢复透明导航栏EPMineViewController 使用的是透明导航栏)
let transparentAppearance = UINavigationBarAppearance()
transparentAppearance.configureWithTransparentBackground()
transparentAppearance.backgroundColor = .clear
transparentAppearance.shadowColor = .clear
navigationController?.navigationBar.standardAppearance = transparentAppearance
navigationController?.navigationBar.scrollEdgeAppearance = transparentAppearance
navigationController?.navigationBar.compactAppearance = transparentAppearance
}
private func setupUI() {
view.backgroundColor = UIColor(hex: "#0C0527")
// 设置头像布局
view.addSubview(profileImageView)
profileImageView.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(40)
make.centerX.equalTo(view)
make.size.equalTo(120)
}
// 设置相机图标布局
view.addSubview(cameraIconView)
cameraIconView.snp.makeConstraints { make in
make.bottom.equalTo(profileImageView.snp.bottom)
make.trailing.equalTo(profileImageView.snp.trailing)
make.size.equalTo(30)
}
// 设置 Logout 按钮布局
view.addSubview(logoutButton)
logoutButton.snp.makeConstraints { make in
make.leading.trailing.equalTo(view).inset(20)
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-40)
make.height.equalTo(50)
}
// 设置 TableView 布局
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.top.equalTo(profileImageView.snp.bottom).offset(40)
make.leading.trailing.equalTo(view)
make.bottom.equalTo(logoutButton.snp.top).offset(-20)
}
// 添加头像点击手势
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(profileImageTapped))
profileImageView.addGestureRecognizer(tapGesture)
// 添加相机图标点击手势
let cameraTapGesture = UITapGestureRecognizer(target: self, action: #selector(profileImageTapped))
cameraIconView.addGestureRecognizer(cameraTapGesture)
}
private func setupData() {
settingItems = [
SettingItem(
title: YMLocalizedString("EPEditSetting.PersonalInfo"),
action: { [weak self] in self?.handleReservedAction("PersonalInfo") }
),
SettingItem(
title: YMLocalizedString("EPEditSetting.Help"),
action: { [weak self] in self?.handleReservedAction("Help") }
),
SettingItem(
title: YMLocalizedString("EPEditSetting.ClearCache"),
action: { [weak self] in self?.handleReservedAction("ClearCache") }
),
SettingItem(
title: YMLocalizedString("EPEditSetting.AboutUs"),
action: { [weak self] in self?.handleReservedAction("AboutUs") }
)
]
NSLog("[EPEditSetting] setupData 完成,设置项数量: \(settingItems.count)")
}
private func loadUserInfo() {
// 如果已经有用户信息(从 EPMineViewController 传递),则不需要重新加载
if userInfo != nil {
updateProfileImage()
tableView.reloadData()
return
}
// 获取当前用户信息
guard let uid = AccountInfoStorage.instance().getUid(), !uid.isEmpty else {
print("[EPEditSetting] 未登录,无法获取用户信息")
return
}
// TODO: 调用API获取用户详细信息
// 这里暂时创建默认的UserInfoModel用于显示
let tempUserInfo = UserInfoModel()
tempUserInfo.nick = "User"
tempUserInfo.avatar = ""
userInfo = tempUserInfo
updateProfileImage()
tableView.reloadData()
}
private func updateProfileImage() {
guard let avatarUrl = userInfo?.avatar, !avatarUrl.isEmpty else {
profileImageView.image = UIImage(systemName: "person.circle.fill")
return
}
// 使用SDWebImage加载头像
if let url = URL(string: avatarUrl) {
profileImageView.sd_setImage(with: url, placeholderImage: UIImage(systemName: "person.circle.fill"))
}
}
// MARK: - Actions
@objc private func profileImageTapped() {
showAvatarSelectionSheet()
}
@objc private func openSettings() {
// 预留设置按钮功能
handleReservedAction("Settings")
}
@objc private func logoutButtonTapped() {
showLogoutConfirm()
}
private func showAvatarSelectionSheet() {
let alert = UIAlertController(title: YMLocalizedString("EPEditSetting.EditNickname"), message: nil, preferredStyle: .actionSheet)
// 拍照选项
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Camera"), style: .default) { [weak self] _ in
self?.checkCameraPermissionAndPresent()
})
// 相册选项
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.PhotoLibrary"), style: .default) { [weak self] _ in
self?.checkPhotoLibraryPermissionAndPresent()
})
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Cancel"), style: .cancel))
// iPad支持
if let popover = alert.popoverPresentationController {
popover.sourceView = profileImageView
popover.sourceRect = profileImageView.bounds
}
present(alert, animated: true)
}
private func checkCameraPermissionAndPresent() {
YYUtility.checkCameraAvailable { [weak self] in
self?.presentImagePicker(sourceType: .camera)
} denied: { [weak self] in
self?.showPermissionAlert(title: "Camera Access", message: "Please allow camera access in Settings")
} restriction: { [weak self] in
self?.showPermissionAlert(title: "Camera Restricted", message: "Camera access is restricted on this device")
}
}
private func checkPhotoLibraryPermissionAndPresent() {
YYUtility.checkAssetsLibrayAvailable { [weak self] in
self?.presentImagePicker(sourceType: .photoLibrary)
} denied: { [weak self] in
self?.showPermissionAlert(title: "Photo Library Access", message: "Please allow photo library access in Settings")
} restriction: { [weak self] in
self?.showPermissionAlert(title: "Photo Library Restricted", message: "Photo library access is restricted on this device")
}
}
private func presentImagePicker(sourceType: UIImagePickerController.SourceType) {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = sourceType
imagePicker.allowsEditing = true
present(imagePicker, animated: true)
}
private func showPermissionAlert(title: String, message: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Settings", style: .default) { _ in
if let settingsURL = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(settingsURL)
}
})
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Cancel"), style: .cancel))
present(alert, animated: true)
}
private func showNicknameEditAlert() {
let alert = UIAlertController(
title: YMLocalizedString("EPEditSetting.EditNickname"),
message: nil,
preferredStyle: .alert
)
alert.addTextField { [weak self] textField in
textField.text = self?.userInfo?.nick ?? ""
textField.placeholder = YMLocalizedString("EPEditSetting.EnterNickname")
}
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Cancel"), style: .cancel))
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Confirm"), style: .default) { [weak self] _ in
guard let newNickname = alert.textFields?.first?.text, !newNickname.isEmpty else { return }
self?.updateNickname(newNickname)
})
present(alert, animated: true)
}
private func updateNickname(_ newNickname: String) {
// 显示加载状态
showLoading()
// 调用 API 更新昵称
apiHelper.updateNickname(withNick: newNickname,
completion: { [weak self] in
self?.hideHUD()
// 更新成功后才更新本地显示
self?.userInfo?.nick = newNickname
self?.tableView.reloadData()
// 显示成功提示
self?.showSuccessToast(YMLocalizedString("XPMineUserInfoEditViewController13"))
print("[EPEditSetting] 昵称更新成功: \(newNickname)")
},
failure: { [weak self] (code: Int, msg: String?) in
self?.hideHUD()
// 显示错误提示
let errorMsg = msg ?? YMLocalizedString("setting.nickname_update_failed")
self?.showErrorToast(errorMsg)
print("[EPEditSetting] 昵称更新失败: \(code) - \(errorMsg)")
}
)
}
private func showLogoutConfirm() {
let alert = UIAlertController(
title: YMLocalizedString("EPEditSetting.LogoutConfirm"),
message: nil,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Cancel"), style: .cancel))
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Logout"), style: .destructive) { [weak self] _ in
self?.performLogout()
})
present(alert, animated: true)
}
private func performLogout() {
guard let account = AccountInfoStorage.instance().accountModel else {
print("[EPEditSetting] 账号信息不存在")
return
}
// 调用登出API
Api.logoutCurrentAccount({ [weak self] (data, code, msg) in
DispatchQueue.main.async {
// 清除本地数据
AccountInfoStorage.instance().saveAccountInfo(nil)
AccountInfoStorage.instance().saveTicket(nil)
// 跳转登录页
self?.navigateToLogin()
}
}, access_token: account.access_token)
}
private func navigateToLogin() {
let loginVC = EPLoginViewController()
let nav = UINavigationController(rootViewController: loginVC)
if let window = UIApplication.shared.windows.first {
window.rootViewController = nav
window.makeKeyAndVisible()
}
print("[EPEditSetting] 已跳转到登录页面")
}
private func handleReservedAction(_ title: String) {
print("[\(title)] - 功能触发")
// About Us 已实现
if title == "AboutUs" {
let aboutVC = EPAboutUsViewController()
navigationController?.pushViewController(aboutVC, animated: true)
return
}
// Personal Info - 显示协议和隐私政策选项
if title == "PersonalInfo" {
showPolicyOptionsSheet()
return
}
// Help - 跳转到 FAQ 页面
if title == "Help" {
let faqUrl = getFAQURL()
openPolicyInExternalBrowser(faqUrl)
return
}
// Clear Cache - 清理图片和网页缓存
if title == "ClearCache" {
showClearCacheConfirmation()
return
}
// 其他功能预留
// TODO: Phase 2 implementation
let alert = UIAlertController(title: "Coming Soon", message: "This feature will be available in the next update.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
private func showClearCacheConfirmation() {
let alert = UIAlertController(
title: YMLocalizedString("EPEditSetting.ClearCacheTitle"),
message: YMLocalizedString("EPEditSetting.ClearCacheMessage"),
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Cancel"), style: .cancel))
alert.addAction(UIAlertAction(title: YMLocalizedString("EPEditSetting.Confirm"), style: .destructive) { [weak self] _ in
self?.performClearCache()
})
present(alert, animated: true)
}
private func performClearCache() {
print("[EPEditSetting] 开始清理缓存")
// 显示加载状态
showLoading()
// 1. 清理 SDWebImage 图片缓存
SDWebImageManager.shared.imageCache.clear?(with: .all) {
print("[EPEditSetting] SDWebImage 缓存已清理")
// 2. 清理 WKWebsiteDataStore 网页缓存
let websiteDataTypes = WKWebsiteDataStore.allWebsiteDataTypes()
let dateFrom = Date(timeIntervalSince1970: 0)
WKWebsiteDataStore.default().removeData(ofTypes: websiteDataTypes, modifiedSince: dateFrom) { [weak self] in
print("[EPEditSetting] WKWebsiteDataStore 缓存已清理")
DispatchQueue.main.async {
self?.hideHUD()
self?.showSuccessToast(YMLocalizedString("EPEditSetting.ClearCacheSuccess"))
print("[EPEditSetting] 缓存清理完成")
}
}
}
}
private func showPolicyOptionsSheet() {
let alert = UIAlertController(
title: nil,
message: nil,
preferredStyle: .actionSheet
)
// 用户服务协议
alert.addAction(UIAlertAction(
title: YMLocalizedString("EPEditSetting.UserAgreement"),
style: .default
) { [weak self] _ in
let url = self?.getUserAgreementURL() ?? ""
self?.openPolicyInExternalBrowser(url)
})
// 隐私政策
alert.addAction(UIAlertAction(
title: YMLocalizedString("EPEditSetting.PrivacyPolicy"),
style: .default
) { [weak self] _ in
let url = self?.getPrivacyPolicyURL() ?? ""
self?.openPolicyInExternalBrowser(url)
})
// 取消
alert.addAction(UIAlertAction(
title: YMLocalizedString("EPEditSetting.Cancel"),
style: .cancel
))
// iPad 支持
if let popover = alert.popoverPresentationController {
popover.sourceView = view
popover.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
popover.permittedArrowDirections = []
}
present(alert, animated: true)
}
/// 获取用户协议 URL
private func getUserAgreementURL() -> String {
// kUserProtocalURL 对应枚举值 4
let url = URLWithType(URLType(rawValue: 4)!) as String
print("[EPEditSetting] User agreement URL from URLWithType: \(url)")
return url
}
/// 获取隐私政策 URL
private func getPrivacyPolicyURL() -> String {
// kPrivacyURL 对应枚举值 0
let url = URLWithType(URLType(rawValue: 0)!) as String
print("[EPEditSetting] Privacy policy URL from URLWithType: \(url)")
return url
}
/// 获取 FAQ 帮助页面 URL
private func getFAQURL() -> String {
// kFAQURL 对应枚举值 6
let url = URLWithType(URLType(rawValue: 6)!) as String
print("[EPEditSetting] FAQ URL from URLWithType: \(url)")
return url
}
private func openPolicyInExternalBrowser(_ urlString: String) {
print("[EPEditSetting] Original URL: \(urlString)")
// 如果不是完整 URL拼接域名
var fullUrl = urlString
if !urlString.hasPrefix("http") && !urlString.hasPrefix("https") {
let hostUrl = HttpRequestHelper.getHostUrl()
fullUrl = "\(hostUrl)/\(urlString)"
print("[EPEditSetting] Added host URL, full URL: \(fullUrl)")
}
print("[EPEditSetting] Opening URL in external browser: \(fullUrl)")
guard let url = URL(string: fullUrl) else {
print("[EPEditSetting] ❌ Invalid URL: \(fullUrl)")
return
}
print("[EPEditSetting] URL object created: \(url)")
// 在外部浏览器中打开
if UIApplication.shared.canOpenURL(url) {
print("[EPEditSetting] ✅ Can open URL, attempting to open...")
UIApplication.shared.open(url, options: [:]) { success in
print("[EPEditSetting] Open external browser: \(success ? "✅ Success" : "❌ Failed")")
}
} else {
print("[EPEditSetting] ❌ Cannot open URL: \(fullUrl)")
}
}
// MARK: - Public Methods
/// 更新用户信息(从 EPMineViewController 传递)
@objc func updateWithUserInfo(_ userInfo: UserInfoModel) {
self.userInfo = userInfo
updateProfileImage()
tableView.reloadData()
NSLog("[EPEditSetting] 已更新用户信息: \(userInfo.nick)")
}
}
// MARK: - UITableViewDataSource & UITableViewDelegate
extension EPEditSettingViewController: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
return 1 // 只有一个 section
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let count = settingItems.count + 1 // +1 for nickname row
NSLog("[EPEditSetting] TableView rows count: \(count), settingItems: \(settingItems.count)")
return count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SettingCell", for: indexPath)
cell.backgroundColor = UIColor(hex: "#0C0527")
cell.textLabel?.textColor = .white
cell.selectionStyle = .none
// 清除之前的自定义视图
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
if indexPath.row == 0 {
// 昵称行
cell.textLabel?.text = YMLocalizedString("EPEditSetting.Nickname")
// 添加右箭头图标
let arrowImageView = UIImageView()
arrowImageView.image = UIImage(named: "icon_setting_right_arrow")
arrowImageView.contentMode = .scaleAspectFit
cell.contentView.addSubview(arrowImageView)
arrowImageView.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-20)
make.centerY.equalToSuperview()
make.size.equalTo(22)
}
// 添加用户昵称标签
let nicknameLabel = UILabel()
nicknameLabel.text = userInfo?.nick ?? YMLocalizedString("user.not_set")
nicknameLabel.textColor = .lightGray
nicknameLabel.font = UIFont.systemFont(ofSize: 16)
cell.contentView.addSubview(nicknameLabel)
nicknameLabel.snp.makeConstraints { make in
make.trailing.equalTo(arrowImageView.snp.leading).offset(-12)
make.centerY.equalToSuperview()
}
} else {
// 其他设置项
let item = settingItems[indexPath.row - 1]
cell.textLabel?.text = item.title
cell.textLabel?.textColor = .white
// 添加右箭头图标
let arrowImageView = UIImageView()
arrowImageView.image = UIImage(named: "icon_setting_right_arrow")
arrowImageView.contentMode = .scaleAspectFit
cell.contentView.addSubview(arrowImageView)
arrowImageView.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-20)
make.centerY.equalToSuperview()
make.size.equalTo(22)
}
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 60 // 所有行都是 60pt 高度
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if indexPath.row == 0 {
// 昵称点击
showNicknameEditAlert()
} else {
// 设置项点击
let item = settingItems[indexPath.row - 1]
item.action()
}
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 0
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return nil
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 0
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
return nil
}
}
// MARK: - UIImagePickerControllerDelegate & UINavigationControllerDelegate
extension EPEditSettingViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
picker.dismiss(animated: true)
guard let image = info[.editedImage] as? UIImage ?? info[.originalImage] as? UIImage else {
print("[EPEditSetting] 未能获取选择的图片")
return
}
// 更新头像显示
profileImageView.image = image
// 上传头像到腾讯云
uploadAvatar(image)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true)
}
private func uploadAvatar(_ image: UIImage) {
// 显示上传进度
EPProgressHUD.showProgress(0, total: 1)
// 使用 EPSDKManager 统一上传接口(避免腾讯云 OCR 配置问题)
EPSDKManager.shared.uploadImages([image],
progress: { uploaded, total in
EPProgressHUD.showProgress(uploaded, total: total)
},
success: { [weak self] resList in
EPProgressHUD.dismiss()
guard !resList.isEmpty,
let firstRes = resList.first,
let avatarUrl = firstRes["resUrl"] as? String else {
print("[EPEditSetting] 头像上传成功但无法获取URL")
return
}
print("[EPEditSetting] 头像上传成功: \(avatarUrl)")
// 调用API更新头像
self?.updateAvatarAPI(avatarUrl: avatarUrl)
},
failure: { [weak self] errorMsg in
EPProgressHUD.dismiss()
print("[EPEditSetting] 头像上传失败: \(errorMsg)")
// 显示错误提示
DispatchQueue.main.async {
let alert = UIAlertController(title: YMLocalizedString("common.upload_failed"), message: errorMsg, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: YMLocalizedString("common.confirm"), style: .default))
self?.present(alert, animated: true)
}
}
)
}
private func updateAvatarAPI(avatarUrl: String) {
// 使用 API Helper 更新头像
apiHelper.updateAvatar(withUrl: avatarUrl, completion: { [weak self] in
print("[EPEditSetting] 头像更新成功")
// 更新本地用户信息
self?.userInfo?.avatar = avatarUrl
// 通知父页面头像已更新
self?.notifyParentAvatarUpdated(avatarUrl)
}, failure: { [weak self] (code: Int, msg: String?) in
print("[EPEditSetting] 头像更新失败: \(code) - \(msg ?? "未知错误")")
// 显示错误提示
DispatchQueue.main.async {
let alert = UIAlertController(
title: YMLocalizedString("common.update_failed"),
message: msg ?? YMLocalizedString("setting.avatar_update_failed"),
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: YMLocalizedString("common.confirm"), style: .default))
self?.present(alert, animated: true)
}
})
}
private func notifyParentAvatarUpdated(_ avatarUrl: String) {
// 发送通知给 EPMineViewController 更新头像
let userInfo = ["avatarUrl": avatarUrl]
NotificationCenter.default.post(name: NSNotification.Name("EPEditSettingAvatarUpdated"), object: nil, userInfo: userInfo)
}
}
// MARK: - Helper Models
private struct SettingItem {
let title: String
let action: () -> Void
init(title: String, action: @escaping () -> Void) {
self.title = title
self.action = action
}
}
// MARK: - UIColor Extension
private extension UIColor {
convenience init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (1, 1, 1, 0)
}
self.init(
red: CGFloat(r) / 255,
green: CGFloat(g) / 255,
blue: CGFloat(b) / 255,
alpha: CGFloat(a) / 255
)
}
}

View File

@@ -1,20 +0,0 @@
//
// EPMineViewController.h
// YuMi
//
// Created by AI on 2025-10-09.
// Copyright © 2025 YuMi. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/// 新的个人中心页面控制器
/// 采用纵向卡片式设计,完全不同于原 XPMineViewController
/// 注意:直接继承 UIViewController不继承 BaseViewController避免依赖链
@interface EPMineViewController : UIViewController
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,220 +0,0 @@
//
// EPMineViewController.m
// YuMi
//
// Created by AI on 2025-10-09.
// Copyright © 2025 YuMi. All rights reserved.
//
#import "EPMineViewController.h"
#import "EPMineHeaderView.h"
#import "EPMomentListView.h"
#import "EPMineAPIHelper.h"
#import "AccountInfoStorage.h"
#import "UserInfoModel.h"
#import <Masonry/Masonry.h>
#import "YuMi-Swift.h" // 导入Swift桥接
@interface EPMineViewController ()
// MARK: - UI Components
/// 动态列表视图(复用 EPMomentListView
@property (nonatomic, strong) EPMomentListView *momentListView;
/// 顶部个人信息卡片
@property (nonatomic, strong) EPMineHeaderView *headerView;
// MARK: - Data
/// 用户信息模型
@property (nonatomic, strong) UserInfoModel *userInfo;
/// API Helper
@property (nonatomic, strong) EPMineAPIHelper *apiHelper;
@end
@implementation EPMineViewController
// MARK: - Lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
[self setupUI];
NSLog(@"[EPMineViewController] viewDidLoad 完成");
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.navigationController setNavigationBarHidden:YES animated:animated];
// 每次显示时加载最新数据
[self loadUserDetailInfo];
}
// MARK: - Setup
- (void)setupUI {
UIImageView *bgImageView = [[UIImageView alloc] initWithImage:kImage(@"vc_bg")];
bgImageView.contentMode = UIViewContentModeScaleAspectFill;
bgImageView.clipsToBounds = YES;
[self.view addSubview:bgImageView];
[bgImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.view);
}];
[self setupHeaderView];
[self setupMomentListView];
NSLog(@"[EPMineViewController] UI 设置完成");
}
- (void)setupHeaderView {
self.headerView = [[EPMineHeaderView alloc] initWithFrame:CGRectZero];
[self.view addSubview:self.headerView];
// 使用 Masonry 约束布局
[self.headerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.view);
make.leading.mas_equalTo(self.view);
make.trailing.mas_equalTo(self.view);
make.height.mas_equalTo(kGetScaleWidth(260));
}];
// 设置按钮点击回调
__weak typeof(self) weakSelf = self;
self.headerView.onSettingsButtonTapped = ^{
__strong typeof(weakSelf) self = weakSelf;
[self openSettings];
};
// 监听头像更新事件
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onAvatarUpdated:)
name:@"EPEditSettingAvatarUpdated"
object:nil];
}
- (void)setupMomentListView {
self.momentListView = [[EPMomentListView alloc] initWithFrame:CGRectZero];
[self.view addSubview:self.momentListView];
[self.momentListView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.headerView.mas_bottom);
make.bottom.mas_equalTo(self.view);
make.leading.mas_equalTo(self.view);
make.trailing.mas_equalTo(self.view);
}];
// 初始化为空的本地模式,避免在数据加载前触发网络请求
__weak typeof(self) weakSelf = self;
[self.momentListView loadWithDynamicInfo:@[] refreshCallback:^{
__strong typeof(weakSelf) self = weakSelf;
[self loadUserDetailInfo];
}];
}
// MARK: - Data Loading
- (void)loadUserDetailInfo {
NSString *uid = [[AccountInfoStorage instance] getUid];
if (!uid || uid.length == 0) {
NSLog(@"[EPMineViewController] 用户未登录");
return;
}
@kWeakify(self);
[self.apiHelper getUserDetailInfoWithUid:uid
completion:^(UserInfoModel * _Nullable userInfo) {
@kStrongify(self);
if (!userInfo) {
NSLog(@"[EPMineViewController] 加载用户信息失败");
return;
}
self.userInfo = userInfo;
[self updateHeaderWithUserInfo:userInfo];
// 如果有动态信息,直接使用
if (userInfo.dynamicInfo && userInfo.dynamicInfo.count > 0) {
[self.momentListView loadWithDynamicInfo:userInfo.dynamicInfo refreshCallback:^{
[self loadUserDetailInfo]; // 刷新时重新加载
}];
}
} failure:^(NSInteger code, NSString * _Nullable msg) {
NSLog(@"[EPMineViewController] 加载用户信息失败: %@", msg);
}];
}
- (void)updateHeaderWithUserInfo:(UserInfoModel *)userInfo {
NSDictionary *userInfoDict = @{
@"nickname": userInfo.nick ?: @"未设置昵称",
@"uid": [NSString stringWithFormat:@"%ld", (long)userInfo.erbanNo],
@"avatar": userInfo.avatar ?: @"",
@"following": @(userInfo.followNum),
@"followers": @(userInfo.fansNum)
};
[self.headerView updateWithUserInfo:userInfoDict];
}
// MARK: - Lazy Loading
- (EPMomentListView *)momentListView {
if (!_momentListView) {
_momentListView = [[EPMomentListView alloc] init];
__weak typeof(self) weakSelf = self;
_momentListView.onSelectMoment = ^(NSInteger index) {
__strong typeof(weakSelf) self = weakSelf;
NSLog(@"[EPMineViewController] 点击了第 %ld 条动态", (long)index);
// TODO: 跳转到动态详情页
};
}
return _momentListView;
}
- (EPMineAPIHelper *)apiHelper {
if (!_apiHelper) {
_apiHelper = [[EPMineAPIHelper alloc] init];
}
return _apiHelper;
}
// MARK: - Actions
- (void)openSettings {
// 隐藏返回按钮文字,只保留白色箭头
self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@""
style:UIBarButtonItemStylePlain
target:nil
action:nil];
EPEditSettingViewController *settingsVC = [[EPEditSettingViewController alloc] init];
// 传递用户信息到设置页面
if (self.userInfo) {
[settingsVC updateWithUserInfo:self.userInfo];
}
[self.navigationController pushViewController:settingsVC animated:YES];
NSLog(@"[EPMineViewController] 打开设置页面,已传递用户信息");
}
- (void)onAvatarUpdated:(NSNotification *)notification {
NSString *avatarUrl = notification.userInfo[@"avatarUrl"];
if (avatarUrl && self.userInfo) {
// 更新本地用户信息
self.userInfo.avatar = avatarUrl;
// 更新 UI 显示
[self updateHeaderWithUserInfo:self.userInfo];
NSLog(@"[EPMineViewController] 头像已更新: %@", avatarUrl);
}
}
- (void)dealloc {
// 只移除头像更新通知的观察者,设置按钮现在使用 block 回调
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"EPEditSettingAvatarUpdated" object:nil];
}
@end

View File

@@ -1,40 +0,0 @@
//
// EPMineAPIHelper.h
// YuMi
//
// Created by AI on 2025-10-10.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@class UserInfoModel;
/// 封装用户信息相关 API
@interface EPMineAPIHelper : NSObject
/// 获取用户基础信息
- (void)getUserInfoWithUid:(NSString *)uid
completion:(void (^)(UserInfoModel * _Nullable userInfo))completion
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure;
/// 获取用户详细信息(包含 dynamicInfo
- (void)getUserDetailInfoWithUid:(NSString *)uid
completion:(void (^)(UserInfoModel * _Nullable userInfo))completion
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure;
/// 更新用户头像
- (void)updateAvatarWithUrl:(NSString *)avatarUrl
completion:(void (^)(void))completion
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure;
/// 更新用户昵称
- (void)updateNicknameWithNick:(NSString *)nickname
completion:(void (^)(void))completion
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,77 +0,0 @@
//
// EPMineAPIHelper.m
// YuMi
//
// Created by AI on 2025-10-10.
//
#import "EPMineAPIHelper.h"
#import "Api+Mine.h"
#import "UserInfoModel.h"
#import "BaseModel.h"
#import "AccountInfoStorage.h"
@implementation EPMineAPIHelper
- (void)getUserInfoWithUid:(NSString *)uid
completion:(void (^)(UserInfoModel * _Nullable userInfo))completion
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure {
[Api getUserInfo:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
if (code == 200 && data.data) {
UserInfoModel *userInfo = [UserInfoModel modelWithDictionary:data.data];
if (completion) completion(userInfo);
} else {
if (failure) failure(code, msg);
}
} uid:uid];
}
- (void)getUserDetailInfoWithUid:(NSString *)uid
completion:(void (^)(UserInfoModel * _Nullable userInfo))completion
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure {
[Api userDetailInfoCompletion:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
if (code == 200 && data.data) {
UserInfoModel *userInfo = [UserInfoModel modelWithDictionary:data.data];
if (completion) completion(userInfo);
} else {
if (failure) failure(code, msg);
}
} uid:uid page:@"1" pageSize:@"20"];
}
- (void)updateAvatarWithUrl:(NSString *)avatarUrl
completion:(void (^)(void))completion
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure {
[Api userV2UploadAvatar:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
if (code == 200) {
if (completion) completion();
} else {
if (failure) failure(code, msg);
}
} avatarUrl:avatarUrl needPay:@NO];
}
- (void)updateNicknameWithNick:(NSString *)nickname
completion:(void (^)(void))completion
failure:(void (^)(NSInteger code, NSString * _Nullable msg))failure {
NSString *uid = [[AccountInfoStorage instance] getUid];
NSString *ticket = [[AccountInfoStorage instance] getTicket];
NSMutableDictionary *params = [NSMutableDictionary dictionary];
if (nickname.length > 0) {
[params setValue:nickname forKey:@"nick"];
}
[params setObject:uid forKey:@"uid"];
[params setObject:ticket forKey:@"ticket"];
[Api completeUserInfo:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
if (code == 200) {
if (completion) completion();
} else {
if (failure) failure(code, msg);
}
} userInfo:params];
}
@end

View File

@@ -1,26 +0,0 @@
//
// EPMineHeaderView.h
// YuMi
//
// Created by AI on 2025-10-09.
// Copyright © 2025 YuMi. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/// EP 系列个人主页头部视图
/// 大圆形头像 + 渐变背景 + 用户信息展示
@interface EPMineHeaderView : UIView
/// 设置按钮点击回调
@property (nonatomic, copy, nullable) void(^onSettingsButtonTapped)(void);
/// 更新用户信息
/// @param userInfoDict 用户信息字典
- (void)updateWithUserInfo:(NSDictionary *)userInfoDict;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,242 +0,0 @@
//
// EPMineHeaderView.m
// YuMi
//
// Created by AI on 2025-10-09.
// Copyright © 2025 YuMi. All rights reserved.
//
#import "EPMineHeaderView.h"
#import <Masonry/Masonry.h>
#import <SDWebImage/SDWebImage.h>
#import "EPEmotionColorStorage.h"
@interface EPMineHeaderView ()
/// 头像视图
@property (nonatomic, strong) UIImageView *avatarImageView;
/// 呼吸光晕层
@property (nonatomic, strong) CALayer *glowLayer;
/// 昵称标签
@property (nonatomic, strong) UILabel *nicknameLabel;
/// ID 标签
@property (nonatomic, strong) UILabel *idLabel;
/// 设置按钮
@property (nonatomic, strong) UIButton *settingsButton;
@end
@implementation EPMineHeaderView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self setupUI];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
// 更新光晕层 frame跟随头像位置
if (self.glowLayer) {
self.glowLayer.frame = CGRectInset(self.avatarImageView.frame, -8, -8);
}
}
- (void)setupUI {
// 大圆形头像
self.avatarImageView = [[UIImageView alloc] init];
self.avatarImageView.layer.cornerRadius = 60;
self.avatarImageView.layer.masksToBounds = NO; // 改为 NO 以显示阴影
self.avatarImageView.layer.borderWidth = 0; // 移除边框
self.avatarImageView.backgroundColor = [UIColor whiteColor];
self.avatarImageView.contentMode = UIViewContentModeScaleAspectFill;
// 为了同时显示圆角和阴影,需要设置 clipsToBounds
self.avatarImageView.clipsToBounds = YES;
[self addSubview:self.avatarImageView];
[self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self);
make.top.equalTo(self).offset(60);
make.size.mas_equalTo(CGSizeMake(120, 120));
}];
// 昵称
self.nicknameLabel = [[UILabel alloc] init];
self.nicknameLabel.font = [UIFont boldSystemFontOfSize:24];
self.nicknameLabel.textColor = [UIColor whiteColor];
self.nicknameLabel.textAlignment = NSTextAlignmentCenter;
[self addSubview:self.nicknameLabel];
[self.nicknameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self);
make.top.equalTo(self.avatarImageView.mas_bottom).offset(16);
}];
// ID
self.idLabel = [[UILabel alloc] init];
self.idLabel.font = [UIFont systemFontOfSize:14];
self.idLabel.textColor = [UIColor whiteColor];
self.idLabel.alpha = 0.8;
self.idLabel.textAlignment = NSTextAlignmentCenter;
[self addSubview:self.idLabel];
[self.idLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self);
make.top.equalTo(self.nicknameLabel.mas_bottom).offset(8);
}];
// 设置按钮(右上角)
self.settingsButton = [UIButton buttonWithType:UIButtonTypeCustom];
[self.settingsButton setImage:[UIImage systemImageNamed:@"gearshape"] forState:UIControlStateNormal];
self.settingsButton.tintColor = [UIColor whiteColor];
self.settingsButton.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.2];
self.settingsButton.layer.cornerRadius = 20;
[self.settingsButton addTarget:self action:@selector(settingsButtonTapped) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:self.settingsButton];
[self.settingsButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self).offset(50);
make.trailing.equalTo(self).offset(-20);
make.size.mas_equalTo(CGSizeMake(40, 40));
}];
}
- (void)updateWithUserInfo:(NSDictionary *)userInfoDict {
// 更新昵称
NSString *nickname = userInfoDict[@"nickname"] ?: YMLocalizedString(@"user.nickname_not_set");
self.nicknameLabel.text = nickname;
// 更新 ID
NSString *uid = userInfoDict[@"uid"] ?: @"";
self.idLabel.text = [NSString stringWithFormat:@"ID:%@", uid];
// 加载头像
NSString *avatarURL = userInfoDict[@"avatar"];
if (avatarURL && avatarURL.length > 0) {
[self.avatarImageView sd_setImageWithURL:[NSURL URLWithString:avatarURL]
placeholderImage:[UIImage imageNamed:@"default_avatar"]];
} else {
// 使用默认头像
self.avatarImageView.image = [UIImage imageNamed:@"default_avatar"];
}
// 应用用户专属情绪颜色
[self applyUserSignatureColor];
}
/// 应用用户专属情绪颜色到头像边框和阴影
- (void)applyUserSignatureColor {
NSString *signatureColor = [EPEmotionColorStorage userSignatureColor];
if (signatureColor) {
// 有专属颜色,使用该颜色
UIColor *color = [self colorFromHex:signatureColor];
// 取消边框
self.avatarImageView.layer.borderWidth = 0;
// 设置阴影(使用情绪颜色)
self.avatarImageView.layer.shadowColor = color.CGColor;
self.avatarImageView.layer.shadowOffset = CGSizeMake(0, 4);
self.avatarImageView.layer.shadowOpacity = 0.6;
self.avatarImageView.layer.shadowRadius = 12;
NSLog(@"[EPMineHeaderView] 应用专属颜色: %@", signatureColor);
// 应用呼吸光晕动效 ⭐
[self applyBreathingGlow];
} else {
// 没有专属颜色,保持无边框
self.avatarImageView.layer.borderWidth = 0;
// 默认轻微阴影
self.avatarImageView.layer.shadowColor = [UIColor blackColor].CGColor;
self.avatarImageView.layer.shadowOffset = CGSizeMake(0, 2);
self.avatarImageView.layer.shadowOpacity = 0.2;
self.avatarImageView.layer.shadowRadius = 8;
// 移除光晕层
if (self.glowLayer) {
[self.glowLayer removeFromSuperlayer];
self.glowLayer = nil;
}
}
}
/// 应用呼吸光晕动效
- (void)applyBreathingGlow {
NSString *signatureColor = [EPEmotionColorStorage userSignatureColor];
if (!signatureColor) return;
UIColor *color = [self colorFromHex:signatureColor];
// 创建光晕层(如果不存在)
if (!self.glowLayer) {
self.glowLayer = [CALayer layer];
self.glowLayer.frame = CGRectInset(self.avatarImageView.frame, -8, -8); // 比头像大 16pt
self.glowLayer.cornerRadius = 68; // 头像 60 + 扩展 8
self.glowLayer.backgroundColor = [color colorWithAlphaComponent:0.75].CGColor; // 大幅加深
// 插入到头像 layer 下方
[self.layer insertSublayer:self.glowLayer below:self.avatarImageView.layer];
} else {
// 更新颜色
self.glowLayer.backgroundColor = [color colorWithAlphaComponent:0.75].CGColor; // 大幅加深
}
// 移除旧动画
[self.glowLayer removeAllAnimations];
// 创建呼吸动画组
CAAnimationGroup *breathingGroup = [CAAnimationGroup animation];
breathingGroup.duration = 1.8; // 加速
breathingGroup.repeatCount = HUGE_VALF; // 无限循环
breathingGroup.autoreverses = YES;
breathingGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
// 动画 1透明度变化呼吸亮度
CABasicAnimation *opacityAnim = [CABasicAnimation animationWithKeyPath:@"opacity"];
opacityAnim.fromValue = @(0.65);
opacityAnim.toValue = @(1.0); // 接近完全不透明,颜色饱和
// 动画 2缩放变化呼吸扩散
CABasicAnimation *scaleAnim = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
scaleAnim.fromValue = @(1.0);
scaleAnim.toValue = @(1.1);
breathingGroup.animations = @[opacityAnim, scaleAnim];
[self.glowLayer addAnimation:breathingGroup forKey:@"breathing"];
NSLog(@"[EPMineHeaderView] 启动呼吸光晕动效");
}
/// Hex 转 UIColor
- (UIColor *)colorFromHex:(NSString *)hexString {
unsigned rgbValue = 0;
NSScanner *scanner = [NSScanner scannerWithString:hexString];
[scanner setScanLocation:1]; // 跳过 #
[scanner scanHexInt:&rgbValue];
return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0
green:((rgbValue & 0xFF00) >> 8)/255.0
blue:(rgbValue & 0xFF)/255.0
alpha:1.0];
}
- (void)settingsButtonTapped {
NSLog(@"[EPMineHeaderView] 设置按钮点击");
// 使用 block 回调
if (self.onSettingsButtonTapped) {
self.onSettingsButtonTapped();
}
}
@end

View File

@@ -1,22 +0,0 @@
//
// EPMomentPublishViewController.h
// YuMi
//
// Created by AI on 2025-10-10.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/// 发布成功通知
extern NSString *const EPMomentPublishSuccessNotification;
/// EP 版:图文发布页面
@interface EPMomentPublishViewController : UIViewController
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,442 +0,0 @@
//
// EPMomentPublishViewController.m
// YuMi
//
// Created by AI on 2025-10-10.
//
// NOTE: 话题选择功能未实现
// 旧版本 XPMonentsPublishViewController 包含话题选择 UI (addTopicView)
// 但实际业务中话题功能使用率低,新版本暂不实现
// 如需实现参考: YuMi/Modules/YMMonents/View/XPMonentsPublishTopicView
#import "EPMomentPublishViewController.h"
#import <Masonry/Masonry.h>
#import <TZImagePickerController/TZImagePickerController.h>
#import "DJDKMIMOMColor.h"
#import "SZTextView.h"
#import "YuMi-Swift.h"
#import "EPEmotionColorPicker.h"
#import "EPEmotionColorStorage.h"
#import "UIView+GradientLayer.h"
// 发布成功通知
NSString *const EPMomentPublishSuccessNotification = @"EPMomentPublishSuccessNotification";
@interface EPMomentPublishViewController () <UICollectionViewDataSource, UICollectionViewDelegate, TZImagePickerControllerDelegate, UITextViewDelegate>
@property (nonatomic, strong) UIView *navView;
@property (nonatomic, strong) UIButton *backButton;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UIButton *publishButton;
@property (nonatomic, strong) UIView *contentView;
@property (nonatomic, strong) SZTextView *textView;
@property (nonatomic, strong) UILabel *limitLabel;
@property (nonatomic, strong) UIView *lineView;
@property (nonatomic, strong) UIButton *emotionButton;
@property (nonatomic, strong) UICollectionView *collectionView;
@property (nonatomic, strong) NSMutableArray<UIImage *> *images;
@property (nonatomic, strong) NSMutableArray *selectedAssets; // TZImagePicker 已选资源
@property (nonatomic, copy) NSString *selectedEmotionColor; // 选中的情绪颜色
@property (nonatomic, assign) BOOL hasAddedGradient; // 标记是否已添加渐变背景
@end
@implementation EPMomentPublishViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor colorWithRed:0x0C/255.0 green:0x05/255.0 blue:0x27/255.0 alpha:1.0];
[self setupUI];
// 自动加载用户专属颜色
[self loadUserSignatureColor];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
// 添加渐变背景到发布按钮(只添加一次)
if (!self.hasAddedGradient && self.publishButton.bounds.size.width > 0) {
// 使用与登录页面相同的渐变颜色EPLoginConfig.Colors
// gradientStart: #F854FC, gradientEnd: #500FFF
[self.publishButton addGradientBackgroundWithColors:@[
[UIColor colorWithRed:0xF8/255.0 green:0x54/255.0 blue:0xFC/255.0 alpha:1.0], // #F854FC
[UIColor colorWithRed:0x50/255.0 green:0x0F/255.0 blue:0xFF/255.0 alpha:1.0] // #500FFF
] startPoint:CGPointMake(0, 0.5) endPoint:CGPointMake(1, 0.5) cornerRadius:25];
self.hasAddedGradient = YES;
}
}
/// 加载用户专属颜色作为默认选中
- (void)loadUserSignatureColor {
NSString *signatureColor = [EPEmotionColorStorage userSignatureColor];
if (signatureColor) {
self.selectedEmotionColor = signatureColor;
[self updateEmotionButtonAppearance];
NSLog(@"[Publish] 自动选中专属颜色: %@", signatureColor);
}
}
- (void)setupUI {
[self.view addSubview:self.navView];
[self.view addSubview:self.contentView];
[self.navView addSubview:self.backButton];
[self.navView addSubview:self.titleLabel];
// 发布按钮移到底部
[self.contentView addSubview:self.textView];
[self.contentView addSubview:self.limitLabel];
[self.contentView addSubview:self.lineView];
[self.contentView addSubview:self.emotionButton];
[self.contentView addSubview:self.collectionView];
[self.contentView addSubview:self.publishButton];
[self.navView mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.trailing.top.equalTo(self.view);
make.height.mas_equalTo(kNavigationHeight);
}];
[self.backButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.equalTo(self.view).offset(10);
make.top.mas_equalTo(statusbarHeight);
make.size.mas_equalTo(CGSizeMake(44, 44));
}];
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.navView);
make.centerY.equalTo(self.backButton);
}];
// 发布按钮约束移到底部
[self.contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.trailing.equalTo(self.view);
make.top.equalTo(self.navView.mas_bottom);
make.bottom.equalTo(self.view);
}];
[self.textView mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.trailing.equalTo(self.contentView).inset(15);
make.top.equalTo(self.contentView).offset(10);
make.height.mas_equalTo(150);
}];
[self.limitLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.textView.mas_bottom).offset(5);
make.trailing.equalTo(self.textView);
}];
[self.lineView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.limitLabel.mas_bottom).offset(10);
make.leading.trailing.equalTo(self.textView);
make.height.mas_equalTo(1);
}];
// 情绪按钮
[self.emotionButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.trailing.equalTo(self.contentView).inset(15);
make.top.equalTo(self.lineView.mas_bottom).offset(10);
make.height.mas_equalTo(44);
}];
// 计算显示3行图片所需的高度
// itemW = (屏幕宽度 - 左右边距30 - 列间距20) / 3
// 总高度 = 3行itemW + 2个行间距(10*2)
CGFloat itemW = (KScreenWidth - 15*2 - 10*2)/3.0;
CGFloat collectionHeight = itemW * 3 + 10 * 2;
[self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.trailing.equalTo(self.contentView).inset(15);
make.top.equalTo(self.emotionButton.mas_bottom).offset(10);
make.height.mas_equalTo(collectionHeight);
}];
// 底部发布按钮
[self.publishButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.trailing.equalTo(self.view).inset(20);
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-20);
make.height.mas_equalTo(50);
}];
}
#pragma mark - Actions
- (void)onBack {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)onEmotionButtonTapped {
EPEmotionColorPicker *picker = [[EPEmotionColorPicker alloc] init];
// 预选中当前颜色(如果有)
picker.preselectedColor = self.selectedEmotionColor;
__weak typeof(self) weakSelf = self;
picker.onColorSelected = ^(NSString *hexColor) {
__strong typeof(weakSelf) self = weakSelf;
self.selectedEmotionColor = hexColor;
[self updateEmotionButtonAppearance];
};
[picker showInView:self.view];
}
- (void)updateEmotionButtonAppearance {
if (self.selectedEmotionColor) {
// 显示选中的颜色
UIColor *color = [self colorFromHex:self.selectedEmotionColor];
// 创建色块视图
UIView *colorDot = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)];
colorDot.backgroundColor = color;
colorDot.layer.cornerRadius = 10;
colorDot.layer.masksToBounds = YES;
colorDot.layer.borderWidth = 2;
colorDot.layer.borderColor = [UIColor whiteColor].CGColor;
// 转换为 UIImage
UIGraphicsBeginImageContextWithOptions(colorDot.bounds.size, NO, 0);
[colorDot.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *colorDotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[self.emotionButton setImage:colorDotImage forState:UIControlStateNormal];
// 获取情绪名称
NSString *emotionName = [EPEmotionColorStorage emotionNameForColor:self.selectedEmotionColor];
NSString *title = emotionName
? [NSString stringWithFormat:@" Selected Emotion: %@", emotionName]
: @" Emotion Selected";
[self.emotionButton setTitle:title forState:UIControlStateNormal];
} else {
[self.emotionButton setImage:nil forState:UIControlStateNormal];
[self.emotionButton setTitle:@"🎨 Add Emotion" forState:UIControlStateNormal];
}
}
- (UIColor *)colorFromHex:(NSString *)hexString {
unsigned rgbValue = 0;
NSScanner *scanner = [NSScanner scannerWithString:hexString];
[scanner setScanLocation:1]; // 跳过 #
[scanner scanHexInt:&rgbValue];
return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0
green:((rgbValue & 0xFF00) >> 8)/255.0
blue:(rgbValue & 0xFF)/255.0
alpha:1.0];
}
- (void)onPublish {
[self.view endEditing:YES];
// 验证:文本或图片至少有一项
if (self.textView.text.length == 0 && self.images.count == 0) {
[EPProgressHUD showError:YMLocalizedString(@"publish.content_or_image_required")];
return;
}
// 创建 Swift API Helper
EPMomentAPISwiftHelper *apiHelper = [[EPMomentAPISwiftHelper alloc] init];
// 保存情绪颜色用于发布后关联
NSString *emotionColorToSave = self.selectedEmotionColor;
if (self.images.count > 0) {
// 有图片:上传后发布(统一入口)
[[EPSDKManager shared] uploadImages:self.images
progress:^(NSInteger uploaded, NSInteger total) {
[EPProgressHUD showProgress:uploaded total:total];
}
success:^(NSArray<NSDictionary *> *resList) {
[EPProgressHUD dismiss];
[apiHelper publishMomentWithType:@"2"
content:self.textView.text ?: @""
resList:resList
completion:^{
// 保存临时情绪颜色(等待列表刷新后匹配)
if (emotionColorToSave) {
[self savePendingEmotionColor:emotionColorToSave];
}
// 发送发布成功通知
[[NSNotificationCenter defaultCenter] postNotificationName:EPMomentPublishSuccessNotification object:nil];
[self dismissViewControllerAnimated:YES completion:nil];
} failure:^(NSInteger code, NSString *msg) {
// TODO: 显示错误 Toast
NSLog(@"发布失败: %ld - %@", (long)code, msg);
}];
}
failure:^(NSString *errorMsg) {
[EPProgressHUD dismiss];
// TODO: 显示错误 Toast
NSLog(@"上传失败: %@", errorMsg);
}];
} else {
// 纯文本:直接发布
[apiHelper publishMomentWithType:@"0"
content:self.textView.text
resList:@[]
completion:^{
// 保存临时情绪颜色(等待列表刷新后匹配)
if (emotionColorToSave) {
[self savePendingEmotionColor:emotionColorToSave];
}
// 发送发布成功通知
[[NSNotificationCenter defaultCenter] postNotificationName:EPMomentPublishSuccessNotification object:nil];
[self dismissViewControllerAnimated:YES completion:nil];
} failure:^(NSInteger code, NSString *msg) {
// TODO: 显示错误 Toast
NSLog(@"发布失败: %ld - %@", (long)code, msg);
}];
}
}
/// 保存待处理的情绪颜色(临时存储,供列表刷新后匹配)
- (void)savePendingEmotionColor:(NSString *)color {
[[NSUserDefaults standardUserDefaults] setObject:color forKey:@"EP_Pending_Emotion_Color"];
[[NSUserDefaults standardUserDefaults] setObject:@([[NSDate date] timeIntervalSince1970]) forKey:@"EP_Pending_Emotion_Timestamp"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
#pragma mark - UICollectionView
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.images.count + 1; // 最后一个是添加按钮
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"ep.publish.cell" forIndexPath:indexPath];
cell.contentView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.06];
cell.contentView.layer.cornerRadius = 12;
// 清空复用子视图,避免加号被覆盖
for (UIView *sub in cell.contentView.subviews) { [sub removeFromSuperview]; }
BOOL showAdd = (self.images.count < 9) && (indexPath.item == self.images.count);
if (showAdd) {
UIImageView *iv = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"icon_moment_addphoto"]];
iv.contentMode = UIViewContentModeScaleAspectFill;
iv.clipsToBounds = YES;
[cell.contentView addSubview:iv];
[iv mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(cell.contentView); }];
} else {
UIImageView *iv = [[UIImageView alloc] init];
iv.contentMode = UIViewContentModeScaleAspectFill;
iv.layer.masksToBounds = YES;
[cell.contentView addSubview:iv];
[iv mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(cell.contentView); }];
NSInteger idx = MIN(indexPath.item, (NSInteger)self.images.count - 1);
if (idx >= 0 && idx < self.images.count) iv.image = self.images[idx];
}
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.item == self.images.count) {
TZImagePickerController *picker = [[TZImagePickerController alloc] initWithMaxImagesCount:9 delegate:self];
picker.allowPickingVideo = NO;
picker.allowTakeVideo = NO;
picker.selectedAssets = self.selectedAssets; // 预选
picker.maxImagesCount = 9; // 总上限
[self presentViewController:picker animated:YES completion:nil];
}
}
#pragma mark - TZImagePickerControllerDelegate
- (void)imagePickerController:(TZImagePickerController *)picker didFinishPickingPhotos:(NSArray<UIImage *> *)photos sourceAssets:(NSArray *)assets isSelectOriginalPhoto:(BOOL)isSelectOriginalPhoto infos:(NSArray<NSDictionary *> *)infos {
// 合并选择:在已有基础上追加,最多 9 张
for (NSInteger i = 0; i < assets.count; i++) {
id asset = assets[i];
UIImage *img = [photos xpSafeObjectAtIndex:i] ?: photos[i];
if (![self.selectedAssets containsObject:asset] && self.images.count < 9) {
[self.selectedAssets addObject:asset];
[self.images addObject:img];
}
}
[self.collectionView reloadData];
}
#pragma mark - UITextViewDelegate
- (void)textViewDidChange:(UITextView *)textView {
if (textView.text.length > 500) {
textView.text = [textView.text substringToIndex:500];
}
self.limitLabel.text = [NSString stringWithFormat:@"%lu/500", (unsigned long)textView.text.length];
}
#pragma mark - Lazy
- (UIView *)navView { if (!_navView) { _navView = [UIView new]; _navView.backgroundColor = [UIColor clearColor]; } return _navView; }
- (UIButton *)backButton {
if (!_backButton) {
_backButton = [UIButton buttonWithType:UIButtonTypeCustom];
// 使用系统返回图标
UIImage *backImage = [UIImage systemImageNamed:@"chevron.left"];
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:20 weight:UIImageSymbolWeightMedium];
backImage = [backImage imageByApplyingSymbolConfiguration:config];
[_backButton setImage:backImage forState:UIControlStateNormal];
[_backButton setTintColor:[UIColor whiteColor]]; // 白色适配深色背景
[_backButton addTarget:self action:@selector(onBack) forControlEvents:UIControlEventTouchUpInside];
}
return _backButton;
}
- (UILabel *)titleLabel {
if (!_titleLabel) {
_titleLabel = [UILabel new];
_titleLabel.text = YMLocalizedString(@"publish.title");
_titleLabel.textColor = [UIColor whiteColor]; // 白色适配深色背景
_titleLabel.font = [UIFont systemFontOfSize:17];
}
return _titleLabel;
}
- (UIButton *)publishButton {
if (!_publishButton) {
_publishButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_publishButton setTitle:YMLocalizedString(@"common.publish") forState:UIControlStateNormal];
[_publishButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
_publishButton.titleLabel.font = [UIFont systemFontOfSize:17 weight:UIFontWeightMedium];
_publishButton.layer.cornerRadius = 25;
_publishButton.layer.masksToBounds = NO; // 改为 NO 以便渐变层正常显示
// 渐变背景将在 viewDidLayoutSubviews 中添加(与登录页面统一)
[_publishButton addTarget:self action:@selector(onPublish) forControlEvents:UIControlEventTouchUpInside];
}
return _publishButton;
}
- (UIView *)contentView { if (!_contentView) { _contentView = [UIView new]; _contentView.backgroundColor = [UIColor clearColor]; } return _contentView; }
- (SZTextView *)textView {
if (!_textView) {
_textView = [SZTextView new];
_textView.placeholder = @"Enter Content";
_textView.textColor = [UIColor whiteColor]; // 白色文本适配深色背景
_textView.placeholderTextColor = [[UIColor whiteColor] colorWithAlphaComponent:0.4]; // 半透明白色占位符
_textView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.08]; // 轻微背景色
_textView.layer.cornerRadius = 12;
_textView.layer.masksToBounds = YES;
_textView.font = [UIFont systemFontOfSize:15];
_textView.delegate = self;
}
return _textView;
}
- (UILabel *)limitLabel {
if (!_limitLabel) {
_limitLabel = [UILabel new];
_limitLabel.text = @"0/500";
_limitLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.6]; // 浅色适配深色背景
_limitLabel.font = [UIFont systemFontOfSize:12];
}
return _limitLabel;
}
- (UIView *)lineView { if (!_lineView) { _lineView = [UIView new]; _lineView.backgroundColor = [DJDKMIMOMColor dividerColor]; } return _lineView; }
- (UIButton *)emotionButton {
if (!_emotionButton) {
_emotionButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_emotionButton setTitle:@"🎨 Add Emotion" forState:UIControlStateNormal];
[_emotionButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; // 白色文本
_emotionButton.titleLabel.font = [UIFont systemFontOfSize:15];
_emotionButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
_emotionButton.contentEdgeInsets = UIEdgeInsetsMake(0, 15, 0, 0);
_emotionButton.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.08]; // 稍微提亮背景
_emotionButton.layer.cornerRadius = 8;
_emotionButton.layer.masksToBounds = YES;
[_emotionButton addTarget:self action:@selector(onEmotionButtonTapped) forControlEvents:UIControlEventTouchUpInside];
}
return _emotionButton;
}
- (UICollectionView *)collectionView { if (!_collectionView) { UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; layout.minimumLineSpacing = 10; layout.minimumInteritemSpacing = 10; CGFloat itemW = (KScreenWidth - 15*2 - 10*2)/3.0; layout.itemSize = CGSizeMake(itemW, itemW); _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; _collectionView.delegate = self; _collectionView.dataSource = self; _collectionView.backgroundColor = [UIColor clearColor]; [_collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"ep.publish.cell"]; } return _collectionView; }
- (NSMutableArray<UIImage *> *)images { if (!_images) { _images = [NSMutableArray array]; } return _images; }
- (NSMutableArray *)selectedAssets { if (!_selectedAssets) { _selectedAssets = [NSMutableArray array]; } return _selectedAssets; }
@end

View File

@@ -1,20 +0,0 @@
//
// EPMomentViewController.h
// YuMi
//
// Created by AI on 2025-10-09.
// Copyright © 2025 YuMi. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/// 新的动态页面控制器
/// 采用卡片式布局,完全不同于原 XPMomentsViewController
/// 注意:直接继承 UIViewController不继承 BaseViewController避免依赖链
@interface EPMomentViewController : UIViewController
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,195 +0,0 @@
//
// EPMomentViewController.m
// YuMi
//
// Created by AI on 2025-10-09.
// Copyright © 2025 YuMi. All rights reserved.
//
#import "EPMomentViewController.h"
#import <UIKit/UIKit.h>
#import <Masonry/Masonry.h>
#import "EPMomentCell.h"
#import "EPMomentListView.h"
#import "EPMomentPublishViewController.h"
#import "YUMIMacroUitls.h"
@interface EPMomentViewController ()
// MARK: - UI Components
/// 列表视图MVVMView
@property (nonatomic, strong) EPMomentListView *listView;
/// 顶部图标
@property (nonatomic, strong) UIImageView *topIconImageView;
/// 顶部固定文案
@property (nonatomic, strong) UILabel *topTipLabel;
@end
@implementation EPMomentViewController
// MARK: - Lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"Enjoy your Life Time";
// 设置 title 为白色
[self.navigationController.navigationBar setTitleTextAttributes:@{
NSForegroundColorAttributeName: [UIColor whiteColor]
}];
[self setupUI];
[self.listView reloadFirstPage];
// 监听发布成功通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onMomentPublishSuccess:)
name:EPMomentPublishSuccessNotification
object:nil];
// ✅ 新增:冷启动时延迟检查数据,如果没有数据则自动刷新一次
[self scheduleAutoRefreshIfNeeded];
NSLog(@"[EPMomentViewController] 页面加载完成");
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
}
// MARK: - Setup UI
- (void)setupUI {
UIImageView *bgImageView = [[UIImageView alloc] initWithImage:kImage(@"vc_bg")];
bgImageView.contentMode = UIViewContentModeScaleAspectFill;
bgImageView.clipsToBounds = YES;
[self.view addSubview:bgImageView];
[bgImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.view);
}];
// 顶部图标
[self.view addSubview:self.topIconImageView];
[self.topIconImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view);
make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop).offset(14);
make.size.mas_equalTo(CGSizeMake(56, 41));
}];
// 顶部固定文案
[self.view addSubview:self.topTipLabel];
[self.topTipLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.topIconImageView.mas_bottom).offset(14);
make.leading.trailing.equalTo(self.view).inset(20);
}];
// 列表视图
[self.view addSubview:self.listView];
[self.listView mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.trailing.bottom.equalTo(self.view);
make.top.equalTo(self.topTipLabel.mas_bottom).offset(8);
}];
// 右上角发布按钮
UIImage *addIcon = [UIImage imageNamed:@"icon_moment_add"];
UIButton *publishButton = [UIButton buttonWithType:UIButtonTypeCustom];
publishButton.contentMode = UIViewContentModeScaleAspectFit;
[publishButton setImage:addIcon forState:UIControlStateNormal];
publishButton.frame = CGRectMake(0, 0, 40, 40);
[publishButton addTarget:self action:@selector(onPublishButtonTapped) forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem *publishItem = [[UIBarButtonItem alloc] initWithCustomView:publishButton];
self.navigationItem.rightBarButtonItem = publishItem;
NSLog(@"[EPMomentViewController] UI 设置完成");
}
// 不再在 VC 内部直接发请求/维护分页
// MARK: - Auto Refresh
/// 延迟检查数据,如果没有数据则自动刷新(解决冷启动数据未加载问题)
- (void)scheduleAutoRefreshIfNeeded {
__weak typeof(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) self = weakSelf;
if (!self) return;
// 检查是否有数据
if (self.listView.rawList.count == 0) {
NSLog(@"[EPMomentViewController] ⚠️ 冷启动 1 秒后检测到无数据,自动刷新一次");
[self.listView reloadFirstPage];
} else {
NSLog(@"[EPMomentViewController] ✅ 冷启动 1 秒后检测到已有 %lu 条数据,无需刷新", (unsigned long)self.listView.rawList.count);
}
});
}
// MARK: - Actions
- (void)onPublishButtonTapped {
NSLog(@"[EPMomentViewController] 发布按钮点击");
EPMomentPublishViewController *vc = [[EPMomentPublishViewController alloc] init];
vc.modalPresentationStyle = UIModalPresentationFullScreen;
[self.navigationController presentViewController:vc animated:YES completion:nil];
}
- (void)showAlertWithMessage:(NSString *)message {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:YMLocalizedString(@"common.tips")
message:message
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:YMLocalizedString(@"common.confirm") style:UIAlertActionStyleDefault handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}
- (void)onMomentPublishSuccess:(NSNotification *)notification {
NSLog(@"[EPMomentViewController] 收到发布成功通知,刷新列表");
[self.listView reloadFirstPage];
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
// 列表点击回调由 listView 暴露
// MARK: - Lazy Loading
- (EPMomentListView *)listView {
if (!_listView) {
_listView = [[EPMomentListView alloc] initWithFrame:CGRectZero];
// __weak typeof(self) weakSelf = self;
_listView.onSelectMoment = ^(NSInteger index) {
// __strong typeof(weakSelf) self = weakSelf;
// [self showAlertWithMessage:[NSString stringWithFormat:YMLocalizedString(@"moment.item_clicked"), (long)index]];
};
}
return _listView;
}
- (UIImageView *)topIconImageView {
if (!_topIconImageView) {
_topIconImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"icon_moment_Volume"]];
_topIconImageView.contentMode = UIViewContentModeScaleAspectFit;
}
return _topIconImageView;
}
- (UILabel *)topTipLabel {
if (!_topTipLabel) {
_topTipLabel = [UILabel new];
_topTipLabel.numberOfLines = 0;
_topTipLabel.textColor = [UIColor whiteColor];
_topTipLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightRegular];
_topTipLabel.text = @"In the quiet gallery of the heart, we learn to see the colors of emotion. And in the shared silence between souls, we begin to find the sound of resonance. This is more than an app—it's a space where your inner world is both a masterpiece and a melody.";
}
return _topTipLabel;
}
// 无数据源属性
@end

View File

@@ -1,58 +0,0 @@
//
// EPEmotionColorStorage.h
// YuMi
//
// Created by AI on 2025-10-14.
// 本地情绪颜色存储管理器(基于 UserDefaults
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EPEmotionColorStorage : NSObject
/// 保存动态的情绪颜色
/// @param hexColor Hex 格式颜色值,如 #FF0000
/// @param dynamicId 动态 ID
+ (void)saveColor:(NSString *)hexColor forDynamicId:(NSString *)dynamicId;
/// 获取动态关联的情绪颜色
/// @param dynamicId 动态 ID
/// @return Hex 格式颜色值,若未设置则返回 nil
+ (nullable NSString *)colorForDynamicId:(NSString *)dynamicId;
/// 删除动态的情绪颜色
/// @param dynamicId 动态 ID
+ (void)removeColorForDynamicId:(NSString *)dynamicId;
/// 获取所有预设情绪颜色6种基础情绪
/// @return Hex 颜色数组
+ (NSArray<NSString *> *)allEmotionColors;
/// 获取随机情绪颜色(不持久化)
+ (NSString *)randomEmotionColor;
/// 根据颜色值获取情绪名称
/// @param hexColor Hex 格式颜色值,如 #FFD700
/// @return 情绪名称(如 "Joy"),若未匹配返回 nil
+ (nullable NSString *)emotionNameForColor:(NSString *)hexColor;
#pragma mark - User Signature Color
/// 保存用户专属颜色
+ (void)saveUserSignatureColor:(NSString *)hexColor;
/// 获取用户专属颜色
+ (nullable NSString *)userSignatureColor;
/// 是否已设置专属颜色
+ (BOOL)hasUserSignatureColor;
/// 清除专属颜色(调试用)
+ (void)clearUserSignatureColor;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,118 +0,0 @@
//
// EPEmotionColorStorage.m
// YuMi
//
// Created by AI on 2025-10-14.
//
#import "EPEmotionColorStorage.h"
static NSString *const kEmotionColorStorageKey = @"EP_Emotion_Colors";
static NSString *const kUserSignatureColorKey = @"EP_User_Signature_Color";
static NSString *const kUserSignatureTimestampKey = @"EP_User_Signature_Timestamp";
@implementation EPEmotionColorStorage
#pragma mark - Public Methods
+ (void)saveColor:(NSString *)hexColor forDynamicId:(NSString *)dynamicId {
if (!hexColor || !dynamicId) return;
NSMutableDictionary *colorDict = [[self loadColorDictionary] mutableCopy];
colorDict[dynamicId] = hexColor;
[[NSUserDefaults standardUserDefaults] setObject:colorDict forKey:kEmotionColorStorageKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
+ (NSString *)colorForDynamicId:(NSString *)dynamicId {
if (!dynamicId) return nil;
NSDictionary *colorDict = [self loadColorDictionary];
return colorDict[dynamicId];
}
+ (void)removeColorForDynamicId:(NSString *)dynamicId {
if (!dynamicId) return;
NSMutableDictionary *colorDict = [[self loadColorDictionary] mutableCopy];
[colorDict removeObjectForKey:dynamicId];
[[NSUserDefaults standardUserDefaults] setObject:colorDict forKey:kEmotionColorStorageKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
+ (NSArray<NSString *> *)allEmotionColors {
return @[
@"#FFD700", // 喜悦 Joy金黄- 降低亮度,更温暖
@"#4A90E2", // 悲伤 Sadness天蓝- 提高亮度,更柔和
@"#E74C3C", // 愤怒 Anger珊瑚红- 降低饱和度
@"#9B59B6", // 恐惧 Fear紫罗兰- 稍微提亮
@"#FF9A3D", // 惊讶 Surprise柔和橙- 略微调暗
@"#2ECC71", // 厌恶 Disgust翡翠绿- 大幅降低亮度
@"#3498DB", // 信任 Trust亮蓝- 清新明亮
@"#F39C12" // 期待 Anticipation琥珀色- 温暖期待
];
}
+ (NSString *)randomEmotionColor {
NSArray *colors = [self allEmotionColors];
uint32_t randomIndex = arc4random_uniform((uint32_t)colors.count);
return colors[randomIndex];
}
+ (NSString *)emotionNameForColor:(NSString *)hexColor {
if (!hexColor || hexColor.length == 0) return nil;
NSArray<NSString *> *colors = [self allEmotionColors];
NSArray<NSString *> *emotions = @[@"Joy", @"Sadness", @"Anger", @"Fear", @"Surprise", @"Disgust", @"Trust", @"Anticipation"];
// 大小写不敏感比较
NSString *upperHex = [hexColor uppercaseString];
for (NSInteger i = 0; i < colors.count; i++) {
if ([[colors[i] uppercaseString] isEqualToString:upperHex]) {
return emotions[i];
}
}
return nil;
}
#pragma mark - Private Methods
+ (NSDictionary *)loadColorDictionary {
NSDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:kEmotionColorStorageKey];
return dict ?: @{};
}
#pragma mark - User Signature Color
+ (void)saveUserSignatureColor:(NSString *)hexColor {
if (!hexColor) return;
[[NSUserDefaults standardUserDefaults] setObject:hexColor forKey:kUserSignatureColorKey];
[[NSUserDefaults standardUserDefaults] setObject:@([[NSDate date] timeIntervalSince1970])
forKey:kUserSignatureTimestampKey];
[[NSUserDefaults standardUserDefaults] synchronize];
NSLog(@"[EPEmotionColorStorage] 保存用户专属颜色: %@", hexColor);
}
+ (NSString *)userSignatureColor {
return [[NSUserDefaults standardUserDefaults] stringForKey:kUserSignatureColorKey];
}
+ (BOOL)hasUserSignatureColor {
return [self userSignatureColor] != nil;
}
+ (void)clearUserSignatureColor {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:kUserSignatureColorKey];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:kUserSignatureTimestampKey];
[[NSUserDefaults standardUserDefaults] synchronize];
NSLog(@"[EPEmotionColorStorage] 清除用户专属颜色");
}
@end

View File

@@ -1,112 +0,0 @@
//
// EPMomentAPISwiftHelper.swift
// YuMi
//
// Created by AI on 2025-10-11.
//
import Foundation
/// 动态 API 封装Swift 现代化版本)
/// 统一封装列表获取和发布功能,完全替代 OC 版本
@objc class EPMomentAPISwiftHelper: NSObject {
/// 拉取最新动态列表
/// - Parameters:
/// - nextID: 下一页 ID首次传空字符串
/// - completion: 成功回调 (动态列表, 下一页ID)
/// - failure: 失败回调 (错误码, 错误信息)
@objc func fetchLatestMomentsWithNextID(
_ nextID: String,
completion: @escaping ([MomentsInfoModel], String) -> Void,
failure: @escaping (Int, String) -> Void
) {
let pageSize = "20"
let types = "0,2" // 图片+文字
Api.momentsLatestList({ (data, code, msg) in
if code == 200, let dict = data?.data as? NSDictionary {
// 使用 MomentsListInfoModel 序列化响应数据(标准化方式)
// 参考: XPMomentsLatestPresenter.m line 25 / EPLoginService.swift line 34
// Swift 中使用 mj_object(withKeyValues:) 而不是 model(withJSON:)
if let listInfo = MomentsListInfoModel.mj_object(withKeyValues: dict) {
let dynamicList = listInfo.dynamicList
let nextDynamicId = listInfo.nextDynamicId
completion(dynamicList, nextDynamicId)
} else {
// 序列化失败时返回空数据
completion([], "")
}
} else {
failure(Int(code), msg ?? YMLocalizedString("error.request_failed"))
}
}, dynamicId: nextID, pageSize: pageSize, types: types)
}
/// 发布动态
/// - Parameters:
/// - type: "0"=纯文本, "2"=图片
/// - content: 文本内容
/// - resList: 图片信息数组
/// - completion: 成功回调
/// - failure: 失败回调 (错误码, 错误信息)
@objc func publishMoment(
type: String,
content: String,
resList: [[String: Any]],
completion: @escaping () -> Void,
failure: @escaping (Int, String) -> Void
) {
guard let uid = AccountInfoStorage.instance().getUid() else {
failure(-1, YMLocalizedString("error.not_logged_in"))
return
}
// worldId 传空字符串(话题功能不实现)
// NOTE: 旧版本 XPMonentsPublishViewController 包含话题选择功能
// 但实际业务中话题功能使用率低,新版本暂不实现
// 如需实现参考: YuMi/Modules/YMMonents/View/XPMonentsPublishTopicView
Api.momentsPublish({ (data, code, msg) in
if code == 200 {
completion()
} else {
failure(Int(code), msg ?? YMLocalizedString("error.publish_failed"))
}
}, uid: uid, type: type, worldId: "", content: content, resList: resList)
}
/// 点赞/取消点赞动态
/// - Parameters:
/// - dynamicId: 动态 ID
/// - isLike: true=点赞false=取消点赞
/// - likedUid: 动态发布者 UID
/// - worldId: 话题 ID
/// - completion: 成功回调
/// - failure: 失败回调 (错误码, 错误信息)
@objc func likeMoment(
dynamicId: String,
isLike: Bool,
likedUid: String,
worldId: Int,
completion: @escaping () -> Void,
failure: @escaping (Int, String) -> Void
) {
guard let uid = AccountInfoStorage.instance().getUid() else {
failure(-1, YMLocalizedString("error.not_logged_in"))
return
}
let status = isLike ? "1" : "0"
let worldIdStr = String(format: "%ld", worldId)
Api.momentsLike({ (data, code, msg) in
if code == 200 {
completion()
} else {
failure(Int(code), msg ?? YMLocalizedString("error.like_failed"))
}
}, dynamicId: dynamicId, uid: uid, status: status, likedUid: likedUid, worldId: worldIdStr)
}
}

View File

@@ -1,31 +0,0 @@
//
// EPEmotionColorPicker.h
// YuMi
//
// Created by AI on 2025-10-14.
// 情绪色轮选择器 - 环形布局
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface EPEmotionColorPicker : UIView
/// 颜色选择回调
@property (nonatomic, copy) void(^onColorSelected)(NSString *hexColor);
/// 预选中的颜色(用于标记默认选中状态)
@property (nonatomic, copy) NSString *preselectedColor;
/// 在指定视图中显示选择器
/// @param parentView 父视图(通常是 ViewController 的 view
- (void)showInView:(UIView *)parentView;
/// 关闭选择器
- (void)dismiss;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,302 +0,0 @@
//
// EPEmotionColorPicker.m
// YuMi
//
// Created by AI on 2025-10-14.
//
#import "EPEmotionColorPicker.h"
#import "EPEmotionColorWheelView.h"
#import "EPEmotionInfoView.h"
#import <Masonry/Masonry.h>
@interface EPEmotionColorPicker ()
@property (nonatomic, strong) UIView *backgroundMask;
@property (nonatomic, strong) UIView *containerView;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UIButton *infoButton;
@property (nonatomic, strong) UIView *selectedColorView;
@property (nonatomic, strong) UILabel *selectedColorLabel;
@property (nonatomic, strong) UIButton *okButton;
@property (nonatomic, strong) EPEmotionColorWheelView *colorWheelView;
@property (nonatomic, copy) NSString *currentSelectedColor;
@property (nonatomic, assign) NSInteger currentSelectedIndex;
@end
@implementation EPEmotionColorPicker
#pragma mark - Lifecycle
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self setupUI];
}
return self;
}
- (void)setupUI {
self.backgroundColor = [UIColor clearColor];
// 背景遮罩
[self addSubview:self.backgroundMask];
[self.backgroundMask mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
// 底部卡片容器
[self addSubview:self.containerView];
[self.containerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.trailing.bottom.equalTo(self);
make.height.mas_equalTo(450); // 增加高度以适应新布局
}];
// 标题
[self.containerView addSubview:self.titleLabel];
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.containerView).offset(20);
make.centerX.equalTo(self.containerView);
}];
// Info 按钮(左上角)
[self.containerView addSubview:self.infoButton];
[self.infoButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.equalTo(self.containerView).offset(16);
make.centerY.equalTo(self.titleLabel);
make.size.mas_equalTo(CGSizeMake(28, 28));
}];
// OK 按钮(右上角)
[self.containerView addSubview:self.okButton];
[self.okButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.trailing.equalTo(self.containerView).offset(-16);
make.centerY.equalTo(self.titleLabel);
make.size.mas_equalTo(CGSizeMake(60, 32));
}];
// 选中状态显示区域
[self.containerView addSubview:self.selectedColorView];
[self.selectedColorView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.titleLabel.mas_bottom).offset(20);
make.centerX.equalTo(self.containerView);
make.height.mas_equalTo(50);
make.leading.trailing.equalTo(self.containerView).inset(20);
}];
// 色轮视图(使用共享组件)
[self.containerView addSubview:self.colorWheelView];
[self.colorWheelView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.containerView);
make.top.equalTo(self.selectedColorView.mas_bottom).offset(20);
make.size.mas_equalTo(CGSizeMake(280, 280)); // 调整尺寸
}];
}
#pragma mark - Actions
- (void)onBackgroundTapped {
[self dismiss];
}
- (void)onInfoButtonTapped {
EPEmotionInfoView *infoView = [[EPEmotionInfoView alloc] init];
[infoView showInView:self];
}
- (void)onOkButtonTapped {
if (self.currentSelectedColor && self.onColorSelected) {
self.onColorSelected(self.currentSelectedColor);
}
[self dismiss];
}
#pragma mark - Public Methods
- (void)showInView:(UIView *)parentView {
[parentView addSubview:self];
[self mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(parentView);
}];
// 初始状态
self.backgroundMask.alpha = 0;
self.containerView.transform = CGAffineTransformMakeTranslation(0, 450); // 更新动画偏移量
// 弹出动画
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.backgroundMask.alpha = 1;
self.containerView.transform = CGAffineTransformIdentity;
} completion:nil];
}
- (void)dismiss {
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
self.backgroundMask.alpha = 0;
self.containerView.transform = CGAffineTransformMakeTranslation(0, 450); // 更新动画偏移量
} completion:^(BOOL finished) {
[self removeFromSuperview];
}];
}
#pragma mark - Lazy Loading
- (UIView *)backgroundMask {
if (!_backgroundMask) {
_backgroundMask = [[UIView alloc] init];
_backgroundMask.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onBackgroundTapped)];
[_backgroundMask addGestureRecognizer:tap];
}
return _backgroundMask;
}
- (UIView *)containerView {
if (!_containerView) {
_containerView = [[UIView alloc] init];
_containerView.backgroundColor = [UIColor colorWithRed:0x0C/255.0 green:0x05/255.0 blue:0x27/255.0 alpha:1.0];
_containerView.layer.cornerRadius = 20;
_containerView.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner;
_containerView.layer.masksToBounds = YES;
}
return _containerView;
}
- (UILabel *)titleLabel {
if (!_titleLabel) {
_titleLabel = [[UILabel alloc] init];
_titleLabel.text = @"Choose your emotion";
_titleLabel.textColor = [UIColor whiteColor];
_titleLabel.font = [UIFont systemFontOfSize:20 weight:UIFontWeightSemibold];
_titleLabel.textAlignment = NSTextAlignmentCenter;
}
return _titleLabel;
}
- (UIButton *)infoButton {
if (!_infoButton) {
_infoButton = [UIButton buttonWithType:UIButtonTypeCustom];
// 使用系统 info.circle 图标
UIImage *infoIcon = [UIImage systemImageNamed:@"info.circle"];
[_infoButton setImage:infoIcon forState:UIControlStateNormal];
_infoButton.tintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.7];
// 点击效果
[_infoButton addTarget:self action:@selector(onInfoButtonTapped) forControlEvents:UIControlEventTouchUpInside];
}
return _infoButton;
}
- (UIView *)selectedColorView {
if (!_selectedColorView) {
_selectedColorView = [[UIView alloc] init];
_selectedColorView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.1];
_selectedColorView.layer.cornerRadius = 25;
_selectedColorView.layer.masksToBounds = YES;
_selectedColorView.hidden = YES; // 初始隐藏
// 颜色圆点
UIView *colorDot = [[UIView alloc] init];
colorDot.tag = 100; // 用于后续查找
colorDot.layer.cornerRadius = 12;
colorDot.layer.masksToBounds = YES;
[_selectedColorView addSubview:colorDot];
[colorDot mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.equalTo(_selectedColorView).offset(15);
make.centerY.equalTo(_selectedColorView);
make.size.mas_equalTo(CGSizeMake(24, 24));
}];
// 情绪名称标签
[_selectedColorView addSubview:self.selectedColorLabel];
[self.selectedColorLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.equalTo(colorDot.mas_trailing).offset(12);
make.centerY.equalTo(_selectedColorView);
make.trailing.equalTo(_selectedColorView).offset(-15);
}];
}
return _selectedColorView;
}
- (UILabel *)selectedColorLabel {
if (!_selectedColorLabel) {
_selectedColorLabel = [[UILabel alloc] init];
_selectedColorLabel.textColor = [UIColor whiteColor];
_selectedColorLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
_selectedColorLabel.text = @"Select an emotion";
}
return _selectedColorLabel;
}
- (UIButton *)okButton {
if (!_okButton) {
_okButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_okButton setTitle:@"OK" forState:UIControlStateNormal];
[_okButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
_okButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
_okButton.backgroundColor = [UIColor colorWithRed:0x9B/255.0 green:0x59/255.0 blue:0xB6/255.0 alpha:1.0];
_okButton.layer.cornerRadius = 16;
_okButton.layer.masksToBounds = YES;
_okButton.enabled = NO; // 初始禁用
_okButton.alpha = 0.5;
[_okButton addTarget:self action:@selector(onOkButtonTapped) forControlEvents:UIControlEventTouchUpInside];
}
return _okButton;
}
- (EPEmotionColorWheelView *)colorWheelView {
if (!_colorWheelView) {
_colorWheelView = [[EPEmotionColorWheelView alloc] init];
_colorWheelView.radius = 100.0;
_colorWheelView.buttonSize = 50.0;
_colorWheelView.preselectedColor = self.preselectedColor;
__weak typeof(self) weakSelf = self;
_colorWheelView.onColorTapped = ^(NSString *hexColor, NSInteger index) {
__strong typeof(weakSelf) self = weakSelf;
// 保存当前选择
self.currentSelectedColor = hexColor;
self.currentSelectedIndex = index;
// 更新选中状态显示
[self updateSelectedColorDisplay:hexColor index:index];
};
}
return _colorWheelView;
}
/// 更新选中颜色显示
- (void)updateSelectedColorDisplay:(NSString *)hexColor index:(NSInteger)index {
NSArray<NSString *> *emotions = @[@"Joy", @"Sadness", @"Anger", @"Fear", @"Surprise", @"Disgust", @"Trust", @"Anticipation"];
// 显示选中状态区域
self.selectedColorView.hidden = NO;
// 更新颜色圆点
UIView *colorDot = [self.selectedColorView viewWithTag:100];
colorDot.backgroundColor = [self colorFromHex:hexColor];
// 更新情绪名称
self.selectedColorLabel.text = emotions[index];
// 启用OK按钮
self.okButton.enabled = YES;
self.okButton.alpha = 1.0;
}
/// Hex 转 UIColor
- (UIColor *)colorFromHex:(NSString *)hexString {
unsigned rgbValue = 0;
NSScanner *scanner = [NSScanner scannerWithString:hexString];
[scanner setScanLocation:1]; // 跳过 #
[scanner scanHexInt:&rgbValue];
return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0
green:((rgbValue & 0xFF00) >> 8)/255.0
blue:(rgbValue & 0xFF)/255.0
alpha:1.0];
}
@end

View File

@@ -1,42 +0,0 @@
//
// EPEmotionColorWheelView.h
// YuMi
//
// Created by AI on 2025-10-15.
// 共享情绪色轮组件 - 纯渲染逻辑,不包含容器和外部交互
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface EPEmotionColorWheelView : UIView
#pragma mark - Configuration
/// 圆周半径(默认 80pt
@property (nonatomic, assign) CGFloat radius;
/// 按钮直径(默认 50pt
@property (nonatomic, assign) CGFloat buttonSize;
/// 预选中的颜色Hex 格式,如 #FFD700
@property (nonatomic, copy, nullable) NSString *preselectedColor;
#pragma mark - Callbacks
/// 颜色点击回调
/// @param hexColor 选中的颜色值
/// @param index 颜色索引 (0-7)
@property (nonatomic, copy) void(^onColorTapped)(NSString *hexColor, NSInteger index);
#pragma mark - Methods
/// 刷新色轮(支持动态更新预选中颜色)
/// @param color 新的预选中颜色
- (void)reloadWithPreselectedColor:(nullable NSString *)color;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,147 +0,0 @@
//
// EPEmotionColorWheelView.m
// YuMi
//
// Created by AI on 2025-10-15.
//
#import "EPEmotionColorWheelView.h"
#import "EPEmotionColorStorage.h"
@interface EPEmotionColorWheelView ()
@property (nonatomic, strong) NSMutableArray<UIButton *> *colorButtons;
@property (nonatomic, assign) NSInteger selectedIndex; // 当前选中的索引
@end
@implementation EPEmotionColorWheelView
#pragma mark - Lifecycle
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// 默认配置
_radius = 80.0;
_buttonSize = 50.0;
_colorButtons = [NSMutableArray array];
self.backgroundColor = [UIColor clearColor];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
// 如果色轮还未创建,自动创建
if (self.colorButtons.count == 0) {
[self createColorButtons];
}
}
#pragma mark - Public Methods
- (void)reloadWithPreselectedColor:(NSString *)color {
self.preselectedColor = color;
// 清除旧按钮
for (UIButton *btn in self.colorButtons) {
[btn removeFromSuperview];
}
[self.colorButtons removeAllObjects];
// 重新创建
[self createColorButtons];
}
#pragma mark - Private Methods
- (void)createColorButtons {
NSArray<NSString *> *colors = [EPEmotionColorStorage allEmotionColors];
NSArray<NSString *> *emotions = @[@"Joy", @"Sadness", @"Anger", @"Fear", @"Surprise", @"Disgust", @"Trust", @"Anticipation"];
CGFloat angleStep = M_PI * 2.0 / colors.count;
CGFloat centerX = CGRectGetWidth(self.bounds) / 2.0;
CGFloat centerY = CGRectGetHeight(self.bounds) / 2.0;
for (NSInteger i = 0; i < colors.count; i++) {
// 从顶部开始,顺时针排列
CGFloat angle = angleStep * i - M_PI_2;
CGFloat x = centerX + self.radius * cos(angle) - self.buttonSize / 2.0;
CGFloat y = centerY + self.radius * sin(angle) - self.buttonSize / 2.0;
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = CGRectMake(x, y, self.buttonSize, self.buttonSize);
button.backgroundColor = [self colorFromHex:colors[i]];
button.layer.cornerRadius = self.buttonSize / 2.0;
button.layer.masksToBounds = YES;
button.layer.borderWidth = 3.0;
button.layer.borderColor = [UIColor whiteColor].CGColor;
button.tag = i;
[button addTarget:self action:@selector(onButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
// 如果是预选中颜色,添加选中态标识
if (self.preselectedColor && [colors[i] isEqualToString:self.preselectedColor]) {
button.layer.borderWidth = 5.0; // 加粗边框
button.transform = CGAffineTransformMakeScale(1.1, 1.1); // 稍微放大
}
// 添加阴影效果
button.layer.shadowColor = [self colorFromHex:colors[i]].CGColor;
button.layer.shadowOffset = CGSizeMake(0, 2);
button.layer.shadowOpacity = 0.6;
button.layer.shadowRadius = 8;
button.layer.masksToBounds = NO;
[self addSubview:button];
[self.colorButtons addObject:button];
}
}
- (void)onButtonTapped:(UIButton *)sender {
NSInteger index = sender.tag;
self.selectedIndex = index;
// 更新选中状态
[self updateSelectionState];
// 执行回调仅用于更新UI不直接确认选择
NSArray<NSString *> *colors = [EPEmotionColorStorage allEmotionColors];
NSString *selectedColor = colors[index];
if (self.onColorTapped) {
self.onColorTapped(selectedColor, index);
}
}
/// 更新选中状态
- (void)updateSelectionState {
for (NSInteger i = 0; i < self.colorButtons.count; i++) {
UIButton *button = self.colorButtons[i];
if (i == self.selectedIndex) {
// 选中状态:加粗边框,稍微放大
button.layer.borderWidth = 5.0;
button.transform = CGAffineTransformMakeScale(1.1, 1.1);
} else {
// 未选中状态:正常边框,正常大小
button.layer.borderWidth = 3.0;
button.transform = CGAffineTransformIdentity;
}
}
}
#pragma mark - Utilities
- (UIColor *)colorFromHex:(NSString *)hexString {
unsigned rgbValue = 0;
NSScanner *scanner = [NSScanner scannerWithString:hexString];
[scanner setScanLocation:1]; // 跳过 #
[scanner scanHexInt:&rgbValue];
return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0
green:((rgbValue & 0xFF00) >> 8)/255.0
blue:(rgbValue & 0xFF)/255.0
alpha:1.0];
}
@end

View File

@@ -1,25 +0,0 @@
//
// EPEmotionInfoView.h
// YuMi
//
// Created by AI on 2025-10-16.
// 普拉奇克情绪轮说明视图
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface EPEmotionInfoView : UIView
/// 在指定视图中显示说明
/// @param parentView 父视图
- (void)showInView:(UIView *)parentView;
/// 关闭说明视图
- (void)dismiss;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,213 +0,0 @@
//
// EPEmotionInfoView.m
// YuMi
//
// Created by AI on 2025-10-16.
//
#import "EPEmotionInfoView.h"
#import <Masonry/Masonry.h>
@interface EPEmotionInfoView ()
@property (nonatomic, strong) UIView *backgroundMask;
@property (nonatomic, strong) UIView *contentContainer;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) UILabel *contentLabel;
@property (nonatomic, strong) UIButton *closeButton;
@end
@implementation EPEmotionInfoView
#pragma mark - Lifecycle
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self setupUI];
}
return self;
}
- (void)setupUI {
self.backgroundColor = [UIColor clearColor];
// 背景遮罩
[self addSubview:self.backgroundMask];
[self.backgroundMask mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
// 内容容器
[self addSubview:self.contentContainer];
[self.contentContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self);
make.leading.trailing.equalTo(self).inset(30);
make.height.mas_lessThanOrEqualTo(500);
}];
// 标题
[self.contentContainer addSubview:self.titleLabel];
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.contentContainer).offset(24);
make.leading.trailing.equalTo(self.contentContainer).inset(20);
}];
// 滚动视图
[self.contentContainer addSubview:self.scrollView];
[self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.titleLabel.mas_bottom).offset(16);
make.leading.trailing.equalTo(self.contentContainer).inset(20);
make.height.mas_lessThanOrEqualTo(320);
}];
// 内容文本
[self.scrollView addSubview:self.contentLabel];
[self.contentLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.scrollView);
make.width.equalTo(self.scrollView);
}];
// 关闭按钮
[self.contentContainer addSubview:self.closeButton];
[self.closeButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.scrollView.mas_bottom).offset(20);
make.centerX.equalTo(self.contentContainer);
make.leading.trailing.equalTo(self.contentContainer).inset(20);
make.height.mas_equalTo(50);
make.bottom.equalTo(self.contentContainer).offset(-24);
}];
}
#pragma mark - Actions
- (void)onBackgroundTapped {
[self dismiss];
}
- (void)onCloseButtonTapped {
[self dismiss];
}
#pragma mark - Public Methods
- (void)showInView:(UIView *)parentView {
[parentView addSubview:self];
[self mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(parentView);
}];
// 初始状态
self.backgroundMask.alpha = 0;
self.contentContainer.alpha = 0;
self.contentContainer.transform = CGAffineTransformMakeScale(0.9, 0.9);
// 弹出动画
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.backgroundMask.alpha = 1;
self.contentContainer.alpha = 1;
self.contentContainer.transform = CGAffineTransformIdentity;
} completion:nil];
}
- (void)dismiss {
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
self.backgroundMask.alpha = 0;
self.contentContainer.alpha = 0;
self.contentContainer.transform = CGAffineTransformMakeScale(0.95, 0.95);
} completion:^(BOOL finished) {
[self removeFromSuperview];
}];
}
#pragma mark - Lazy Loading
- (UIView *)backgroundMask {
if (!_backgroundMask) {
_backgroundMask = [[UIView alloc] init];
_backgroundMask.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.6];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onBackgroundTapped)];
[_backgroundMask addGestureRecognizer:tap];
}
return _backgroundMask;
}
- (UIView *)contentContainer {
if (!_contentContainer) {
_contentContainer = [[UIView alloc] init];
_contentContainer.backgroundColor = [UIColor colorWithRed:0x1a/255.0 green:0x1a/255.0 blue:0x2e/255.0 alpha:1.0];
_contentContainer.layer.cornerRadius = 16;
_contentContainer.layer.masksToBounds = YES;
}
return _contentContainer;
}
- (UILabel *)titleLabel {
if (!_titleLabel) {
_titleLabel = [[UILabel alloc] init];
_titleLabel.text = @"About Emotion Colors";
_titleLabel.textColor = [UIColor whiteColor];
_titleLabel.font = [UIFont systemFontOfSize:20 weight:UIFontWeightBold];
_titleLabel.textAlignment = NSTextAlignmentCenter;
}
return _titleLabel;
}
- (UIScrollView *)scrollView {
if (!_scrollView) {
_scrollView = [[UIScrollView alloc] init];
_scrollView.showsVerticalScrollIndicator = YES;
_scrollView.alwaysBounceVertical = YES;
}
return _scrollView;
}
- (UILabel *)contentLabel {
if (!_contentLabel) {
_contentLabel = [[UILabel alloc] init];
_contentLabel.numberOfLines = 0;
_contentLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.9];
_contentLabel.font = [UIFont systemFontOfSize:15];
// 普拉奇克情绪轮说明文本
NSString *content = @"Based on Plutchik's Wheel of Emotions, we use 8 core colors to represent fundamental human emotions:\n\n"
"🟡 Joy (Gold)\n"
"Represents happiness, delight, and cheerfulness. Like sunshine warming your heart.\n\n"
"🔵 Sadness (Sky Blue)\n"
"Reflects sorrow, melancholy, and contemplation. The quiet depth of blue skies.\n\n"
"🔴 Anger (Coral Red)\n"
"Expresses frustration, rage, and intensity. The fire of passionate emotions.\n\n"
"🟣 Fear (Violet)\n"
"Embodies anxiety, worry, and apprehension. The uncertainty of purple twilight.\n\n"
"🟠 Surprise (Amber)\n"
"Captures amazement, shock, and wonder. The spark of unexpected moments.\n\n"
"🟢 Disgust (Emerald)\n"
"Conveys aversion, distaste, and rejection. The instinctive green of caution.\n\n"
"🔵 Trust (Bright Blue)\n"
"Symbolizes confidence, faith, and security. The clarity of open skies.\n\n"
"🟡 Anticipation (Amber)\n"
"Represents expectation, hope, and eagerness. The warmth of looking forward.\n\n"
"Each color helps you express your current emotional state in moments you share.";
_contentLabel.text = content;
}
return _contentLabel;
}
- (UIButton *)closeButton {
if (!_closeButton) {
_closeButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_closeButton setTitle:@"Got it" forState:UIControlStateNormal];
[_closeButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
_closeButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
_closeButton.backgroundColor = [UIColor colorWithRed:0x9B/255.0 green:0x59/255.0 blue:0xB6/255.0 alpha:1.0];
_closeButton.layer.cornerRadius = 25;
_closeButton.layer.masksToBounds = YES;
[_closeButton addTarget:self action:@selector(onCloseButtonTapped) forControlEvents:UIControlEventTouchUpInside];
}
return _closeButton;
}
@end

View File

@@ -1,26 +0,0 @@
//
// NewMomentCell.h
// YuMi
//
// Created by AI on 2025-10-09.
// Copyright © 2025 YuMi. All rights reserved.
//
#import <UIKit/UIKit.h>
@class MomentsInfoModel;
@class SDPhotoBrowser;
NS_ASSUME_NONNULL_BEGIN
/// 新的动态 Cell卡片式设计
/// 完全不同于原 XPMomentsCell 的列表式设计
@interface EPMomentCell : UITableViewCell
/// 配置 Cell 数据
/// @param model 动态数据模型
- (void)configureWithModel:(MomentsInfoModel *)model;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,564 +0,0 @@
//
// NewMomentCell.m
// YuMi
//
// Created by AI on 2025-10-09.
// Copyright © 2025 YuMi. All rights reserved.
//
#import "EPMomentCell.h"
#import "MomentsInfoModel.h"
#import "AccountInfoStorage.h"
#import "NetImageView.h"
#import "EPEmotionColorStorage.h"
#import "SDPhotoBrowser.h"
#import "YuMi-Swift.h" // Swift 互操作
@interface EPMomentCell () <SDPhotoBrowserDelegate>
// MARK: - UI Components
/// 卡片容器
@property (nonatomic, strong) UIView *cardView;
/// 彩色背景层(毛玻璃下方)
@property (nonatomic, strong) UIView *colorBackgroundView;
/// 毛玻璃效果视图
@property (nonatomic, strong) UIVisualEffectView *blurEffectView;
/// 头像(网络)
@property (nonatomic, strong) NetImageView *avatarImageView;
/// 用户名
@property (nonatomic, strong) UILabel *nameLabel;
/// 时间标签
@property (nonatomic, strong) UILabel *timeLabel;
/// 内容标签
@property (nonatomic, strong) UILabel *contentLabel;
/// 图片容器(九宫格)
@property (nonatomic, strong) UIView *imagesContainer;
@property (nonatomic, strong) NSMutableArray<NetImageView *> *imageViews;
/// 底部操作栏
@property (nonatomic, strong) UIView *actionBar;
/// 点赞按钮
@property (nonatomic, strong) UIButton *likeButton;
/// 评论按钮
@property (nonatomic, strong) UIButton *commentButton;
// 分享按钮已移除
/// 当前数据模型
@property (nonatomic, strong) MomentsInfoModel *currentModel;
/// API Helper (Swift 版本)
@property (nonatomic, strong) EPMomentAPISwiftHelper *apiHelper;
@end
@implementation EPMomentCell
// MARK: - Lifecycle
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
self.selectionStyle = UITableViewCellSelectionStyleNone;
self.backgroundColor = [UIColor clearColor];
[self setupUI];
}
return self;
}
// MARK: - Setup UI
- (void)setupUI {
// 卡片容器(圆角矩形 + 阴影)
[self.contentView addSubview:self.cardView];
[self.cardView mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.trailing.equalTo(self.contentView).inset(15);
make.top.equalTo(self.contentView).offset(8);
make.bottom.equalTo(self.contentView).offset(-8).priority(UILayoutPriorityRequired - 1);
}];
// 彩色背景层(最底层)
[self.cardView addSubview:self.colorBackgroundView];
[self.colorBackgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.cardView);
}];
// 毛玻璃效果视图(在彩色背景层之上)
[self.cardView addSubview:self.blurEffectView];
[self.blurEffectView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.cardView);
}];
// 头像(
[self.blurEffectView.contentView addSubview:self.avatarImageView];
[self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.equalTo(self.cardView).offset(15);
make.top.equalTo(self.cardView).offset(15);
make.size.mas_equalTo(CGSizeMake(40, 40));
}];
// 用户名
[self.blurEffectView.contentView addSubview:self.nameLabel];
[self.nameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.equalTo(self.avatarImageView.mas_trailing).offset(10);
make.top.equalTo(self.avatarImageView);
make.trailing.equalTo(self.cardView).offset(-15);
}];
// 时间
[self.blurEffectView.contentView addSubview:self.timeLabel];
[self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.equalTo(self.nameLabel);
make.bottom.equalTo(self.avatarImageView);
make.trailing.equalTo(self.cardView).offset(-15);
}];
// 内容
[self.blurEffectView.contentView addSubview:self.contentLabel];
[self.contentLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.trailing.equalTo(self.cardView).inset(15);
make.top.equalTo(self.avatarImageView.mas_bottom).offset(12);
}];
// 图片九宫格
[self.blurEffectView.contentView addSubview:self.imagesContainer];
[self.imagesContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.trailing.equalTo(self.cardView).inset(15);
make.top.equalTo(self.contentLabel.mas_bottom).offset(12);
make.height.mas_equalTo(0); // 初始高度为0renderImages 时会 remakeConstraints
}];
// 底部操作栏
[self.blurEffectView.contentView addSubview:self.actionBar];
[self.actionBar mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.trailing.equalTo(self.cardView);
make.top.equalTo(self.imagesContainer.mas_bottom).offset(12);
make.height.mas_equalTo(50);
// 设置较高优先级,确保底部约束生效
make.bottom.equalTo(self.cardView).offset(-8).priority(UILayoutPriorityRequired - 2);
}];
// 点赞按钮(居左显示,评论功能已隐藏)
[self.actionBar addSubview:self.likeButton];
[self.likeButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.equalTo(self.actionBar);
make.centerY.equalTo(self.actionBar);
make.width.mas_greaterThanOrEqualTo(80);
}];
}
// MARK: - Public Methods
- (void)configureWithModel:(MomentsInfoModel *)model {
self.currentModel = model;
// 配置用户名
self.nameLabel.text = model.nick ?: YMLocalizedString(@"user.anonymous");
// 配置时间(将时间戳转换为 MM/dd 格式)
self.timeLabel.text = [self formatTimestampToDate:model.publishTime];
// 配置内容
self.contentLabel.text = model.content ?: @"";
// 配置图片九宫格
[self renderImages:model.dynamicResList];
// 配置点赞按钮状态和数字
NSInteger likeCnt = MAX(0, model.likeCount.integerValue);
self.likeButton.selected = model.isLike;
[self.likeButton setTitle:[NSString stringWithFormat:@" %ld", (long)likeCnt] forState:UIControlStateNormal];
[self.likeButton setTitle:[NSString stringWithFormat:@" %ld", (long)likeCnt] forState:UIControlStateSelected];
self.avatarImageView.imageUrl = model.avatar;
// 配置情绪颜色 border 和 shadow
[self applyEmotionColorEffect:model.emotionColor];
// 确保布局完成后 cell 高度正确
[self setNeedsLayout];
}
/// 应用情绪颜色视觉效果Background + Shadow
- (void)applyEmotionColorEffect:(NSString *)emotionColorHex {
// 获取颜色(已在列表加载时处理,这里直接使用)
if (!emotionColorHex) {
NSLog(@"[EPMomentCell] 警告emotionColorHex 为 nil");
return;
}
UIColor *color = [self colorFromHex:emotionColorHex];
// 移除边框
self.cardView.layer.borderWidth = 0;
// 设置彩色背景50% 透明度,在毛玻璃下方)
self.colorBackgroundView.backgroundColor = [color colorWithAlphaComponent:0.5];
// 设置 shadow使用情绪颜色
self.cardView.layer.shadowColor = color.CGColor;
self.cardView.layer.shadowOffset = CGSizeMake(0, 2);
self.cardView.layer.shadowOpacity = 0.5;
self.cardView.layer.shadowRadius = 16.0;
}
/// Hex 转 UIColor
- (UIColor *)colorFromHex:(NSString *)hexString {
unsigned rgbValue = 0;
NSScanner *scanner = [NSScanner scannerWithString:hexString];
[scanner setScanLocation:1]; // 跳过 #
[scanner scanHexInt:&rgbValue];
return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0
green:((rgbValue & 0xFF00) >> 8)/255.0
blue:(rgbValue & 0xFF)/255.0
alpha:1.0];
}
// MARK: - Images Grid
- (void)renderImages:(NSArray *)resList {
// 清理旧视图
for (UIView *iv in self.imageViews) { [iv removeFromSuperview]; }
[self.imageViews removeAllObjects];
if (resList.count == 0) {
[self.imagesContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.trailing.equalTo(self.cardView).inset(15);
make.top.equalTo(self.contentLabel.mas_bottom).offset(0);
make.height.mas_equalTo(0);
}];
// 强制触发布局更新
[self.contentView setNeedsLayout];
[self.contentView layoutIfNeeded];
return;
}
NSInteger columns = 3;
CGFloat spacing = 6.0;
CGFloat totalWidth = [UIScreen mainScreen].bounds.size.width - 30 - 30; // 左右各 15 内边距,再减卡片左右 15
CGFloat itemW = floor((totalWidth - spacing * (columns - 1)) / columns);
for (NSInteger i = 0; i < resList.count && i < 9; i++) {
NetImageConfig *config = [[NetImageConfig alloc] init];
config.placeHolder = [UIImageConstant defaultBannerPlaceholder];
NetImageView *iv = [[NetImageView alloc] initWithConfig:config];
iv.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
iv.layer.cornerRadius = 6;
iv.layer.masksToBounds = YES;
iv.contentMode = UIViewContentModeScaleAspectFill;
iv.userInteractionEnabled = YES;
iv.tag = i; // 用于识别点击的图片索引
// 添加点击手势
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onImageTapped:)];
[iv addGestureRecognizer:tap];
[self.imagesContainer addSubview:iv];
[self.imageViews addObject:iv];
NSInteger row = i / columns;
NSInteger col = i % columns;
[iv mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.equalTo(self.imagesContainer).offset((itemW + spacing) * col);
make.top.equalTo(self.imagesContainer).offset((itemW + spacing) * row);
make.size.mas_equalTo(CGSizeMake(itemW, itemW));
}];
// 绑定网络图片
NSString *url = nil;
id item = resList[i];
if ([item isKindOfClass:[NSDictionary class]]) {
url = [item valueForKey:@"resUrl"] ?: [item valueForKey:@"url"];
} else if ([item respondsToSelector:@selector(resUrl)]) {
url = [item valueForKey:@"resUrl"];
}
iv.imageUrl = url;
}
NSInteger rows = ((MIN(resList.count, 9) - 1) / columns) + 1;
CGFloat height = rows * itemW + (rows - 1) * spacing;
[self.imagesContainer mas_remakeConstraints:^(MASConstraintMaker *make) {
make.leading.trailing.equalTo(self.cardView).inset(15);
make.top.equalTo(self.contentLabel.mas_bottom).offset(12);
make.height.mas_equalTo(height);
}];
// 强制触发布局更新,确保 cell 高度正确计算
[self.contentView setNeedsLayout];
[self.contentView layoutIfNeeded];
}
/// 格式化时间戳为 MM/dd 格式
- (NSString *)formatTimestampToDate:(NSString *)timestampString {
if (!timestampString || timestampString.length == 0) {
return @"";
}
// 将字符串转换为时间戳(毫秒)
NSTimeInterval timestamp = [timestampString doubleValue] / 1000.0;
if (timestamp <= 0) {
return @"";
}
NSDate *date = [NSDate dateWithTimeIntervalSince1970:timestamp];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.dateFormat = @"MM/dd";
return [formatter stringFromDate:date];
}
/// 格式化时间戳为相对时间
- (NSString *)formatTimeInterval:(NSInteger)timestamp {
if (timestamp <= 0) return YMLocalizedString(@"time.just_now");
NSTimeInterval interval = [[NSDate date] timeIntervalSince1970] - timestamp / 1000.0;
if (interval < 60) {
return YMLocalizedString(@"time.just_now");
} else if (interval < 3600) {
return [NSString stringWithFormat:YMLocalizedString(@"time.minutes_ago"), interval / 60];
} else if (interval < 86400) {
return [NSString stringWithFormat:YMLocalizedString(@"time.hours_ago"), interval / 3600];
} else if (interval < 604800) {
return [NSString stringWithFormat:YMLocalizedString(@"time.days_ago"), interval / 86400];
} else {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.dateFormat = @"yyyy-MM-dd";
return [formatter stringFromDate:[NSDate dateWithTimeIntervalSince1970:timestamp / 1000.0]];
}
}
// MARK: - Actions
- (void)onLikeButtonTapped {
if (!self.currentModel) return;
// 如果已点赞,执行取消点赞
if (self.currentModel.isLike) {
[self performLikeAction:NO];
return;
}
// 审核中的动态不可点赞
if (self.currentModel.status == 0) {
NSLog(@"[EPMomentCell] 动态审核中,无法点赞");
// TODO: 可选择显示提示 Toast
return;
}
// 执行点赞
[self performLikeAction:YES];
}
- (void)performLikeAction:(BOOL)isLike {
NSLog(@"[EPMomentCell] %@ 动态: %@", isLike ? @"点赞" : @"取消点赞", self.currentModel.dynamicId);
NSString *dynamicId = self.currentModel.dynamicId;
NSString *likedUid = self.currentModel.uid;
long worldId = self.currentModel.worldId;
// 使用 Swift API Helper
@kWeakify(self);
[self.apiHelper likeMomentWithDynamicId:dynamicId
isLike:isLike
likedUid:likedUid
worldId:worldId
completion:^{
@kStrongify(self);
// 更新点赞状态
self.currentModel.isLike = isLike;
NSInteger likeCount = [self.currentModel.likeCount integerValue];
likeCount += isLike ? 1 : -1;
likeCount = MAX(0, likeCount); // 防止负数
self.currentModel.likeCount = @(likeCount).stringValue;
// 更新 UI
self.likeButton.selected = self.currentModel.isLike;
[self.likeButton setTitle:[NSString stringWithFormat:@" %ld", (long)likeCount] forState:UIControlStateNormal];
[self.likeButton setTitle:[NSString stringWithFormat:@" %ld", (long)likeCount] forState:UIControlStateSelected];
NSLog(@"[EPMomentCell] %@ 成功", isLike ? @"点赞" : @"取消点赞");
} failure:^(NSInteger code, NSString * _Nonnull msg) {
NSLog(@"[EPMomentCell] %@ 失败 (code: %ld): %@", isLike ? @"点赞" : @"取消点赞", (long)code, msg);
}];
}
// 评论功能已隐藏
// - (void)onCommentButtonTapped {
// NSLog(@"[EPMomentCell] 评论");
// }
- (void)onImageTapped:(UITapGestureRecognizer *)gesture {
if (!self.currentModel || !self.currentModel.dynamicResList.count) return;
NSInteger index = gesture.view.tag;
NSLog(@"[EPMomentCell] 点击图片索引: %ld", (long)index);
SDPhotoBrowser *browser = [[SDPhotoBrowser alloc] init];
browser.sourceImagesContainerView = self.imagesContainer;
browser.delegate = self;
browser.imageCount = self.currentModel.dynamicResList.count;
browser.currentImageIndex = index;
[browser show];
}
#pragma mark - SDPhotoBrowserDelegate
- (NSURL *)photoBrowser:(SDPhotoBrowser *)browser highQualityImageURLForIndex:(NSInteger)index {
if (index >= 0 && index < self.currentModel.dynamicResList.count) {
id item = self.currentModel.dynamicResList[index];
NSString *url = nil;
if ([item isKindOfClass:[NSDictionary class]]) {
url = [item valueForKey:@"resUrl"] ?: [item valueForKey:@"url"];
} else if ([item respondsToSelector:@selector(resUrl)]) {
url = [item valueForKey:@"resUrl"];
}
if (url) {
return [NSURL URLWithString:url];
}
}
return nil;
}
- (UIImage *)photoBrowser:(SDPhotoBrowser *)browser placeholderImageForIndex:(NSInteger)index {
return [UIImageConstant defaultBannerPlaceholder];
}
// MARK: - Lazy Loading
- (UIView *)cardView {
if (!_cardView) {
_cardView = [[UIView alloc] init];
_cardView.backgroundColor = [UIColor clearColor]; // 透明背景,颜色由 colorBackgroundView 提供
_cardView.layer.cornerRadius = 12; // 圆角
// Shadow 将由 applyEmotionColorEffect 动态设置
_cardView.layer.masksToBounds = NO;
}
return _cardView;
}
- (UIView *)colorBackgroundView {
if (!_colorBackgroundView) {
_colorBackgroundView = [[UIView alloc] init];
_colorBackgroundView.layer.cornerRadius = 12;
_colorBackgroundView.layer.masksToBounds = YES;
}
return _colorBackgroundView;
}
- (UIVisualEffectView *)blurEffectView {
if (!_blurEffectView) {
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
_blurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
_blurEffectView.layer.cornerRadius = 12;
_blurEffectView.layer.masksToBounds = YES;
}
return _blurEffectView;
}
- (UIImageView *)avatarImageView {
if (!_avatarImageView) {
NetImageConfig *config = [[NetImageConfig alloc] init];
_avatarImageView = [[NetImageView alloc] initWithConfig:config];
_avatarImageView.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1.0];
_avatarImageView.layer.cornerRadius = 20;
_avatarImageView.layer.masksToBounds = YES;
_avatarImageView.contentMode = UIViewContentModeScaleAspectFill;
}
return _avatarImageView;
}
- (UILabel *)nameLabel {
if (!_nameLabel) {
_nameLabel = [[UILabel alloc] init];
_nameLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightMedium];
_nameLabel.textColor = [UIColor whiteColor];
}
return _nameLabel;
}
- (UILabel *)timeLabel {
if (!_timeLabel) {
_timeLabel = [[UILabel alloc] init];
_timeLabel.font = [UIFont systemFontOfSize:12];
_timeLabel.textColor = [UIColor colorWithWhite:1 alpha:0.6];
}
return _timeLabel;
}
- (UILabel *)contentLabel {
if (!_contentLabel) {
_contentLabel = [[UILabel alloc] init];
_contentLabel.font = [UIFont systemFontOfSize:15];
_contentLabel.textColor = [UIColor whiteColor];
_contentLabel.numberOfLines = 0;
_contentLabel.lineBreakMode = NSLineBreakByWordWrapping;
}
return _contentLabel;
}
- (UIView *)actionBar {
if (!_actionBar) {
_actionBar = [[UIView alloc] init];
_actionBar.backgroundColor = [UIColor clearColor]; // 半透明白色,与毛玻璃效果搭配
}
return _actionBar;
}
- (UIButton *)likeButton {
if (!_likeButton) {
_likeButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_likeButton setImage:[UIImage imageNamed:@"monents_info_like_count_normal"] forState:UIControlStateNormal];
[_likeButton setImage:[UIImage imageNamed:@"monents_info_like_count_select"] forState:UIControlStateSelected];
[_likeButton setTitle:@" 0" forState:UIControlStateNormal];
_likeButton.titleLabel.font = [UIFont systemFontOfSize:13];
[_likeButton setTitleColor:[UIColor colorWithWhite:1 alpha:0.6] forState:UIControlStateNormal];
[_likeButton setTitleColor:[UIColor colorWithWhite:1 alpha:1.0] forState:UIControlStateSelected];
[_likeButton addTarget:self action:@selector(onLikeButtonTapped) forControlEvents:UIControlEventTouchUpInside];
}
return _likeButton;
}
// 评论按钮已移除
- (UIButton *)commentButton {
return nil;
}
- (UIButton *)createActionButtonWithTitle:(NSString *)title {
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
[button setTitle:title forState:UIControlStateNormal];
button.titleLabel.font = [UIFont systemFontOfSize:13];
[button setTitleColor:[UIColor colorWithWhite:0.5 alpha:1.0] forState:UIControlStateNormal];
return button;
}
- (UIView *)imagesContainer {
if (!_imagesContainer) {
_imagesContainer = [[UIView alloc] init];
_imagesContainer.backgroundColor = [UIColor clearColor];
}
return _imagesContainer;
}
- (NSMutableArray<NetImageView *> *)imageViews {
if (!_imageViews) {
_imageViews = [NSMutableArray array];
}
return _imageViews;
}
- (EPMomentAPISwiftHelper *)apiHelper {
if (!_apiHelper) {
_apiHelper = [[EPMomentAPISwiftHelper alloc] init];
}
return _apiHelper;
}
@end

View File

@@ -1,46 +0,0 @@
//
// EPMomentListView.h
// YuMi
//
// Created by AI on 2025-10-10.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@class EPMomentAPISwiftHelper;
@class MomentsInfoModel;
/// 推荐/我的动态列表数据源类型
typedef NS_ENUM(NSInteger, EPMomentListSourceType) {
EPMomentListSourceTypeRecommend = 0,
EPMomentListSourceTypeMine = 1
};
/// 承载 Moments 列表与分页刷新的视图
@interface EPMomentListView : UIView
/// 当前数据源(外部可读)
@property (nonatomic, strong, readonly) NSArray *rawList;
/// 列表类型:推荐 / 我的
@property (nonatomic, assign) EPMomentListSourceType sourceType;
/// 外部可设置:当某一项被点击
@property (nonatomic, copy) void (^onSelectMoment)(NSInteger index);
/// 重新加载(刷新到第一页)
- (void)reloadFirstPage;
/// 使用本地数组模式显示动态(禁用分页加载)
/// @param dynamicInfo 本地动态数组
/// @param refreshCallback 下拉刷新回调(由外部重新获取数据)
- (void)loadWithDynamicInfo:(NSArray<MomentsInfoModel *> *)dynamicInfo
refreshCallback:(void(^)(void))refreshCallback;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,253 +0,0 @@
//
// EPMomentListView.m
// YuMi
//
// Created by AI on 2025-10-10.
//
#import <UIKit/UIKit.h>
#import "EPMomentListView.h"
#import "EPMomentCell.h"
#import <MJRefresh/MJRefresh.h>
#import "YuMi-Swift.h"
#import "EPEmotionColorStorage.h"
@interface EPMomentListView () <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) UIRefreshControl *refreshControl;
@property (nonatomic, strong) NSMutableArray *mutableRawList;
@property (nonatomic, strong) EPMomentAPISwiftHelper *api;
@property (nonatomic, assign) BOOL isLoading;
@property (nonatomic, copy) NSString *nextID;
@property (nonatomic, assign) BOOL isLocalMode;
@property (nonatomic, copy) void (^refreshCallback)(void);
@end
@implementation EPMomentListView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
_api = [[EPMomentAPISwiftHelper alloc] init];
_mutableRawList = [NSMutableArray array];
_sourceType = EPMomentListSourceTypeRecommend;
_isLocalMode = NO; // 明确初始化为网络模式
[self addSubview:self.tableView];
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
}
return self;
}
- (NSArray<NSMutableDictionary *> *)rawList {
return [self.mutableRawList copy];
}
- (void)reloadFirstPage {
if (self.isLocalMode) {
// 本地模式:调用外部刷新回调
if (self.refreshCallback) {
self.refreshCallback();
}
[self.refreshControl endRefreshing];
return;
}
// 网络模式:重新请求第一页
self.nextID = @"";
[self.mutableRawList removeAllObjects];
[self.tableView reloadData];
[self.tableView.mj_footer resetNoMoreData];
[self requestNextPage];
}
- (void)loadWithDynamicInfo:(NSArray<MomentsInfoModel *> *)dynamicInfo
refreshCallback:(void (^)(void))refreshCallback {
self.isLocalMode = YES;
self.refreshCallback = refreshCallback;
[self.mutableRawList removeAllObjects];
if (dynamicInfo.count > 0) {
[self.mutableRawList addObjectsFromArray:dynamicInfo];
}
// 隐藏加载更多 footer
self.tableView.mj_footer.hidden = YES;
[self.tableView reloadData];
[self.refreshControl endRefreshing];
}
- (void)requestNextPage {
if (self.isLoading) return;
self.isLoading = YES;
@kWeakify(self);
[self.api fetchLatestMomentsWithNextID:self.nextID
completion:^(NSArray<MomentsInfoModel *> * _Nonnull list, NSString * _Nonnull nextMomentID) {
@kStrongify(self);
[self endLoading];
if (list.count > 0) {
// 处理情绪颜色
[self processEmotionColors:list isFirstPage:(self.nextID.length == 0)];
self.nextID = nextMomentID;
[self.mutableRawList addObjectsFromArray:list];
[self.tableView reloadData];
if (nextMomentID.length > 0) {
[self.tableView.mj_footer endRefreshing];
} else {
[self.tableView.mj_footer endRefreshingWithNoMoreData];
}
} else {
// 返回空数据:显示 "no more data" 状态
[self.tableView.mj_footer endRefreshingWithNoMoreData];
}
} failure:^(NSInteger code, NSString * _Nonnull msg) {
@kStrongify(self);
[self endLoading];
// TODO: 完全没有数据情况下,后续补充数据异常页面
[self.tableView.mj_footer endRefreshing];
}];
}
- (void)endLoading {
self.isLoading = NO;
[self.refreshControl endRefreshing];
}
/// 处理动态的情绪颜色(从 UserDefaults 匹配 + 处理临时颜色)
- (void)processEmotionColors:(NSArray<MomentsInfoModel *> *)list isFirstPage:(BOOL)isFirstPage {
// 检查是否有待处理的临时情绪颜色
NSString *pendingColor = [[NSUserDefaults standardUserDefaults] stringForKey:@"EP_Pending_Emotion_Color"];
NSNumber *pendingTimestamp = [[NSUserDefaults standardUserDefaults] objectForKey:@"EP_Pending_Emotion_Timestamp"];
for (NSInteger i = 0; i < list.count; i++) {
MomentsInfoModel *model = list[i];
// 优先检查临时颜色(仅第一页第一条)
if (isFirstPage && i == 0 && pendingColor && pendingTimestamp) {
// 检查时间戳5秒内有效避免误匹配
NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
NSTimeInterval pending = pendingTimestamp.doubleValue;
if ((now - pending) < 5.0) {
model.emotionColor = pendingColor;
// 保存到持久化存储
[EPEmotionColorStorage saveColor:pendingColor forDynamicId:model.dynamicId];
// 清除临时数据
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"EP_Pending_Emotion_Color"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"EP_Pending_Emotion_Timestamp"];
[[NSUserDefaults standardUserDefaults] synchronize];
continue;
}
}
// 从持久化存储中匹配
NSString *savedColor = [EPEmotionColorStorage colorForDynamicId:model.dynamicId];
if (savedColor) {
model.emotionColor = savedColor;
} else {
// 无保存颜色,生成随机颜色并立即持久化
NSString *randomColor = [EPEmotionColorStorage randomEmotionColor];
model.emotionColor = randomColor;
[EPEmotionColorStorage saveColor:randomColor forDynamicId:model.dynamicId];
NSLog(@"[EPMomentListView] 为动态 %@ 分配随机颜色: %@", model.dynamicId, randomColor);
}
}
}
#pragma mark - UITableView
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.mutableRawList.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
EPMomentCell *cell = [tableView dequeueReusableCellWithIdentifier:@"NewMomentCell" forIndexPath:indexPath];
if (indexPath.row < self.mutableRawList.count) {
MomentsInfoModel *model = [self.mutableRawList xpSafeObjectAtIndex:indexPath.row];
[cell configureWithModel:model];
}
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 200;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
if (self.onSelectMoment) self.onSelectMoment(indexPath.row);
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// 本地模式下不触发加载更多
if (self.isLocalMode) return;
CGFloat offsetY = scrollView.contentOffset.y;
CGFloat contentHeight = scrollView.contentSize.height;
CGFloat screenHeight = scrollView.frame.size.height;
if (offsetY > contentHeight - screenHeight - 100 && !self.isLoading) {
[self requestNextPage];
}
}
#pragma mark - Lazy
- (UITableView *)tableView {
if (!_tableView) {
_tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
_tableView.backgroundColor = [UIColor clearColor];
_tableView.estimatedRowHeight = 200;
_tableView.rowHeight = UITableViewAutomaticDimension;
_tableView.showsVerticalScrollIndicator = NO;
// 底部留出更高空间,避免被悬浮 TabBar 遮挡
_tableView.contentInset = UIEdgeInsetsMake(10, 0, 120, 0);
_tableView.scrollIndicatorInsets = UIEdgeInsetsMake(10, 0, 120, 0);
[_tableView registerClass:[EPMomentCell class] forCellReuseIdentifier:@"NewMomentCell"];
_tableView.refreshControl = self.refreshControl;
// MJRefresh Footer - 加载更多
__weak typeof(self) weakSelf = self;
MJRefreshAutoNormalFooter *footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{
__strong typeof(weakSelf) self = weakSelf;
if (!self.isLoading && self.nextID.length > 0) {
[self requestNextPage];
} else if (self.nextID.length == 0) {
[self.tableView.mj_footer endRefreshingWithNoMoreData];
} else {
[self.tableView.mj_footer endRefreshing];
}
}];
// 设置白色文字和指示器
footer.stateLabel.textColor = [UIColor whiteColor];
footer.loadingView.color = [UIColor whiteColor];
_tableView.mj_footer = footer;
}
return _tableView;
}
- (UIRefreshControl *)refreshControl {
if (!_refreshControl) {
_refreshControl = [[UIRefreshControl alloc] init];
_refreshControl.tintColor = [UIColor whiteColor]; // 白色加载指示器
[_refreshControl addTarget:self action:@selector(reloadFirstPage) forControlEvents:UIControlEventValueChanged];
}
return _refreshControl;
}
@end

View File

@@ -1,36 +0,0 @@
//
// EPSignatureColorGuideView.h
// YuMi
//
// Created by AI on 2025-10-15.
// 用户专属情绪颜色首次引导页
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface EPSignatureColorGuideView : UIView
/// 颜色确认回调
@property (nonatomic, copy) void(^onColorConfirmed)(NSString *hexColor);
/// Skip 按钮点击回调(仅 debug 模式且已有颜色时显示)
@property (nonatomic, copy) void(^onSkipTapped)(void);
/// 在 window 中显示引导页(全屏模态)
/// @param window 应用主 window
- (void)showInWindow:(UIWindow *)window;
/// 在 window 中显示引导页(带 Skip 按钮)
/// @param window 应用主 window
/// @param showSkip 是否显示 Skip 按钮(用于 debug 模式)
- (void)showInWindow:(UIWindow *)window showSkipButton:(BOOL)showSkip;
/// 关闭引导页
- (void)dismiss;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,372 +0,0 @@
//
// EPSignatureColorGuideView.m
// YuMi
//
// Created by AI on 2025-10-15.
//
#import "EPSignatureColorGuideView.h"
#import "EPEmotionColorWheelView.h"
#import "EPEmotionInfoView.h"
#import <Masonry/Masonry.h>
@interface EPSignatureColorGuideView ()
@property (nonatomic, strong) CAGradientLayer *gradientLayer;
@property (nonatomic, strong) UIView *contentContainer;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UILabel *subtitleLabel;
@property (nonatomic, strong) UIButton *infoButton;
@property (nonatomic, strong) UIView *selectedColorView;
@property (nonatomic, strong) UILabel *selectedColorLabel;
@property (nonatomic, strong) EPEmotionColorWheelView *colorWheelView;
@property (nonatomic, strong) UIButton *confirmButton;
@property (nonatomic, strong) UIButton *skipButton;
@property (nonatomic, copy) NSString *selectedColor; // 当前选中的颜色
@end
@implementation EPSignatureColorGuideView
#pragma mark - Lifecycle
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self setupUI];
}
return self;
}
- (void)setupUI {
// 渐变背景
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.colors = @[
(id)[UIColor colorWithRed:0x1a/255.0 green:0x09/255.0 blue:0x33/255.0 alpha:1.0].CGColor,
(id)[UIColor colorWithRed:0x0d/255.0 green:0x1b/255.0 blue:0x2a/255.0 alpha:1.0].CGColor
];
gradientLayer.startPoint = CGPointMake(0.5, 0);
gradientLayer.endPoint = CGPointMake(0.5, 1);
[self.layer insertSublayer:gradientLayer atIndex:0];
self.gradientLayer = gradientLayer;
// 内容容器
[self addSubview:self.contentContainer];
[self.contentContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self);
make.leading.trailing.equalTo(self).inset(30);
}];
// 标题
[self.contentContainer addSubview:self.titleLabel];
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.contentContainer);
make.centerX.equalTo(self.contentContainer);
}];
// 副标题
[self.contentContainer addSubview:self.subtitleLabel];
[self.subtitleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.titleLabel.mas_bottom).offset(12);
make.centerX.equalTo(self.contentContainer);
make.leading.trailing.equalTo(self.contentContainer).inset(20);
}];
// Info 按钮(左上角,与 Skip 按钮对齐)
[self addSubview:self.infoButton];
[self.infoButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.equalTo(self).offset(20);
make.top.equalTo(self).offset(60);
make.size.mas_equalTo(CGSizeMake(36, 36));
}];
// 选中状态显示区域
[self.contentContainer addSubview:self.selectedColorView];
[self.selectedColorView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.subtitleLabel.mas_bottom).offset(30);
make.centerX.equalTo(self.contentContainer);
make.height.mas_equalTo(60);
make.leading.trailing.equalTo(self.contentContainer).inset(40);
}];
// 色轮视图(使用共享组件)
[self.contentContainer addSubview:self.colorWheelView];
[self.colorWheelView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.selectedColorView.mas_bottom).offset(30);
make.centerX.equalTo(self.contentContainer);
make.size.mas_equalTo(CGSizeMake(360, 360)); // 从280x280增加到360x360
}];
// 确认按钮
[self.contentContainer addSubview:self.confirmButton];
[self.confirmButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.colorWheelView.mas_bottom).offset(50);
make.leading.trailing.equalTo(self.contentContainer).inset(20);
make.height.mas_equalTo(56);
make.bottom.equalTo(self.contentContainer);
}];
// Skip 按钮(右上角,初始隐藏)
[self addSubview:self.skipButton];
[self.skipButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self).offset(60);
make.trailing.equalTo(self).offset(-20);
make.size.mas_equalTo(CGSizeMake(60, 36));
}];
}
- (void)layoutSubviews {
[super layoutSubviews];
// 更新渐变层 frame
self.gradientLayer.frame = self.bounds;
}
#pragma mark - Actions
- (void)onConfirmButtonTapped {
if (!self.selectedColor) return;
// 执行回调
if (self.onColorConfirmed) {
self.onColorConfirmed(self.selectedColor);
}
// 关闭引导页
[self dismiss];
}
- (void)onSkipButtonTapped {
// 执行 skip 回调
if (self.onSkipTapped) {
self.onSkipTapped();
}
// 关闭引导页
[self dismiss];
}
- (void)onInfoButtonTapped {
EPEmotionInfoView *infoView = [[EPEmotionInfoView alloc] init];
[infoView showInView:self];
}
#pragma mark - Public Methods
- (void)showInWindow:(UIWindow *)window {
[self showInWindow:window showSkipButton:NO];
}
- (void)showInWindow:(UIWindow *)window showSkipButton:(BOOL)showSkip {
[window addSubview:self];
[self mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(window);
}];
// 控制 Skip 按钮显示
self.skipButton.hidden = !showSkip;
// 初始状态
self.alpha = 0;
self.contentContainer.transform = CGAffineTransformMakeScale(0.8, 0.8);
// 显示动画
[UIView animateWithDuration:0.4 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.alpha = 1.0;
self.contentContainer.transform = CGAffineTransformIdentity;
} completion:nil];
}
- (void)dismiss {
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
self.alpha = 0;
self.contentContainer.transform = CGAffineTransformMakeScale(0.95, 0.95);
} completion:^(BOOL finished) {
[self removeFromSuperview];
}];
}
#pragma mark - Lazy Loading
- (UIView *)contentContainer {
if (!_contentContainer) {
_contentContainer = [[UIView alloc] init];
_contentContainer.backgroundColor = [UIColor clearColor];
}
return _contentContainer;
}
- (UILabel *)titleLabel {
if (!_titleLabel) {
_titleLabel = [[UILabel alloc] init];
_titleLabel.text = @"Choose your signature emotion";
_titleLabel.textColor = [UIColor whiteColor];
_titleLabel.font = [UIFont systemFontOfSize:24 weight:UIFontWeightBold];
_titleLabel.textAlignment = NSTextAlignmentCenter;
}
return _titleLabel;
}
- (UILabel *)subtitleLabel {
if (!_subtitleLabel) {
_subtitleLabel = [[UILabel alloc] init];
_subtitleLabel.text = @"This color represents your emotional identity";
_subtitleLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.7];
_subtitleLabel.font = [UIFont systemFontOfSize:14];
_subtitleLabel.textAlignment = NSTextAlignmentCenter;
_subtitleLabel.numberOfLines = 0;
}
return _subtitleLabel;
}
- (UIView *)selectedColorView {
if (!_selectedColorView) {
_selectedColorView = [[UIView alloc] init];
_selectedColorView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.15];
_selectedColorView.layer.cornerRadius = 30;
_selectedColorView.layer.masksToBounds = YES;
_selectedColorView.hidden = YES; // 初始隐藏
// 颜色圆点
UIView *colorDot = [[UIView alloc] init];
colorDot.tag = 100; // 用于后续查找
colorDot.layer.cornerRadius = 16;
colorDot.layer.masksToBounds = YES;
[_selectedColorView addSubview:colorDot];
[colorDot mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.equalTo(_selectedColorView).offset(20);
make.centerY.equalTo(_selectedColorView);
make.size.mas_equalTo(CGSizeMake(32, 32));
}];
// 情绪名称标签
[_selectedColorView addSubview:self.selectedColorLabel];
[self.selectedColorLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.equalTo(colorDot.mas_trailing).offset(16);
make.centerY.equalTo(_selectedColorView);
make.trailing.equalTo(_selectedColorView).offset(-20);
}];
}
return _selectedColorView;
}
- (UILabel *)selectedColorLabel {
if (!_selectedColorLabel) {
_selectedColorLabel = [[UILabel alloc] init];
_selectedColorLabel.textColor = [UIColor whiteColor];
_selectedColorLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightMedium];
_selectedColorLabel.text = @"Select your signature emotion";
}
return _selectedColorLabel;
}
- (EPEmotionColorWheelView *)colorWheelView {
if (!_colorWheelView) {
_colorWheelView = [[EPEmotionColorWheelView alloc] init];
_colorWheelView.radius = 100.0;
_colorWheelView.buttonSize = 54.0;
__weak typeof(self) weakSelf = self;
_colorWheelView.onColorTapped = ^(NSString *hexColor, NSInteger index) {
__strong typeof(weakSelf) self = weakSelf;
// 保存选中的颜色
self.selectedColor = hexColor;
// 更新选中状态显示
[self updateSelectedColorDisplay:hexColor index:index];
// 启用确认按钮
self.confirmButton.enabled = YES;
self.confirmButton.alpha = 1.0;
};
}
return _colorWheelView;
}
/// 更新选中颜色显示
- (void)updateSelectedColorDisplay:(NSString *)hexColor index:(NSInteger)index {
NSArray<NSString *> *emotions = @[@"Joy", @"Sadness", @"Anger", @"Fear", @"Surprise", @"Disgust", @"Trust", @"Anticipation"];
// 显示选中状态区域
self.selectedColorView.hidden = NO;
// 更新颜色圆点
UIView *colorDot = [self.selectedColorView viewWithTag:100];
colorDot.backgroundColor = [self colorFromHex:hexColor];
// 更新情绪名称
self.selectedColorLabel.text = emotions[index];
}
/// Hex 转 UIColor
- (UIColor *)colorFromHex:(NSString *)hexString {
unsigned rgbValue = 0;
NSScanner *scanner = [NSScanner scannerWithString:hexString];
[scanner setScanLocation:1]; // 跳过 #
[scanner scanHexInt:&rgbValue];
return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0
green:((rgbValue & 0xFF00) >> 8)/255.0
blue:(rgbValue & 0xFF)/255.0
alpha:1.0];
}
- (UIButton *)confirmButton {
if (!_confirmButton) {
_confirmButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_confirmButton setTitle:@"Confirm & Continue" forState:UIControlStateNormal];
[_confirmButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
_confirmButton.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold];
_confirmButton.layer.cornerRadius = 28;
_confirmButton.layer.masksToBounds = YES;
// 渐变背景
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.colors = @[
(id)[UIColor colorWithRed:0x9B/255.0 green:0x59/255.0 blue:0xB6/255.0 alpha:1.0].CGColor,
(id)[UIColor colorWithRed:0x6C/255.0 green:0x34/255.0 blue:0x83/255.0 alpha:1.0].CGColor
];
gradient.startPoint = CGPointMake(0, 0);
gradient.endPoint = CGPointMake(1, 0);
gradient.frame = CGRectMake(0, 0, 1000, 56); // 宽度设大一点
[_confirmButton.layer insertSublayer:gradient atIndex:0];
[_confirmButton addTarget:self action:@selector(onConfirmButtonTapped) forControlEvents:UIControlEventTouchUpInside];
// 初始禁用状态
_confirmButton.enabled = NO;
_confirmButton.alpha = 0.5;
}
return _confirmButton;
}
- (UIButton *)infoButton {
if (!_infoButton) {
_infoButton = [UIButton buttonWithType:UIButtonTypeCustom];
// 使用系统 info.circle 图标
UIImage *infoIcon = [UIImage systemImageNamed:@"info.circle"];
[_infoButton setImage:infoIcon forState:UIControlStateNormal];
_infoButton.tintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8];
// 点击效果
[_infoButton addTarget:self action:@selector(onInfoButtonTapped) forControlEvents:UIControlEventTouchUpInside];
}
return _infoButton;
}
- (UIButton *)skipButton {
if (!_skipButton) {
_skipButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_skipButton setTitle:@"Skip" forState:UIControlStateNormal];
[_skipButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
_skipButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
_skipButton.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.2];
_skipButton.layer.cornerRadius = 18;
_skipButton.layer.masksToBounds = YES;
[_skipButton addTarget:self action:@selector(onSkipButtonTapped) forControlEvents:UIControlEventTouchUpInside];
_skipButton.hidden = YES; // 默认隐藏
}
return _skipButton;
}
@end

View File

@@ -1,534 +0,0 @@
//
// EPTabBarController.swift
// YuMi
//
// Created by AI on 2025-10-09.
// Copyright © 2025 YuMi. All rights reserved.
//
import UIKit
import SnapKit
/// EP 系列 TabBar 控制器
/// 悬浮设计 + 液态玻璃效果,只包含 Moment 和 Mine 两个 Tab
@objc class EPTabBarController: UITabBarController {
// MARK: - Properties
/// 是否已登录
private var isLoggedIn: Bool = false
/// 自定义悬浮 TabBar 容器
private var customTabBarView: UIView!
/// 毛玻璃背景视图
private var tabBarBackgroundView: UIVisualEffectView!
/// Tab 按钮数组
private var tabButtons: [UIButton] = []
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
// 测试域名配置
#if DEBUG
APIConfig.testEncryption()
#endif
// 隐藏原生 TabBar
self.tabBar.isHidden = true
// 设置 delegate 以完全控制切换行为
self.delegate = self
// ✅ 启动时验证 ticket与 OC 版本保持一致)
performAutoLogin()
setupCustomFloatingTabBar()
setupInitialViewControllers()
NSLog("[EPTabBarController] 悬浮 TabBar 初始化完成")
}
deinit {
NSLog("[EPTabBarController] 已释放")
}
// MARK: - Setup
/// 设置自定义悬浮 TabBar
private func setupCustomFloatingTabBar() {
// 创建悬浮容器
customTabBarView = UIView()
customTabBarView.translatesAutoresizingMaskIntoConstraints = false
customTabBarView.backgroundColor = .clear
view.addSubview(customTabBarView)
// 液态玻璃/毛玻璃效果
let effect: UIVisualEffect
if #available(iOS 26.0, *) {
// iOS 26+ 使用液态玻璃Material
effect = UIGlassEffect()
} else {
// iOS 13-17 使用毛玻璃
effect = UIBlurEffect(style: .systemMaterial)
}
tabBarBackgroundView = UIVisualEffectView(effect: effect)
tabBarBackgroundView.translatesAutoresizingMaskIntoConstraints = false
tabBarBackgroundView.layer.cornerRadius = 28
tabBarBackgroundView.layer.masksToBounds = true
// 添加边框
tabBarBackgroundView.layer.borderWidth = 0.5
tabBarBackgroundView.layer.borderColor = UIColor.white.withAlphaComponent(0.2).cgColor
customTabBarView.addSubview(tabBarBackgroundView)
// 简化的布局约束(类似 Masonry 风格)
customTabBarView.snp.makeConstraints { make in
make.leading.equalTo(view).offset(16)
make.trailing.equalTo(view).offset(-16)
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-12)
make.height.equalTo(64)
}
tabBarBackgroundView.snp.makeConstraints { make in
make.edges.equalTo(customTabBarView)
}
// 添加 Tab 按钮
setupTabButtons()
NSLog("[EPTabBarController] 悬浮 TabBar 设置完成")
}
/// 设置 Tab 按钮
private func setupTabButtons() {
let momentButton = createTabButton(
normalImage: "tab_moment_off",
selectedImage: "tab_moment_on",
tag: 0
)
let mineButton = createTabButton(
normalImage: "tab_mine_off",
selectedImage: "tab_mine_on",
tag: 1
)
tabButtons = [momentButton, mineButton]
let stackView = UIStackView(arrangedSubviews: tabButtons)
stackView.axis = .horizontal
stackView.distribution = .fillEqually
stackView.spacing = 20
stackView.translatesAutoresizingMaskIntoConstraints = false
tabBarBackgroundView.contentView.addSubview(stackView)
stackView.snp.makeConstraints { make in
make.top.equalTo(tabBarBackgroundView).offset(8)
make.leading.equalTo(tabBarBackgroundView).offset(20)
make.trailing.equalTo(tabBarBackgroundView).offset(-20)
make.bottom.equalTo(tabBarBackgroundView).offset(-8)
}
// 默认选中第一个
updateTabButtonStates(selectedIndex: 0)
}
/// 创建 Tab 按钮
private func createTabButton(normalImage: String, selectedImage: String, tag: Int) -> UIButton {
let button = UIButton(type: .custom)
button.tag = tag
button.adjustsImageWhenHighlighted = false // 禁用高亮效果,避免闪烁
// 尝试设置自定义图片,如果不存在则使用 SF Symbols
if let normalImg = UIImage(named: normalImage), let selectedImg = UIImage(named: selectedImage) {
// 正确设置:分别为 normal 和 selected 状态设置图片
button.setImage(normalImg, for: .normal)
button.setImage(selectedImg, for: .selected)
} else {
// 使用 SF Symbols 作为备用
let fallbackIcons = ["sparkles", "person.circle"]
let iconName = fallbackIcons[tag]
let imageConfig = UIImage.SymbolConfiguration(pointSize: 24, weight: .medium)
let normalIcon = UIImage(systemName: iconName, withConfiguration: imageConfig)
button.setImage(normalIcon, for: .normal)
button.setImage(normalIcon, for: .selected)
button.tintColor = .white.withAlphaComponent(0.6)
}
// 图片渲染模式
button.imageView?.contentMode = .scaleAspectFit
// 移除标题
button.setTitle(nil, for: .normal)
button.setTitle(nil, for: .selected)
// 设置图片大小约束
button.imageView?.snp.makeConstraints { make in
make.size.equalTo(28)
}
button.addTarget(self, action: #selector(tabButtonTapped(_:)), for: .touchUpInside)
return button
}
/// Tab 按钮点击事件
@objc private func tabButtonTapped(_ sender: UIButton) {
let newIndex = sender.tag
// 如果点击的是当前已选中的 tab不做任何操作
if newIndex == selectedIndex {
return
}
// 先更新按钮状态
updateTabButtonStates(selectedIndex: newIndex)
// 禁用 UITabBarController 的默认切换动画,避免闪烁
UIView.performWithoutAnimation {
selectedIndex = newIndex
}
let tabNames = [YMLocalizedString("tab.moment"), YMLocalizedString("tab.mine")]
NSLog("[EPTabBarController] 选中 Tab: \(tabNames[newIndex])")
}
/// 更新 Tab 按钮状态
private func updateTabButtonStates(selectedIndex: Int) {
// 禁用按钮交互,避免快速点击
tabButtons.forEach { $0.isUserInteractionEnabled = false }
for (index, button) in tabButtons.enumerated() {
let isSelected = (index == selectedIndex)
// 直接设置 isSelected 属性即可,图片会自动切换
button.isSelected = isSelected
// SF Symbols 的情况需要手动更新 tintColor
if button.currentImage?.isSymbolImage == true {
button.tintColor = isSelected ? .white : .white.withAlphaComponent(0.6)
}
// 选中状态缩放动画
UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseOut], animations: {
button.transform = isSelected ? CGAffineTransform(scaleX: 1.1, y: 1.1) : .identity
})
}
// 延迟恢复按钮交互
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
self.tabButtons.forEach { $0.isUserInteractionEnabled = true }
}
}
/// 设置初始 ViewController未登录状态
private func setupInitialViewControllers() {
// TODO: 暂时使用空白页面占位
let blankVC1 = UIViewController()
blankVC1.view.backgroundColor = .white
blankVC1.tabBarItem = createTabBarItem(
title: YMLocalizedString("tab.moment"),
normalImage: "tab_moment_normal",
selectedImage: "tab_moment_selected"
)
let blankVC2 = UIViewController()
blankVC2.view.backgroundColor = .white
blankVC2.tabBarItem = createTabBarItem(
title: YMLocalizedString("tab.mine"),
normalImage: "tab_mine_normal",
selectedImage: "tab_mine_selected"
)
viewControllers = [blankVC1, blankVC2]
selectedIndex = 0
NSLog("[EPTabBarController] 初始 ViewControllers 设置完成")
}
/// 创建 TabBarItem
/// - Parameters:
/// - title: 标题
/// - normalImage: 未选中图标名称
/// - selectedImage: 选中图标名称
/// - Returns: UITabBarItem
private func createTabBarItem(title: String, normalImage: String, selectedImage: String) -> UITabBarItem {
let item = UITabBarItem(
title: title,
image: UIImage(named: normalImage)?.withRenderingMode(.alwaysOriginal),
selectedImage: UIImage(named: selectedImage)?.withRenderingMode(.alwaysOriginal)
)
return item
}
// MARK: - Public Methods
/// 登录成功后刷新 TabBar
/// - Parameter isLogin: 是否已登录
func refreshTabBar(isLogin: Bool) {
isLoggedIn = isLogin
if isLogin {
setupLoggedInViewControllers()
} else {
setupInitialViewControllers()
}
NSLog("[EPTabBarController] TabBar 已刷新,登录状态: \(isLogin)")
}
/// 设置登录后的 ViewControllers
private func setupLoggedInViewControllers() {
// 只在 viewControllers 为空或不是正确类型时才创建
if viewControllers?.count != 2 ||
!(viewControllers?[0] is UINavigationController) ||
!(viewControllers?[1] is UINavigationController) {
// 创建动态页
let momentVC = EPMomentViewController()
momentVC.title = YMLocalizedString("tab.moment")
let momentNav = createTransparentNavigationController(
rootViewController: momentVC,
tabTitle: YMLocalizedString("tab.moment"),
normalImage: "tab_moment_normal",
selectedImage: "tab_moment_selected"
)
// 创建我的页
let mineVC = EPMineViewController()
mineVC.title = YMLocalizedString("tab.mine")
let mineNav = createTransparentNavigationController(
rootViewController: mineVC,
tabTitle: YMLocalizedString("tab.mine"),
normalImage: "tab_mine_normal",
selectedImage: "tab_mine_selected"
)
viewControllers = [momentNav, mineNav]
NSLog("[EPTabBarController] 登录后 ViewControllers 创建完成 - Moment & Mine")
}
selectedIndex = 0
}
/// 创建透明导航控制器(统一配置)
/// - Parameters:
/// - rootViewController: 根视图控制器
/// - tabTitle: TabBar 标题
/// - normalImage: 未选中图标
/// - selectedImage: 选中图标
/// - Returns: 配置好的 UINavigationController
private func createTransparentNavigationController(
rootViewController: UIViewController,
tabTitle: String,
normalImage: String,
selectedImage: String
) -> UINavigationController {
let nav = UINavigationController(rootViewController: rootViewController)
nav.navigationBar.isTranslucent = true
nav.navigationBar.setBackgroundImage(UIImage(), for: .default)
nav.navigationBar.shadowImage = UIImage()
nav.view.backgroundColor = .clear
nav.tabBarItem = createTabBarItem(
title: tabTitle,
normalImage: normalImage,
selectedImage: selectedImage
)
// 设置 delegate 以监听页面切换
nav.delegate = self
return nav
}
// MARK: - TabBar Visibility Control
/// 显示悬浮 TabBar
private func showCustomTabBar(animated: Bool = true) {
guard customTabBarView.isHidden else { return }
if animated {
customTabBarView.isHidden = false
customTabBarView.alpha = 0
customTabBarView.transform = CGAffineTransform(translationX: 0, y: 20)
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut) {
self.customTabBarView.alpha = 1
self.customTabBarView.transform = .identity
}
} else {
customTabBarView.isHidden = false
customTabBarView.alpha = 1
}
}
/// 隐藏悬浮 TabBar
private func hideCustomTabBar(animated: Bool = true) {
guard !customTabBarView.isHidden else { return }
if animated {
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseIn, animations: {
self.customTabBarView.alpha = 0
self.customTabBarView.transform = CGAffineTransform(translationX: 0, y: 20)
}) { _ in
self.customTabBarView.isHidden = true
self.customTabBarView.transform = .identity
}
} else {
customTabBarView.isHidden = true
customTabBarView.alpha = 0
}
}
}
// MARK: - UITabBarControllerDelegate
extension EPTabBarController: UITabBarControllerDelegate {
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
NSLog("[EPTabBarController] 选中 Tab: \(item.title ?? "Unknown")")
}
/// 禁用系统默认的切换动画
func tabBarController(_ tabBarController: UITabBarController,
animationControllerForTransitionFrom fromVC: UIViewController,
to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
// 返回 nil 表示不使用动画
return nil
}
/// 完全控制是否允许切换
func tabBarController(_ tabBarController: UITabBarController,
shouldSelect viewController: UIViewController) -> Bool {
// 允许切换,但通过返回 nil 的 animationController 来禁用动画
return true
}
}
// MARK: - UINavigationControllerDelegate
extension EPTabBarController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController,
willShow viewController: UIViewController,
animated: Bool) {
// 判断是否是根页面(一级页面)
let isRootViewController = navigationController.viewControllers.count == 1
if isRootViewController {
// 一级页面:显示 TabBar
showCustomTabBar(animated: animated)
NSLog("[EPTabBarController] 显示 TabBar - 根页面")
} else {
// 二级及以上页面:隐藏 TabBar
hideCustomTabBar(animated: animated)
NSLog("[EPTabBarController] 隐藏 TabBar - 子页面 (层级: \(navigationController.viewControllers.count))")
}
}
}
// MARK: - Auto Login & Ticket Validation
extension EPTabBarController {
/// 自动登录:验证 ticket 有效性(与 OC MainPresenter.autoLogin 保持一致)
private func performAutoLogin() {
// 1. 检查账号信息
guard let accountModel = AccountInfoStorage.instance().getCurrentAccountInfo() else {
NSLog("[EPTabBarController] ⚠️ 账号信息不存在,跳转到登录页")
handleTokenInvalid()
return
}
// 2. 检查 uid 和 access_token
let uid = accountModel.uid
let accessToken = accountModel.access_token
guard !uid.isEmpty, !accessToken.isEmpty else {
NSLog("[EPTabBarController] ⚠️ uid 或 access_token 为空,跳转到登录页")
handleTokenInvalid()
return
}
// 3. 检查 ticket 是否已存在(内存缓存)
let existingTicket = AccountInfoStorage.instance().getTicket() ?? ""
if !existingTicket.isEmpty {
NSLog("[EPTabBarController] ✅ Ticket 已存在,自动登录成功")
return
}
// 4. Ticket 不存在,请求新的 ticket
NSLog("[EPTabBarController] 🔄 Ticket 不存在,正在请求...")
let loginService = EPLoginService()
loginService.requestTicket(accessToken: accessToken) { ticket in
NSLog("[EPTabBarController] ✅ Ticket 请求成功: \(ticket)")
AccountInfoStorage.instance().saveTicket(ticket)
} failure: { [weak self] code, msg in
NSLog("[EPTabBarController] ❌ Ticket 请求失败 (\(code)): \(msg)")
// ⚠️ Ticket 失败,强制退出登录(与 OC MainPresenter 保持一致)
DispatchQueue.main.async {
self?.handleTokenInvalid()
}
}
}
/// 处理 Token 失效:清空数据并跳转到登录页
private func handleTokenInvalid() {
NSLog("[EPTabBarController] ⚠️ Token 失效,清空账号数据...")
// 1. 清空账号信息
AccountInfoStorage.instance().saveAccountInfo(nil)
AccountInfoStorage.instance().saveTicket("")
// 2. 跳转到登录页
DispatchQueue.main.async {
let loginVC = EPLoginViewController()
let nav = BaseNavigationController(rootViewController: loginVC)
nav.modalPresentationStyle = .fullScreen
// 获取 keyWindowiOS 13+ 兼容)
if #available(iOS 13.0, *) {
for scene in UIApplication.shared.connectedScenes {
if let windowScene = scene as? UIWindowScene,
windowScene.activationState == .foregroundActive,
let window = windowScene.windows.first(where: { $0.isKeyWindow }) {
window.rootViewController = nav
window.makeKeyAndVisible()
break
}
}
} else {
if let window = UIApplication.shared.keyWindow {
window.rootViewController = nav
window.makeKeyAndVisible()
}
}
NSLog("[EPTabBarController] ✅ 已跳转到登录页")
}
}
}
// MARK: - OC Compatibility
extension EPTabBarController {
/// OC 兼容:创建实例的工厂方法
@objc static func create() -> EPTabBarController {
return EPTabBarController()
}
/// OC 兼容:刷新 TabBar 方法
@objc func refreshTabBarWithIsLogin(_ isLogin: Bool) {
refreshTabBar(isLogin: isLogin)
}
}

View File

@@ -1,42 +0,0 @@
//
// BuglyManagerExample.h
// YuMi
//
// Created by BuglyManager Example
// Copyright © 2024 YuMi. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "BuglyManager.h"
NS_ASSUME_NONNULL_BEGIN
/**
* BuglyManager 使用示例类
* 展示如何使用 BuglyManager 的各种功能
*/
@interface BuglyManagerExample : NSObject <BuglyManagerDelegate>
/**
* 设置 Bugly 代理
*/
- (void)setupBuglyDelegate;
/**
* 上报业务错误示例
*/
- (void)reportBusinessErrorExample;
/**
* 上报网络错误示例
*/
- (void)reportNetworkErrorExample;
/**
* 上报内购错误示例
*/
- (void)reportIAPErrorExample;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,79 +0,0 @@
//
// BuglyManagerExample.m
// YuMi
//
// Created by BuglyManager Example
// Copyright © 2024 YuMi. All rights reserved.
//
#import "BuglyManagerExample.h"
#import "BuglyManager.h"
@implementation BuglyManagerExample
#pragma mark - 使用示例
// 示例1设置代理监听卡顿
- (void)setupBuglyDelegate {
[BuglyManager sharedManager].delegate = self;
}
// 示例2上报业务错误
- (void)reportBusinessErrorExample {
NSDictionary *context = @{
@"page": @"HomePage",
@"action": @"loadData",
@"timestamp": @([[NSDate date] timeIntervalSince1970])
};
[[BuglyManager sharedManager] reportBusinessError:@"数据加载失败"
code:1001
context:context];
}
// 示例3上报网络错误
- (void)reportNetworkErrorExample {
NSDictionary *userInfo = @{
@"requestParams": @{@"userId": @"12345"},
@"responseData": @"服务器错误"
};
[[BuglyManager sharedManager] reportNetworkError:@"user123"
api:@"user/profile"
code:500
userInfo:userInfo];
}
// 示例4上报内购错误
- (void)reportIAPErrorExample {
NSDictionary *context = @{
@"retryCount": @3,
@"productId": @"com.yumi.coin100"
};
[[BuglyManager sharedManager] reportIAPError:@"user123"
transactionId:@"txn_123456"
orderId:@"order_789"
status:2
context:context];
}
#pragma mark - BuglyManagerDelegate
- (void)buglyManager:(BuglyManager *)manager didDetectLag:(NSTimeInterval)duration {
NSLog(@"[Example] 检测到卡顿,持续时间: %.2f 秒", duration);
// TODO: 在这里实现卡顿通知逻辑
// 1. 记录到本地日志
// 2. 发送本地通知
// 3. 上报到性能监控系统
// 4. 触发用户反馈机制
}
- (void)buglyManager:(BuglyManager *)manager didDetectBlock:(NSTimeInterval)duration {
NSLog(@"[Example] 检测到主线程阻塞,持续时间: %.2f 秒", duration);
// TODO: 在这里实现阻塞通知逻辑
}
@end

View File

@@ -1,24 +0,0 @@
//
// YMConstant.h
// YUMI
//
// Created by YUMI on 2021/9/13.
//
///项目配置常量
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface YUMIConstant : NSObject
/// 配置Key类型枚举仅保留实际使用的类型
typedef NS_ENUM(NSUInteger, Pi_KeyType) {
KeyType_Sign = 0, ///参数加密签名Key
};
/// 获取指定类型的配置值
NSString * const KeyWithType(Pi_KeyType type);
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,21 +0,0 @@
//
// YMConstant.m
// YUMI
//
// Created by YUMI on 2021/9/13.
//
#import "YUMIConstant.h"
@implementation YUMIConstant
/// 获取指定类型的配置值
NSString * const KeyWithType(Pi_KeyType type) {
// 参数加密签名Key用于API请求签名
if (type == KeyType_Sign) {
return @"rpbs6us1m8r2j9g6u06ff2bo18orwaya";
}
return @"";
}
@end

View File

@@ -1,26 +0,0 @@
//
// YMHtmlUrl.h
// YUMI
//
// Created by YUMI on 2021/9/13.
//
///放置h5的链接地址
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface YUMIHtmlUrl : NSObject
/// URL类型枚举仅保留实际使用的类型
typedef NS_ENUM(NSUInteger, URLType) {
kPrivacyURL = 0, ///隐私政策
kUserProtocalURL = 4, ///用户协议
kFAQURL = 6, ///帮助/常见问题
kCaptchaSwitchPath = 113, ///人机验证
};
NSString * const URLWithType(URLType type);
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,43 +0,0 @@
//
// YMHtmlUrl.m
// YUMI
//
// Created by YUMI on 2021/9/13.
//
#import "YUMIHtmlUrl.h"
#import "AESUtils.h"
@implementation YUMIHtmlUrl
NSString * const URLWithType(URLType type) {
NSString *prefix = @"eparty";
// 明文 URL 配置(无需加密的路径)
NSDictionary *plainUrls = @{
@(kCaptchaSwitchPath): @"modules/humanMachineVerification/index.html",
};
NSString *plainUrl = plainUrls[@(type)];
if (plainUrl) {
return [NSString stringWithFormat:@"%@/%@", prefix, plainUrl];
}
// 加密 URL 配置(需要 AES 解密的路径)
NSDictionary *encryptedUrls = @{
@(kPrivacyURL): @"sPa8x4YF1hFEeCeH5v+RMOulemxgjjZLbxkN8ZrBSM8=", // modules/rule/privacy-wap.html
@(kUserProtocalURL): @"0sBhBaRqf7oBlYvNK4azCrVPTFjv9FYF0A2v9+qkSxg=", // modules/rule/protocol.html
@(kFAQURL): @"k/Bqnh8nGkuhV8KhU6xN5a8EkxEQrbDMAWNBtaAdJCo=", // modules/rule/guide.html
};
NSString *encryptedUrl = encryptedUrls[@(type)];
if (encryptedUrl) {
NSString *decryptedPath = [AESUtils aesDecrypt:encryptedUrl];
return [NSString stringWithFormat:@"%@/%@", prefix, decryptedPath];
}
// 未找到对应的 URL 配置
return @"";
}
@end

View File

@@ -1,17 +0,0 @@
//
// YMEnum.h
// YUMI
//
// Created by YUMI on 2021/9/13.
//
///全局枚举定义(仅保留实际使用的类型)
#ifndef YUMINNNN_h
#define YUMINNNN_h
/// 短信验证码业务类型(仅保留实际使用的类型)
typedef NS_ENUM(NSUInteger, GetSmsType) {
GetSmsType_Regist = 1, ///注册
GetSmsType_Reset_Password = 3, ///重设密码
};
#endif /* YUMINNNN_h */

View File

@@ -1,117 +0,0 @@
//
// Api+Login.h
// YUMI
//
// Created by zu on 2021/9/6.
//
#import "Api.h"
NS_ASSUME_NONNULL_BEGIN
@interface Api (Login)
+ (void)phoneQuickLogin:(HttpRequestHelperCompletion)completion
accessToken:(NSString *)accessToken
token:(NSString *)token;
/// 手机验证码登录
/// @param completion 完成
/// @param phone 手机号
/// @param code 验证码
+ (void)loginWithCode:(HttpRequestHelperCompletion)completion
phone:(NSString *)phone
code:(NSString *)code
client_secret:(NSString *)client_secret
version:(NSString *)version
client_id:(NSString *)client_id
grant_type:(NSString *)grant_type
phoneAreaCode:(NSString *)phoneAreaCode;
/// 手机密码登录
/// @param completion 完成
/// @param phone 手机号
/// @param password 验证码
+ (void)loginWithPassword:(HttpRequestHelperCompletion)completion
phone:(NSString *)phone
password:(NSString *)password
client_secret:(NSString *)client_secret
version:(NSString *)version
client_id:(NSString *)client_id
grant_type:(NSString *)grant_type;
/// 充值密码 没有登录的时候
/// @param complction 完成
/// @param phone 手机号
/// @param newPwd 新的密码
/// @param smsCode 验证码
+ (void)resetPasswordWithPhone:(HttpRequestHelperCompletion)complction
phone:(NSString *)phone
newPwd:(NSString *)newPwd
smsCode:(NSString *)smsCode phoneAreaCode:(NSString *)phoneAreaCode;
/// 第三方登录
/// @param complction 完成
/// @param openid 唯一标识符
/// @param unionid unionid
/// @param access_token access_token
/// @param type 第三方登录的类型
+ (void)loginWithThirdPart:(HttpRequestHelperCompletion)complction
openid:(NSString *)openid
unionid:(NSString *)unionid
access_token:(NSString *)access_token
type:(NSString *)type;
/// 随机获取一个昵称
/// @param completion 完成
+ (void)randomNick:(HttpRequestHelperCompletion)completion;
/// 绑定手机号码
/// @param complection 完成
/// @param phone 手机号
/// @param code 验证码
/// @param ticket ticket
+ (void)bindMoblieCode:(HttpRequestHelperCompletion)complection
phone:(NSString *)phone
code:(NSString *)code
ticket:(NSString *)ticket phoneAreaCode:(NSString *)phoneAreaCode;
+(void)getPhoneAreaCodeList:(HttpRequestHelperCompletion)complection;
/// 绑定授权码
/// @param complection 完成
+(void)bindAuthorizationCode:(HttpRequestHelperCompletion)complection authCode:(NSString *)authCode;
///反馈
+ (void)loadFeedbackConfig:(HttpRequestHelperCompletion)completion;
+ (void)commitFeedback:(HttpRequestHelperCompletion)completion
type:(NSString *)type
desc:(NSString *)desc
screenUrl:(NSString *)screenUrl
contact:(NSString *)contact;
+ (void)emailGetCode:(HttpRequestHelperCompletion)completion emailAddress:(NSString *)emailAddress type:(NSNumber *)type;
+ (void)emailVerify:(HttpRequestHelperCompletion)completion emailAddress:(NSString *)emailAddress code:(NSString *)code;
+ (void)loginWithCode:(HttpRequestHelperCompletion)completion
email:(NSString *)email
code:(NSString *)code
client_secret:(NSString *)client_secret
version:(NSString *)version
client_id:(NSString *)client_id
grant_type:(NSString *)grant_type;
+ (void)userBoundEmail:(HttpRequestHelperCompletion)completion
email:(NSString *)email
code:(NSString *)code;
+ (void)userBoundPhone:(HttpRequestHelperCompletion)completion
phone:(NSString *)email
code:(NSString *)code
phoneAreaCode:(NSString *)phoneAreaCode;
+ (void)resetPasswordWithEmail:(HttpRequestHelperCompletion)completion email:(NSString *)email newPwd:(NSString *)newPwd code:(NSString *)code;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,149 +0,0 @@
//
// Api+Login.m
// YUMI
//
// Created by zu on 2021/9/6.
//
#import "Api+Login.h"
#import <Base64/MF_Base64Additions.h>
@implementation Api (Login)
+ (void)phoneQuickLogin:(HttpRequestHelperCompletion)completion accessToken:(NSString *)accessToken token:(NSString *)token {
NSString * fang = [NSString stringFromBase64String:@"YWNjL29uZWNsaWNrL2xvZ2lu"];///acc/oneclick/login
[self makeRequest:fang method:HttpRequestHelperMethodPOST completion:completion, __FUNCTION__, accessToken, token, nil];
}
/// 手机验证码登录
/// @param completion 完成
/// @param phone 手机号
/// @param code 验证码
+ (void)loginWithCode:(HttpRequestHelperCompletion)completion phone:(NSString *)phone code:(NSString *)code client_secret:(NSString *)client_secret version:(NSString *)version client_id:(NSString *)client_id grant_type:(NSString *)grant_type phoneAreaCode:(NSString *)phoneAreaCode{
NSString * fang = [NSString stringFromBase64String:@"b2F1dGgvdG9rZW4="];///oauth/token
[self makeRequest:fang method:HttpRequestHelperMethodPOST completion:completion, __FUNCTION__,phone,code,client_secret,version, client_id, grant_type,phoneAreaCode, nil];
}
/// 手机密码登录
/// @param completion 完成
/// @param phone 手机号
/// @param password 验证码
+ (void)loginWithPassword:(HttpRequestHelperCompletion)completion phone:(NSString *)phone password:(NSString *)password client_secret:(NSString *)client_secret version:(NSString *)version client_id:(NSString *)client_id grant_type:(NSString *)grant_type {
[self makeRequest:@"oauth/token" method:HttpRequestHelperMethodPOST completion:completion, __FUNCTION__,phone,password,client_secret,version, client_id, grant_type, nil];
}
/// 重置手机号登录密码
/// @param completion 完成
/// @param phone 手机号
/// @param newPwd 新的密码
/// @param smsCode 验证码
+ (void)resetPasswordWithPhone:(HttpRequestHelperCompletion)completion phone:(NSString *)phone newPwd:(NSString *)newPwd smsCode:(NSString *)smsCode phoneAreaCode:(NSString *)phoneAreaCode{
NSString * fang = [NSString stringFromBase64String:@"YWNjL3B3ZC9yZXNldA=="];/// acc/pwd/reset
[self makeRequest:fang method:HttpRequestHelperMethodPOST completion:completion, __FUNCTION__, phone, newPwd, smsCode,phoneAreaCode, nil];
}
+ (void)resetPasswordWithEmail:(HttpRequestHelperCompletion)completion email:(NSString *)email newPwd:(NSString *)newPwd code:(NSString *)code{
[self makeRequest:@"acc/pwd/resetByEmail" method:HttpRequestHelperMethodPOST completion:completion, __FUNCTION__, email, newPwd, code, nil];
}
/// 第三方登录
/// @param completion 完成
/// @param openid 唯一标识符
/// @param unionid unionid
/// @param access_token access_token
/// @param type 第三方登录的类型
+ (void)loginWithThirdPart:(HttpRequestHelperCompletion)completion openid:(NSString *)openid unionid:(NSString *)unionid access_token:(NSString *)access_token type:(NSString *)type {
NSString * fang = [NSString stringFromBase64String:@"YWNjL3RoaXJkL2xvZ2lu"];///acc/third/login
[self makeRequest:fang method:HttpRequestHelperMethodPOST completion:completion, __FUNCTION__, openid, unionid, access_token, type, nil];
}
/// 随机获取一个昵称
/// @param completion 完成
+ (void)randomNick:(HttpRequestHelperCompletion)completion {
NSString * fang = [NSString stringFromBase64String:@"cmFuZG9tL25pY2svZ2V0"];///random/nick/get
[self makeRequest:fang method:HttpRequestHelperMethodPOST completion:completion, __FUNCTION__, nil];
}
/// 绑定手机号码
/// @param complection 完成
/// @param phone 手机号
/// @param code 验证码
/// @param ticket ticket
+ (void)bindMoblieCode:(HttpRequestHelperCompletion)complection
phone:(NSString *)phone
code:(NSString *)code
ticket:(NSString *)ticket phoneAreaCode:(NSString *)phoneAreaCode{
NSString * fang = [NSString stringFromBase64String:@"d2l0aERyYXcvcGhvbmU="];///withDraw/phone
[self makeRequest:fang method:HttpRequestHelperMethodPOST completion:complection, __FUNCTION__, phone, code, ticket,phoneAreaCode, nil];
}
+(void)getPhoneAreaCodeList:(HttpRequestHelperCompletion)complection{
NSString * fang = [NSString stringFromBase64String:@"YXJlYUluZm8vbGlzdA=="];///areaInfo/list
[self makeRequest:fang method:HttpRequestHelperMethodGET completion:complection, __FUNCTION__,nil];
}
/// 绑定授权码
/// @param complection 完成
+(void)bindAuthorizationCode:(HttpRequestHelperCompletion)complection authCode:(NSString *)authCode{
[self makeRequest:@"phone/auth/bound" method:HttpRequestHelperMethodPOST completion:complection, __FUNCTION__,authCode, nil];
}
///反馈
+ (void)loadFeedbackConfig:(HttpRequestHelperCompletion)completion {
[self makeRequest:@"feedback/getConfig" method:HttpRequestHelperMethodGET completion:completion, __FUNCTION__, nil];
}
+ (void)commitFeedback:(HttpRequestHelperCompletion)completion
type:(NSString *)type
desc:(NSString *)desc
screenUrl:(NSString *)screenUrl
contact:(NSString *)contact {
[self makeRequest:@"feedback/commit"
method:HttpRequestHelperMethodPOST
completion:completion, __FUNCTION__, type, desc, screenUrl, contact, nil];
}
+ (void)emailGetCode:(HttpRequestHelperCompletion)completion emailAddress:(NSString *)emailAddress type:(NSNumber *)type {
[self makeRequest:@"email/getCode"
method:HttpRequestHelperMethodPOST
completion:completion, __FUNCTION__, emailAddress, type, nil];
}
+ (void)emailVerify:(HttpRequestHelperCompletion)completion emailAddress:(NSString *)emailAddress code:(NSString *)code {
[self makeRequest:@"email/verify"
method:HttpRequestHelperMethodPOST
completion:completion, __FUNCTION__, emailAddress, code, nil];
}
+ (void)loginWithCode:(HttpRequestHelperCompletion)completion
email:(NSString *)email
code:(NSString *)code
client_secret:(NSString *)client_secret
version:(NSString *)version
client_id:(NSString *)client_id
grant_type:(NSString *)grant_type {
NSString * fang = [NSString stringFromBase64String:@"b2F1dGgvdG9rZW4="];///oauth/token
[self makeRequest:fang method:HttpRequestHelperMethodPOST completion:completion, __FUNCTION__,email,code,client_secret,version, client_id, grant_type, nil];
}
+ (void)userBoundEmail:(HttpRequestHelperCompletion)completion
email:(NSString *)email
code:(NSString *)code {
[self makeRequest:@"user/boundEmail"
method:HttpRequestHelperMethodPOST
completion:completion, __FUNCTION__, email, code, nil];
}
+ (void)userBoundPhone:(HttpRequestHelperCompletion)completion
phone:(NSString *)email
code:(NSString *)code
phoneAreaCode:(NSString *)phoneAreaCode {
[self makeRequest:@"user/boundPhone"
method:HttpRequestHelperMethodPOST
completion:completion, __FUNCTION__, email, code, nil];
}
@end

View File

@@ -1,17 +0,0 @@
//
// PIUserSexView.h
// YuMi
//
// Created by duoban on 2023/8/14.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface PIUserSexView : UIView
@property (nonatomic,assign) NSInteger gender; /// 性别 1:男 2:女
@property (nonatomic,assign) BOOL selected;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,127 +0,0 @@
//
// PIUserSexView.m
// YuMi
//
// Created by duoban on 2023/8/14.
//
#import "PIUserSexView.h"
@interface PIUserSexView ()
///背景
@property (nonatomic,strong) UIImageView *backImageView;
///头像
@property (nonatomic,strong) UIImageView *logoImageView;
///性别图标
@property (nonatomic,strong) UIImageView *sexImageView;
///性别
@property (nonatomic,strong) UILabel *sexLb;
@end
@implementation PIUserSexView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self initSubViews];
[self initSubViewConstraints];
}
return self;
}
- (void)initSubViews {
[self addSubview:self.backImageView];
[self.backImageView addSubview:self.logoImageView];
[self.backImageView addSubview:self.sexLb];
[self.backImageView addSubview:self.sexImageView];
}
- (void)initSubViewConstraints {
[self.backImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self);
}];
[self.logoImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(kGetScaleWidth(66), kGetScaleWidth(66)));
make.centerX.mas_equalTo(self.backImageView);
make.top.mas_equalTo(self.backImageView).offset(kGetScaleWidth(8));
}];
[self.sexImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(kGetScaleWidth(16), kGetScaleWidth(16)));
make.trailing.mas_equalTo(self.backImageView.mas_centerX).offset(-1.5);
make.top.mas_equalTo(self.logoImageView.mas_bottom).offset(kGetScaleWidth(8));
}];
[self.sexLb mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.mas_equalTo(self.backImageView.mas_centerX).offset(1.5);
make.centerY.mas_equalTo(self.sexImageView);
}];
}
- (void)setGender:(NSInteger)gender {
_gender = gender;
if (_gender == 1) { // 男性
self.logoImageView.image = [UIImage imageNamed:@"login_full_male_logo"];
self.sexLb.text = YMLocalizedString(@"PIUserSexView0");
self.sexImageView.image = [UIImage imageNamed:@"login_full_male"];
} else if (_gender == 2) { // 女性
self.logoImageView.image = [UIImage imageNamed:@"login_full_female_logo"];
self.sexLb.text = YMLocalizedString(@"PIUserSexView1");
self.sexImageView.image = [UIImage imageNamed:@"login_full_female"];
}
}
- (void)setSelected:(BOOL)selected {
_selected = selected;
if (_selected) {
if (self.gender == 1) { // 男性
self.backImageView.image = [UIImage gradientColorImageFromColors:@[[DJDKMIMOMColor colorWithHexString:@"#EBF5FF"],[DJDKMIMOMColor colorWithHexString:@"#FFFFFF"]] gradientType:GradientTypeTopToBottom imgSize:CGSizeMake(10, 10)];
self.backImageView.layer.borderColor = [DJDKMIMOMColor colorWithHexString:@"#B8E5FF"].CGColor;
} else { // 女性
self.backImageView.image = [UIImage gradientColorImageFromColors:@[[DJDKMIMOMColor colorWithHexString:@"#FFEBFA"],[DJDKMIMOMColor colorWithHexString:@"#FFFFFF"]] gradientType:GradientTypeTopToBottom imgSize:CGSizeMake(10, 10)];
self.backImageView.layer.borderColor = [DJDKMIMOMColor colorWithHexString:@"#FFB8E2"].CGColor;
}
} else {
self.backImageView.image = [UIImage imageWithColor:[UIColor clearColor]];
self.backImageView.layer.borderColor = [DJDKMIMOMColor colorWithHexString:@"#FAFBFC"].CGColor;
}
}
- (UIImageView *)backImageView {
if (!_backImageView) {
_backImageView = [[UIImageView alloc] init];
_backImageView.userInteractionEnabled = YES;
_backImageView.layer.masksToBounds = YES;
_backImageView.layer.borderWidth = 1;
_backImageView.layer.cornerRadius = kGetScaleWidth(18);
}
return _backImageView;
}
- (UIImageView *)logoImageView {
if (!_logoImageView) {
_logoImageView = [[UIImageView alloc] init];
_logoImageView.userInteractionEnabled = YES;
}
return _logoImageView;
}
- (UIImageView *)sexImageView {
if (!_sexImageView) {
_sexImageView = [[UIImageView alloc] init];
_sexImageView.userInteractionEnabled = YES;
}
return _sexImageView;
}
- (UILabel *)sexLb {
if (!_sexLb) {
_sexLb = [[UILabel alloc] init];
_sexLb.font = kFontMedium(14);
_sexLb.textColor = [DJDKMIMOMColor colorWithHexString:@"#1F1A4E"];
}
return _sexLb;
}
@end

View File

@@ -1,16 +0,0 @@
//
// YMForgetPwdViewController.h
// YUMI
//
// Created by XY on 2023/2/14.
//
#import "MvpViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface XPForgetPwdViewController : MvpViewController
@end
NS_ASSUME_NONNULL_END

Some files were not shown because too many files have changed in this diff Show More