feat: 更新项目配置和功能模块

- 修改Package.swift以支持iOS 15和macOS 12。
- 更新swift-tca-architecture-guidelines.mdc中的alwaysApply设置为false。
- 注释掉AppDelegate中的NIMSDK导入,移除不再使用的NIMConfigurationManager和NIMSessionManager文件。
- 添加新的API相关文件,包括EMailLoginFeature、IDLoginFeature和相关视图,增强登录功能。
- 更新APIConstants和APIEndpoints以反映新的API路径。
- 添加本地化支持文件,包含英文和中文简体的本地化字符串。
- 新增字体管理和安全工具类,支持AES和DES加密。
- 更新Xcode项目配置,调整版本号和启动画面设置。
This commit is contained in:
edwinQQQ
2025-07-09 16:14:19 +08:00
parent 5926906f3c
commit c470dba79c
71 changed files with 4000 additions and 522 deletions

View File

@@ -15,7 +15,7 @@
| 环境 | 地址 | 说明 |
|------|------|------|
| 生产环境 | `https://api.hfighting.com` | 正式服务器 |
| 生产环境 | `https://api.epartylive.com` | 正式服务器 |
| 测试环境 | `http://beta.api.molistar.xyz` | 开发测试服务器 |
| 图片服务 | `https://image.hfighting.com` | 静态资源服务器 |
@@ -177,4 +177,4 @@ YuMi iOS 项目的 API 架构设计了完整的网络请求体系,包含:
- 🛠️ **开发支持**: 环境切换、错误追踪、调试日志
- 🏗️ **架构清晰**: 模块化设计、统一管理、易于维护
这种设计确保了 API 请求的安全性、稳定性和高性能,为应用提供了可靠的网络服务基础。
这种设计确保了 API 请求的安全性、稳定性和高性能,为应用提供了可靠的网络服务基础。

View File

@@ -3,17 +3,13 @@ import Foundation
/// API
///
/// API
/// -
/// -
/// - API
/// -
///
/// APIConfiguration
/// baseURLAppConfig
/// APIConfiguration
enum APIConstants {
// MARK: - Base URLs
///
static let baseURL = "http://beta.api.molistar.xyz"
// MARK: - Common Headers
///
@@ -34,7 +30,7 @@ enum APIConstants {
///
static let clientInit = "/client/init"
///
static let login = "/user/login"
static let login = "/oauth/token"
}
// MARK: - Common Parameters

View File

@@ -16,8 +16,11 @@ import Foundation
enum APIEndpoint: String, CaseIterable {
case config = "/client/config"
case configInit = "/client/init"
case login = "/auth/login"
//
case login = "/oauth/token"
case ticket = "/oauth/ticket"
// Web
case userAgreement = "/modules/rule/protocol.html"
case privacyPolicy = "/modules/rule/privacy-wap.html"
var path: String {
return self.rawValue
@@ -39,10 +42,38 @@ enum APIEndpoint: String, CaseIterable {
/// -
/// -
struct APIConfiguration {
static let baseURL = "http://beta.api.molistar.xyz"
static var baseURL: String { AppConfig.baseURL }
static let timeout: TimeInterval = 30.0
static let maxDataSize: Int = 50 * 1024 * 1024 // 50MB
/// URL
/// - Parameter endpoint: API
/// - Returns: URL
static func fullURL(for endpoint: APIEndpoint) -> String {
return baseURL + endpoint.path
}
/// URL
/// - Parameter endpoint: API
/// - Returns: URL nil
static func url(for endpoint: APIEndpoint) -> URL? {
return URL(string: fullURL(for: endpoint))
}
/// Web URL
/// - Parameter endpoint: API
/// - Returns: Web URL
static func fullWebURL(for endpoint: APIEndpoint) -> String {
return baseURL + AppConfig.webPathPrefix + endpoint.path
}
/// Web URL
/// - Parameter endpoint: API
/// - Returns: Web URL nil
static func webURL(for endpoint: APIEndpoint) -> URL? {
return URL(string: fullWebURL(for: endpoint))
}
///
///
/// API
@@ -58,7 +89,8 @@ struct APIConfiguration {
"Accept": "application/json",
"Accept-Encoding": "gzip, br",
"Accept-Language": Locale.current.languageCode ?? "en",
"App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
"App-Version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0",
"User-Agent": "YuMi/20.20.61 (iPhone; iOS 16.4; Scale/2.00)"
]
// headers

View File

@@ -118,7 +118,7 @@ struct BaseRequest: Codable {
self.appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
//
self.app = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? "yana"
self.app = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? "eparty"
// WiFi=2, =1
self.netType = NetworkTypeDetector.getCurrentNetworkType()
@@ -131,7 +131,7 @@ struct BaseRequest: Codable {
//
#if DEBUG
self.channel = "TestFlight"
self.channel = "molistar_enterprise"
#else
self.channel = "appstore"
#endif
@@ -186,9 +186,10 @@ struct BaseRequest: Codable {
}
// 3. key
// "key0=value0&key1=value1&key2=value2"
let sortedKeys = filteredParams.keys.sorted()
let paramString = sortedKeys.map { key in
"\(key)=\(filteredParams[key] ?? "")"
"\(key)=\(String(describing: filteredParams[key] ?? ""))"
}.joined(separator: "&")
// 4.
@@ -205,7 +206,7 @@ struct NetworkTypeDetector {
static func getCurrentNetworkType() -> Int {
// WiFi = 2, = 1
//
return 1 //
return 2 //
}
}
@@ -224,16 +225,136 @@ struct CarrierInfoManager {
// MARK: - User Info Manager (for Headers)
struct UserInfoManager {
static func getCurrentUserId() -> String? {
// ID
// AccountInfoStorage
return nil
private static let userDefaults = UserDefaults.standard
// MARK: - Storage Keys
private enum StorageKeys {
static let userId = "user_id"
static let accessToken = "access_token"
static let ticket = "user_ticket"
static let userInfo = "user_info"
}
// MARK: - User ID Management
static func getCurrentUserId() -> String? {
return userDefaults.string(forKey: StorageKeys.userId)
}
static func saveUserId(_ userId: String) {
userDefaults.set(userId, forKey: StorageKeys.userId)
userDefaults.synchronize()
print("💾 保存用户ID: \(userId)")
}
// MARK: - Access Token Management
static func getAccessToken() -> String? {
return userDefaults.string(forKey: StorageKeys.accessToken)
}
static func saveAccessToken(_ accessToken: String) {
userDefaults.set(accessToken, forKey: StorageKeys.accessToken)
userDefaults.synchronize()
print("💾 保存 Access Token")
}
// MARK: - Ticket Management ()
private static var currentTicket: String?
static func getCurrentUserTicket() -> String? {
//
// AccountInfoStorage
return nil
return currentTicket
}
static func saveTicket(_ ticket: String) {
currentTicket = ticket
print("💾 保存 Ticket 到内存")
}
static func clearTicket() {
currentTicket = nil
print("🗑️ 清除 Ticket")
}
// MARK: - User Info Management
static func saveUserInfo(_ userInfo: UserInfo) {
do {
let data = try JSONEncoder().encode(userInfo)
userDefaults.set(data, forKey: StorageKeys.userInfo)
userDefaults.synchronize()
// ID
if let userId = userInfo.userId {
saveUserId(userId)
}
print("💾 保存用户信息成功")
} catch {
print("❌ 保存用户信息失败: \(error)")
}
}
static func getUserInfo() -> UserInfo? {
guard let data = userDefaults.data(forKey: StorageKeys.userInfo) else {
return nil
}
do {
return try JSONDecoder().decode(UserInfo.self, from: data)
} catch {
print("❌ 解析用户信息失败: \(error)")
return nil
}
}
// MARK: - Complete Authentication Data Management
/// OAuth Token + Ticket +
static func saveCompleteAuthenticationData(
accessToken: String,
ticket: String,
uid: Int?, // String?Int?
userInfo: UserInfo?
) {
saveAccessToken(accessToken)
saveTicket(ticket)
if let uid = uid {
saveUserId("\(uid)") //
}
if let userInfo = userInfo {
saveUserInfo(userInfo)
}
print("✅ 完整认证信息保存成功")
}
///
static func hasValidAuthentication() -> Bool {
return getAccessToken() != nil && getCurrentUserTicket() != nil
}
///
static func clearAllAuthenticationData() {
userDefaults.removeObject(forKey: StorageKeys.userId)
userDefaults.removeObject(forKey: StorageKeys.accessToken)
userDefaults.removeObject(forKey: StorageKeys.userInfo)
clearTicket()
userDefaults.synchronize()
print("🗑️ 清除所有认证信息")
}
/// Ticket
static func restoreTicketIfNeeded() async -> Bool {
guard let accessToken = getAccessToken(),
getCurrentUserTicket() == nil else {
return false
}
print("🔄 尝试使用 Access Token 恢复 Ticket...")
// APIService false
// TicketHelper.createTicketRequest
return false
}
}
@@ -267,6 +388,7 @@ protocol APIRequestProtocol {
var queryParameters: [String: String]? { get }
var bodyParameters: [String: Any]? { get }
var headers: [String: String]? { get }
var customHeaders: [String: String]? { get } //
var timeout: TimeInterval { get }
var includeBaseParameters: Bool { get }
}
@@ -275,6 +397,7 @@ extension APIRequestProtocol {
var timeout: TimeInterval { 30.0 }
var includeBaseParameters: Bool { true }
var headers: [String: String]? { nil }
var customHeaders: [String: String]? { nil } //
}
// MARK: - Generic API Response
@@ -285,19 +408,5 @@ struct APIResponse<T: Codable>: Codable {
let code: Int?
}
// MARK: - String MD5 Extension
extension String {
func md5() -> String {
let data = Data(self.utf8)
let hash = data.withUnsafeBytes { bytes -> [UInt8] in
var hash = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
CC_MD5(bytes.baseAddress, CC_LONG(data.count), &hash)
return hash
}
return hash.map { String(format: "%02x", $0) }.joined()
}
}
// CommonCrypto
import CommonCrypto
// String+MD5 Utils/Extensions/String+MD5.swift

View File

@@ -93,6 +93,11 @@ struct LiveAPIService: APIServiceProtocol {
headers.merge(customHeaders) { _, new in new }
}
//
if let additionalHeaders = request.customHeaders {
headers.merge(additionalHeaders) { _, new in new }
}
for (key, value) in headers {
urlRequest.setValue(value, forHTTPHeaderField: key)
}

236
yana/APIs/LoginModels.swift Normal file
View File

@@ -0,0 +1,236 @@
import Foundation
// MARK: - ID Login Request Model
struct IDLoginAPIRequest: APIRequestProtocol {
typealias Response = IDLoginResponse
let endpoint = APIEndpoint.login.path // 使
let method: HTTPMethod = .POST
let includeBaseParameters = true
let queryParameters: [String: String]?
let bodyParameters: [String: Any]? = nil
let timeout: TimeInterval = 30.0
/// ID
/// - Parameters:
/// - phone: DESID/
/// - password: DES
/// - clientSecret: "uyzjdhds"
/// - version: "1"
/// - clientId: ID"erban-client"
/// - grantType: "password"
init(phone: String, password: String, clientSecret: String = "uyzjdhds", version: String = "1", clientId: String = "erban-client", grantType: String = "password") {
self.queryParameters = [
"phone": phone,
"password": password,
"client_secret": clientSecret,
"version": version,
"client_id": clientId,
"grant_type": grantType
];
// self.bodyParameters = [
// "phone": phone,
// "password": password,
// "client_secret": clientSecret,
// "version": version,
// "client_id": clientId,
// "grant_type": grantType
// ]
}
}
// MARK: - ID Login Response Model
struct IDLoginResponse: Codable, Equatable {
let status: String?
let message: String?
let code: Int?
let data: IDLoginData?
///
var isSuccess: Bool {
return code == 200 || status?.lowercased() == "success"
}
///
var errorMessage: String {
return message ?? "登录失败,请重试"
}
}
// MARK: - ID Login Data Model
struct IDLoginData: Codable, Equatable {
let accessToken: String?
let refreshToken: String?
let tokenType: String?
let expiresIn: Int?
let scope: String?
let userInfo: UserInfo?
let uid: Int? // String?Int?API
let netEaseToken: String? // token
let jti: String? // JWT token identifier
enum CodingKeys: String, CodingKey {
case accessToken = "access_token"
case refreshToken = "refresh_token"
case tokenType = "token_type"
case expiresIn = "expires_in"
case scope
case userInfo = "user_info"
case uid
case netEaseToken
case jti
}
}
// MARK: - User Info Model
struct UserInfo: Codable, Equatable {
let userId: String?
let username: String?
let nickname: String?
let avatar: String?
let email: String?
let phone: String?
let status: String?
let createTime: String?
let updateTime: String?
enum CodingKeys: String, CodingKey {
case userId = "user_id"
case username
case nickname
case avatar
case email
case phone
case status
case createTime = "create_time"
case updateTime = "update_time"
}
}
// MARK: - Login Helper
struct LoginHelper {
/// ID
/// DES
/// - Parameters:
/// - userID: ID
/// - password:
/// - Returns: APInil
static func createIDLoginRequest(userID: String, password: String) -> IDLoginAPIRequest? {
// 使DESID
let encryptionKey = "1ea53d260ecf11e7b56e00163e046a26"
guard let encryptedID = DESEncrypt.encryptUseDES(userID, key: encryptionKey),
let encryptedPassword = DESEncrypt.encryptUseDES(password, key: encryptionKey) else {
print("❌ DES加密失败")
return nil
}
print("🔐 DES加密成功")
print(" 原始ID: \(userID)")
print(" 加密后ID: \(encryptedID)")
print(" 原始密码: \(password)")
print(" 加密后密码: \(encryptedPassword)")
return IDLoginAPIRequest(
phone: userID,
password: encryptedPassword
)
}
}
// MARK: - Ticket API Models
/// Ticket
struct TicketAPIRequest: APIRequestProtocol {
typealias Response = TicketResponse
let endpoint = "/oauth/ticket"
let method: HTTPMethod = .POST
let includeBaseParameters = true
let queryParameters: [String: String]?
let bodyParameters: [String: Any]? = nil
let timeout: TimeInterval = 30.0
let customHeaders: [String: String]?
/// Ticket
/// - Parameters:
/// - accessToken: OAuth 访
/// - issueType: "multi"
/// - uid:
init(accessToken: String, issueType: String = "multi", uid: Int? = nil) {
self.queryParameters = [
"access_token": accessToken,
"issue_type": issueType
]
//
var headers: [String: String] = [:]
if let uid = uid {
headers["pub_uid"] = "\(uid)" //
}
self.customHeaders = headers.isEmpty ? nil : headers
}
}
/// Ticket
struct TicketResponse: Codable, Equatable {
let code: Int?
let message: String?
let data: TicketData?
///
var isSuccess: Bool {
return code == 200
}
///
var errorMessage: String {
return message ?? "Ticket 获取失败,请重试"
}
/// Ticket
var ticket: String? {
return data?.tickets?.first?.ticket
}
}
/// Ticket
struct TicketData: Codable, Equatable {
let tickets: [TicketInfo]?
}
/// Ticket
struct TicketInfo: Codable, Equatable {
let ticket: String?
}
// MARK: - Ticket Helper
struct TicketHelper {
/// Ticket
/// - Parameters:
/// - accessToken: OAuth 访
/// - uid:
/// - Returns: Ticket API
static func createTicketRequest(accessToken: String, uid: Int?) -> TicketAPIRequest {
return TicketAPIRequest(accessToken: accessToken, uid: uid)
}
/// Ticket
/// - Parameters:
/// - accessToken: OAuth 访
/// - uid:
static func debugTicketRequest(accessToken: String, uid: Int?) {
print("🎫 Ticket 请求调试信息")
print(" AccessToken: \(accessToken)")
print(" UID: \(uid?.description ?? "nil")")
print(" Endpoint: /oauth/ticket")
print(" Method: POST")
print(" Headers: pub_uid = \(uid?.description ?? "nil")")
print(" Parameters: access_token=\(accessToken), issue_type=multi")
}
}
// MARK: - LoginResponse
typealias LoginResponse = IDLoginResponse

262
yana/APIs/oauth flow.md Normal file
View File

@@ -0,0 +1,262 @@
# OAuth/Ticket 认证系统 API 文档
## 概述
本文档描述了 YuMi 应用中 OAuth 认证和 Ticket 会话管理的完整流程。系统采用两阶段认证机制:
1. **OAuth 阶段**:用户登录获取 `access_token`
2. **Ticket 阶段**:使用 `access_token` 获取业务会话 `ticket`
## 认证流程架构
### 核心组件
- **AccountInfoStorage**: 负责账户信息和 ticket 的本地存储
- **HttpRequestHelper**: 网络请求管理,自动添加认证头
- **Api+Login**: 登录相关 API 接口
- **Api+Main**: Ticket 获取相关 API 接口
### 认证数据模型
#### AccountModel
```objc
@interface AccountModel : PIBaseModel
@property (nonatomic, assign) NSString *uid; // 用户唯一标识
@property (nonatomic, copy) NSString *jti; // JWT ID
@property (nonatomic, copy) NSString *token_type; // Token 类型
@property (nonatomic, copy) NSString *refresh_token; // 刷新令牌
@property (nonatomic, copy) NSString *netEaseToken; // 网易云信令牌
@property (nonatomic, copy) NSString *access_token; // OAuth 访问令牌
@property (nonatomic, assign) NSNumber *expires_in; // 过期时间
@end
```
## API 接口详情
### 1. OAuth 登录接口
#### 1.1 手机验证码登录
```objc
+ (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;
```
**接口路径**: `POST /oauth/token`
**请求参数**:
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| phone | String | 是 | 手机号DES加密 |
| code | String | 是 | 验证码 |
| client_secret | String | 是 | 客户端密钥,固定值:"uyzjdhds" |
| version | String | 是 | 版本号,固定值:"1" |
| client_id | String | 是 | 客户端ID固定值"erban-client" |
| grant_type | String | 是 | 授权类型,验证码登录为:"sms_code" |
| phoneAreaCode | String | 是 | 手机区号 |
**返回数据**: AccountModel 对象
#### 1.2 手机密码登录
```objc
+ (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;
```
**接口路径**: `POST /oauth/token`
**请求参数**:
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| phone | String | 是 | 手机号DES加密 |
| password | String | 是 | 密码DES加密 |
| client_secret | String | 是 | 客户端密钥 |
| version | String | 是 | 版本号 |
| client_id | String | 是 | 客户端ID |
| grant_type | String | 是 | 授权类型,密码登录为:"password" |
#### 1.3 第三方登录
```objc
+ (void)loginWithThirdPart:(HttpRequestHelperCompletion)completion
openid:(NSString *)openid
unionid:(NSString *)unionid
access_token:(NSString *)access_token
type:(NSString *)type;
```
**接口路径**: `POST /acc/third/login`
**请求参数**:
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| openid | String | 是 | 第三方平台用户唯一标识 |
| unionid | String | 是 | 第三方平台联合ID |
| access_token | String | 是 | 第三方平台访问令牌 |
| type | String | 是 | 第三方平台类型1:Apple, 2:Facebook, 3:Google等 |
### 2. Ticket 获取接口
#### 2.1 获取 Ticket
```objc
+ (void)requestTicket:(HttpRequestHelperCompletion)completion
access_token:(NSString *)accessToken
issue_type:(NSString *)issueType;
```
**接口路径**: `POST /oauth/ticket`
**请求参数**:
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| access_token | String | 是 | OAuth 登录获取的访问令牌 |
| issue_type | String | 是 | 签发类型,固定值:"multi" |
**返回数据**:
```json
{
"code": 200,
"data": {
"tickets": [
{
"ticket": "eyJhbGciOiJIUzI1NiJ9..."
}
]
}
}
```
### 3. HTTP 请求头配置
所有业务 API 请求都会自动添加以下请求头:
```objc
// 在 HttpRequestHelper 中自动配置
- (void)setupHeader {
AFHTTPSessionManager *client = [HttpRequestHelper requestManager];
// 用户ID头
if ([[AccountInfoStorage instance] getUid].length > 0) {
[client.requestSerializer setValue:[[AccountInfoStorage instance] getUid]
forHTTPHeaderField:@"pub_uid"];
}
// Ticket 认证头
if ([[AccountInfoStorage instance] getTicket].length > 0) {
[client.requestSerializer setValue:[[AccountInfoStorage instance] getTicket]
forHTTPHeaderField:@"pub_ticket"];
}
// 其他公共头
[client.requestSerializer setValue:[NSBundle uploadLanguageText]
forHTTPHeaderField:@"Accept-Language"];
[client.requestSerializer setValue:PI_App_Version
forHTTPHeaderField:@"App-Version"];
}
```
## 使用流程
### 完整登录流程示例
```objc
// 1. 用户登录获取 access_token
[Api loginWithCode:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
if (code == 200) {
// 保存账户信息
AccountModel *accountModel = [AccountModel modelWithDictionary:data.data];
[[AccountInfoStorage instance] saveAccountInfo:accountModel];
// 2. 使用 access_token 获取 ticket
[Api requestTicket:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
if (code == 200) {
NSArray *tickets = [data.data valueForKey:@"tickets"];
NSString *ticket = [tickets[0] valueForKey:@"ticket"];
// 保存 ticket
[[AccountInfoStorage instance] saveTicket:ticket];
// 3. 登录成功,可以进行业务操作
[self navigateToMainPage];
}
} access_token:accountModel.access_token issue_type:@"multi"];
}
} phone:encryptedPhone
code:verificationCode
client_secret:@"uyzjdhds"
version:@"1"
client_id:@"erban-client"
grant_type:@"sms_code"
phoneAreaCode:areaCode];
```
### 自动登录流程
```objc
- (void)autoLogin {
// 检查本地是否有账户信息
AccountModel *accountModel = [[AccountInfoStorage instance] getCurrentAccountInfo];
if (accountModel == nil || accountModel.access_token == nil) {
[self tokenInvalid]; // 跳转到登录页
return;
}
// 检查是否有有效的 ticket
if ([[AccountInfoStorage instance] getTicket].length > 0) {
[[self getView] autoLoginSuccess];
return;
}
// 使用 access_token 重新获取 ticket
[Api requestTicket:^(BaseModel * _Nonnull data) {
NSArray *tickets = [data.data valueForKey:@"tickets"];
NSString *ticket = [tickets[0] valueForKey:@"ticket"];
[[AccountInfoStorage instance] saveTicket:ticket];
[[self getView] autoLoginSuccess];
} fail:^(NSInteger code, NSString * _Nullable msg) {
[self logout]; // ticket 获取失败,重新登录
} access_token:accountModel.access_token issue_type:@"multi"];
}
```
## 错误处理
### 401 未授权错误
当接收到 401 状态码时,系统会自动处理:
```objc
// 在 HttpRequestHelper 中
if (response && response.statusCode == 401) {
failure(response.statusCode, YMLocalizedString(@"HttpRequestHelper7"));
// 通常需要重新登录
}
```
### Ticket 过期处理
- Ticket 过期时服务器返回 401 错误
- 客户端应该使用保存的 `access_token` 重新获取 ticket
- 如果 `access_token` 也过期,则需要用户重新登录
## 安全注意事项
1. **数据加密**: 敏感信息(手机号、密码)使用 DES 加密传输
2. **本地存储**:
- `access_token` 存储在文件系统中
- `ticket` 存储在内存中,应用重启需重新获取
3. **请求头**: 所有业务请求自动携带 `pub_uid``pub_ticket`
4. **错误处理**: 建立完善的 401 错误重试机制
## 相关文件
- `YuMi/Structure/MVP/Model/AccountInfoStorage.h/m` - 账户信息存储管理
- `YuMi/Modules/YMLogin/Api/Api+Login.h/m` - 登录相关接口
- `YuMi/Modules/YMTabbar/Api/Api+Main.h/m` - Ticket 获取接口
- `YuMi/Network/HttpRequestHelper.h/m` - 网络请求管理
- `YuMi/Structure/MVP/Model/AccountModel.h/m` - 账户数据模型

1
yana/APIs/oauth flow.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -1,5 +1,5 @@
import UIKit
import NIMSDK
//import NIMSDK
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
@@ -11,20 +11,67 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// }
#if DEBUG
//
let testURL = URL(string: "http://beta.api.molistar.xyz/client/init")!
let request = URLRequest(url: testURL)
print("🛠 原生URLSession测试开始")
URLSession.shared.dataTask(with: request) { data, response, error in
print("""
=== 网络诊断结果 ===
响应状态码: \((response as? HTTPURLResponse)?.statusCode ?? -1)
错误信息: \(error?.localizedDescription ?? "")
原始数据: \(data?.count ?? 0) bytes
==================
""")
}.resume()
// 🔍 DESOC
// print("🔐 使OCDES")
// DESEncryptOCTest.runInAppDelegate()
// - 使
// let testURL = URL(string: "http://192.168.10.211:8080/oauth/token")!
// var request = URLRequest(url: testURL)
// request.httpMethod = "POST"
// request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// request.setValue("application/json", forHTTPHeaderField: "Accept")
// request.setValue("zh-Hant", forHTTPHeaderField: "Accept-Language")
//
// //
// let testParameters: [String: Any] = [
// "ispType": "65535",
// "phone": "3+TbIQYiwIk=",
// "netType": 2,
// "channel": "molistar_enterprise",
// "version": "20.20.61",
// "pub_sign": "2E7C50AA17A20B32A0023F20B7ECE108",
// "osVersion": "16.4",
// "deviceId": "b715b75715e3417c9c70e72bbe502c6c",
// "grant_type": "password",
// "os": "iOS",
// "app": "youmi",
// "password": "nTW/lEgupIQ=",
// "client_id": "erban-client",
// "lang": "zh-Hant-CN",
// "client_secret": "uyzjdhds",
// "Accept-Language": "zh-Hant",
// "model": "iPhone XR",
// "appVersion": "1.0.0"
// ]
//
// do {
// let jsonData = try JSONSerialization.data(withJSONObject: testParameters, options: .prettyPrinted)
// request.httpBody = jsonData
//
// print("🛠 URLSession")
// print("📍 : \(testURL.absoluteString)")
// print("📦 : \(String(data: jsonData, encoding: .utf8) ?? "")")
//
// URLSession.shared.dataTask(with: request) { data, response, error in
// DispatchQueue.main.async {
// let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
// let responseString = data != nil ? String(data: data!, encoding: .utf8) ?? "" : ""
//
// print("""
// === ===
// 🔗 URL: \(testURL.absoluteString)
// 📊 : \(statusCode)
// : \(error?.localizedDescription ?? "")
// 📦 : \(data?.count ?? 0) bytes
// 📄 : \(responseString)
// ==================
// """)
// }
// }.resume()
// } catch {
// print(" JSON: \(error.localizedDescription)")
// }
#endif
// NIMConfigurationManager.setupNimSDK()

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "bg@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "切图 65@3x (1).png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "切图 65@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "logo@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "勾选@3x (1).png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "top@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "勾选@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -15,9 +15,22 @@ struct AppConfig {
static var baseURL: String {
switch current {
case .development:
// return "http://192.168.10.211:8080"
return "http://beta.api.molistar.xyz"
case .production:
return "https://api.hfighting.com"
return "https://api.epartylive.com"
}
}
/// Web
/// - development: "/molistar"
/// - production: "/eparty"
static var webPathPrefix: String {
switch current {
case .development:
return "/molistar"
case .production:
return "/eparty"
}
}
@@ -34,29 +47,32 @@ struct AppConfig {
current = env
}
//
//
static var enableNetworkDebug: Bool {
#if DEBUG
return true
#else
return false
#endif
switch current {
case .development:
return true
case .production:
return false
}
}
//
//
static var serverTrustPolicies: [String: ServerTrustEvaluating] {
#if DEBUG
return ["beta.api.molistar.xyz": DisabledTrustEvaluator()]
#else
return ["api.hfighting.com": PublicKeysTrustEvaluator()]
#endif
switch current {
case .development:
return ["beta.api.molistar.xyz": DisabledTrustEvaluator()]
case .production:
return ["api.epartylive.com": PublicKeysTrustEvaluator()]
}
}
static var networkDebugEnabled: Bool {
#if DEBUG
return true
#else
return false
#endif
switch current {
case .development:
return true
case .production:
return false
}
}
}
}

View File

@@ -36,53 +36,51 @@ struct ContentView: View {
@State private var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab) {
//
VStack {
//
VStack(alignment: .leading, spacing: 8) {
Text("日志级别:")
.font(.headline)
.foregroundColor(.primary)
Picker("日志级别", selection: $selectedLogLevel) {
Text("日志").tag(APILogger.LogLevel.none)
Text("基础日志").tag(APILogger.LogLevel.basic)
Text("详细日志").tag(APILogger.LogLevel.detailed)
WithPerceptionTracking {
TabView(selection: $selectedTab) {
//
VStack {
//
VStack(alignment: .leading, spacing: 8) {
Text("日志级别:")
.font(.headline)
.foregroundColor(.primary)
Picker("日志级别", selection: $selectedLogLevel) {
Text("日志").tag(APILogger.LogLevel.none)
Text("基础日志").tag(APILogger.LogLevel.basic)
Text("详细日志").tag(APILogger.LogLevel.detailed)
}
.pickerStyle(SegmentedPickerStyle())
}
.pickerStyle(SegmentedPickerStyle())
}
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
Spacer()
VStack(spacing: 20) {
Text("yana")
.font(.largeTitle)
.fontWeight(.bold)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
VStack(spacing: 15) {
WithViewStore(store, observe: { $0 }) { viewStore in
TextField("账号", text: viewStore.binding(
get: \.account,
send: { LoginFeature.Action.updateAccount($0) }
Spacer()
VStack(spacing: 20) {
Text("eparty")
.font(.largeTitle)
.fontWeight(.bold)
VStack(spacing: 15) {
TextField("账号", text: Binding(
get: { store.account },
set: { store.send(.updateAccount($0)) }
))
.textFieldStyle(RoundedBorderTextFieldStyle())
.autocorrectionDisabled(true)
SecureField("密码", text: viewStore.binding(
get: \.password,
send: { LoginFeature.Action.updatePassword($0) }
SecureField("密码", text: Binding(
get: { store.password },
set: { store.send(.updatePassword($0)) }
))
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
.padding(.horizontal)
WithViewStore(store, observe: { $0 }) { viewStore in
if let error = viewStore.error {
.padding(.horizontal)
if let error = store.error {
Text(error)
.foregroundColor(.red)
.font(.caption)
@@ -92,114 +90,112 @@ struct ContentView: View {
VStack(spacing: 10) {
Button(action: {
viewStore.send(.login)
store.send(.login)
}) {
HStack {
if viewStore.isLoading {
if store.isLoading {
ProgressView()
.scaleEffect(0.8)
.progressViewStyle(CircularProgressViewStyle(tint: .white))
}
Text(viewStore.isLoading ? "登录中..." : "登录")
Text(store.isLoading ? "登录中..." : "登录")
}
.frame(maxWidth: .infinity)
.padding()
.background(viewStore.isLoading ? Color.gray : Color.blue)
.background(store.isLoading ? Color.gray : Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
.disabled(viewStore.isLoading || viewStore.account.isEmpty || viewStore.password.isEmpty)
.disabled(store.isLoading || store.account.isEmpty || store.password.isEmpty)
WithViewStore(initStore, observe: { $0 }) { initViewStore in
Button(action: {
initViewStore.send(.initialize)
}) {
HStack {
if initViewStore.isLoading {
ProgressView()
.scaleEffect(0.8)
.progressViewStyle(CircularProgressViewStyle(tint: .white))
}
Text(initViewStore.isLoading ? "测试中..." : "测试初始化")
Button(action: {
initStore.send(.initialize)
}) {
HStack {
if initStore.isLoading {
ProgressView()
.scaleEffect(0.8)
.progressViewStyle(CircularProgressViewStyle(tint: .white))
}
.frame(maxWidth: .infinity)
.padding()
.background(initViewStore.isLoading ? Color.gray : Color.green)
.foregroundColor(.white)
.cornerRadius(10)
Text(initStore.isLoading ? "测试中..." : "测试初始化")
}
.disabled(initViewStore.isLoading)
// API
if let response = initViewStore.response {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("API 测试结果:")
.font(.headline)
.foregroundColor(.primary)
}
ScrollView {
VStack(alignment: .leading, spacing: 4) {
Text("状态: \(response.status)")
if let message = response.message {
Text("消息: \(message)")
}
if let data = response.data {
Text("版本: \(data.version ?? "未知")")
Text("时间戳: \(data.timestamp ?? 0)")
if let config = data.config {
Text("配置:")
ForEach(Array(config.keys), id: \.self) { key in
Text(" \(key): \(config[key] ?? "")")
}
.frame(maxWidth: .infinity)
.padding()
.background(initStore.isLoading ? Color.gray : Color.green)
.foregroundColor(.white)
.cornerRadius(10)
}
.disabled(initStore.isLoading)
// API
if let response = initStore.response {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("API 测试结果:")
.font(.headline)
.foregroundColor(.primary)
}
ScrollView {
VStack(alignment: .leading, spacing: 4) {
Text("状态: \(response.status)")
if let message = response.message {
Text("消息: \(message)")
}
if let data = response.data {
Text("版本: \(data.version ?? "未知")")
Text("时间戳: \(data.timestamp ?? 0)")
if let config = data.config {
Text("配置:")
ForEach(Array(config.keys), id: \.self) { key in
Text(" \(key): \(config[key] ?? "")")
}
}
}
.font(.system(.caption, design: .monospaced))
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(8)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
.frame(maxHeight: 200)
.font(.system(.caption, design: .monospaced))
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(8)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
.padding()
.background(Color.gray.opacity(0.05))
.cornerRadius(10)
}
if let error = initViewStore.error {
Text(error)
.foregroundColor(.red)
.font(.caption)
.multilineTextAlignment(.center)
.padding()
.frame(maxHeight: 200)
}
.padding()
.background(Color.gray.opacity(0.05))
.cornerRadius(10)
}
if let error = initStore.error {
Text(error)
.foregroundColor(.red)
.font(.caption)
.multilineTextAlignment(.center)
.padding()
}
.padding(.horizontal)
}
.padding(.horizontal)
}
Spacer()
}
Spacer()
}
.padding()
.tabItem {
Label("登录", systemImage: "person.circle")
}
.tag(0)
// API
ConfigView(store: configStore)
.padding()
.tabItem {
Label("API 测试", systemImage: "network")
Label("登录", systemImage: "person.circle")
}
.tag(1)
}
.onChange(of: selectedLogLevel) { newValue in
APILogger.logLevel = newValue
.tag(0)
// API
ConfigView(store: configStore)
.tabItem {
Label("API 测试", systemImage: "network")
}
.tag(1)
}
.onChange(of: selectedLogLevel) { newValue in
APILogger.logLevel = newValue
}
}
}
}

View File

@@ -5,7 +5,7 @@ struct ConfigView: View {
let store: StoreOf<ConfigFeature>
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
WithPerceptionTracking {
NavigationView {
VStack(spacing: 20) {
//
@@ -16,7 +16,7 @@ struct ConfigView: View {
//
Group {
if viewStore.isLoading {
if store.isLoading {
VStack {
ProgressView()
.scaleEffect(1.5)
@@ -26,7 +26,7 @@ struct ConfigView: View {
.padding(.top, 8)
}
.frame(height: 100)
} else if let errorMessage = viewStore.errorMessage {
} else if let errorMessage = store.errorMessage {
VStack {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 40))
@@ -43,13 +43,13 @@ struct ConfigView: View {
.padding(.horizontal)
Button("清除错误") {
viewStore.send(.clearError)
store.send(.clearError)
}
.buttonStyle(.borderedProminent)
.padding(.top)
}
.frame(maxHeight: .infinity)
} else if let configData = viewStore.configData {
} else if let configData = store.configData {
//
ScrollView {
VStack(alignment: .leading, spacing: 16) {
@@ -102,7 +102,7 @@ struct ConfigView: View {
.cornerRadius(12)
}
if let lastUpdated = viewStore.lastUpdated {
if let lastUpdated = store.lastUpdated {
Text("最后更新: \(lastUpdated, style: .time)")
.font(.caption)
.foregroundColor(.secondary)
@@ -130,21 +130,21 @@ struct ConfigView: View {
//
VStack(spacing: 12) {
Button(action: {
viewStore.send(.loadConfig)
store.send(.loadConfig)
}) {
HStack {
if viewStore.isLoading {
if store.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
} else {
Image(systemName: "arrow.clockwise")
}
Text(viewStore.isLoading ? "加载中..." : "加载配置")
Text(store.isLoading ? "加载中..." : "加载配置")
}
}
.buttonStyle(.borderedProminent)
.disabled(viewStore.isLoading)
.disabled(store.isLoading)
.frame(maxWidth: .infinity)
.frame(height: 50)
@@ -152,7 +152,7 @@ struct ConfigView: View {
.font(.caption)
.foregroundColor(.secondary)
}
.padding()
}
}
.navigationBarHidden(true)

View File

@@ -0,0 +1,145 @@
import Foundation
import ComposableArchitecture
@Reducer
struct EMailLoginFeature {
@ObservableState
struct State: Equatable {
var email: String = ""
var verificationCode: String = ""
var isLoading: Bool = false
var isCodeLoading: Bool = false
var errorMessage: String? = nil
var codeCountdown: Int = 0
var isCodeButtonEnabled: Bool = true
// Debug
#if DEBUG
init() {
self.email = "85494536@gmail.com"
self.verificationCode = ""
}
#endif
}
enum Action: Equatable {
case emailChanged(String)
case verificationCodeChanged(String)
case getVerificationCodeTapped
case loginButtonTapped(email: String, verificationCode: String)
case forgotPasswordTapped
case codeCountdownTick
case setLoading(Bool)
case setCodeLoading(Bool)
case setError(String?)
case startCodeCountdown
case resetCodeCountdown
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .emailChanged(let email):
state.email = email
state.errorMessage = nil
return .none
case .verificationCodeChanged(let code):
state.verificationCode = code
state.errorMessage = nil
return .none
case .getVerificationCodeTapped:
guard !state.email.isEmpty else {
state.errorMessage = "email_login.email_required".localized
return .none
}
guard isValidEmail(state.email) else {
state.errorMessage = "email_login.invalid_email".localized
return .none
}
state.isCodeLoading = true
state.errorMessage = nil
return .run { send in
// API
try await Task.sleep(nanoseconds: 1_000_000_000) // 1
await send(.setCodeLoading(false))
await send(.startCodeCountdown)
}
case .loginButtonTapped(let email, let verificationCode):
guard !email.isEmpty && !verificationCode.isEmpty else {
state.errorMessage = "email_login.fields_required".localized
return .none
}
guard isValidEmail(email) else {
state.errorMessage = "email_login.invalid_email".localized
return .none
}
state.isLoading = true
state.errorMessage = nil
return .run { send in
// API
try await Task.sleep(nanoseconds: 2_000_000_000) // 2
await send(.setLoading(false))
//
print("🔐 邮箱登录尝试: \(email), 验证码: \(verificationCode)")
}
case .forgotPasswordTapped:
//
print("📧 忘记密码点击")
return .none
case .codeCountdownTick:
if state.codeCountdown > 0 {
state.codeCountdown -= 1
state.isCodeButtonEnabled = false
return .run { send in
try await Task.sleep(nanoseconds: 1_000_000_000) // 1
await send(.codeCountdownTick)
}
} else {
state.isCodeButtonEnabled = true
return .none
}
case .setLoading(let isLoading):
state.isLoading = isLoading
return .none
case .setCodeLoading(let isLoading):
state.isCodeLoading = isLoading
return .none
case .setError(let error):
state.errorMessage = error
return .none
case .startCodeCountdown:
state.codeCountdown = 60
state.isCodeButtonEnabled = false
return .send(.codeCountdownTick)
case .resetCodeCountdown:
state.codeCountdown = 0
state.isCodeButtonEnabled = true
return .none
}
}
}
// MARK: - Helper Methods
private func isValidEmail(_ email: String) -> Bool {
let emailRegex = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"
let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
return emailPredicate.evaluate(with: email)
}
}

View File

@@ -0,0 +1,209 @@
import Foundation
import ComposableArchitecture
@Reducer
struct IDLoginFeature {
@ObservableState
struct State: Equatable {
var userID: String = ""
var password: String = ""
var isPasswordVisible = false
var isLoading = false
var errorMessage: String?
// Ticket
var accessToken: String?
var ticket: String?
var isTicketLoading = false
var ticketError: String?
var loginStep: LoginStep = .initial
var uid: Int? // uidInt
enum LoginStep: Equatable {
case initial //
case authenticating // OAuth
case gettingTicket // Ticket
case completed //
case failed //
}
#if DEBUG
init() {
//
self.userID = ""
self.password = ""
}
#endif
}
enum Action: Equatable {
case togglePasswordVisibility
case loginButtonTapped(userID: String, password: String)
case forgotPasswordTapped
case backButtonTapped
case loginResponse(TaskResult<IDLoginResponse>)
// Ticket actions
case requestTicket(accessToken: String)
case ticketResponse(TaskResult<TicketResponse>)
case clearTicketError
case resetLogin
}
@Dependency(\.apiService) var apiService
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .togglePasswordVisibility:
state.isPasswordVisible.toggle()
return .none
case let .loginButtonTapped(userID, password):
state.userID = userID
state.password = password
state.isLoading = true
state.errorMessage = nil
state.ticketError = nil
state.loginStep = .authenticating
// IDAPI
return .run { send in
do {
// 使LoginHelper
guard let loginRequest = LoginHelper.createIDLoginRequest(userID: userID, password: password) else {
await send(.loginResponse(.failure(APIError.decodingError("加密失败"))))
return
}
//
let response = try await apiService.request(loginRequest)
await send(.loginResponse(.success(response)))
} catch {
if let apiError = error as? APIError {
await send(.loginResponse(.failure(apiError)))
} else {
await send(.loginResponse(.failure(APIError.unknown(error.localizedDescription))))
}
}
}
case .forgotPasswordTapped:
// TODO:
return .none
case .backButtonTapped:
//
return .none
case let .loginResponse(.success(response)):
state.isLoading = false
if response.isSuccess {
// OAuth
state.errorMessage = nil
state.accessToken = response.data?.accessToken
state.uid = response.data?.uid // uid
//
if let userInfo = response.data?.userInfo {
UserInfoManager.saveUserInfo(userInfo)
}
print("✅ ID 登录 OAuth 认证成功")
if let accessToken = response.data?.accessToken {
print("🔑 Access Token: \(accessToken)")
// ticket uid
return .send(.requestTicket(accessToken: accessToken))
}
if let uid = response.data?.uid {
print("🆔 用户 UID: \(uid)")
}
} else {
state.errorMessage = response.errorMessage
state.loginStep = .failed
}
return .none
case let .loginResponse(.failure(error)):
state.isLoading = false
state.errorMessage = error.localizedDescription
state.loginStep = .failed
return .none
case let .requestTicket(accessToken):
state.isTicketLoading = true
state.ticketError = nil
state.loginStep = .gettingTicket
return .run { [uid = state.uid] send in
do {
// 使 TicketHelper uid
let ticketRequest = TicketHelper.createTicketRequest(accessToken: accessToken, uid: uid)
let response = try await apiService.request(ticketRequest)
await send(.ticketResponse(.success(response)))
} catch {
print("❌ ID登录 Ticket 获取失败: \(error)")
await send(.ticketResponse(.failure(APIError.networkError(error.localizedDescription))))
}
}
case let .ticketResponse(.success(response)):
state.isTicketLoading = false
if response.isSuccess {
state.ticketError = nil
state.ticket = response.ticket
state.loginStep = .completed
print("✅ ID 登录完整流程成功")
print("🎫 Ticket 获取成功: \(response.ticket ?? "nil")")
//
if let accessToken = state.accessToken,
let ticket = response.ticket {
//
let userInfo = UserInfoManager.getUserInfo()
UserInfoManager.saveCompleteAuthenticationData(
accessToken: accessToken,
ticket: ticket,
uid: state.uid,
userInfo: userInfo
)
}
// TODO:
} else {
state.ticketError = response.errorMessage
state.loginStep = .failed
}
return .none
case let .ticketResponse(.failure(error)):
state.isTicketLoading = false
state.ticketError = error.localizedDescription
state.loginStep = .failed
print("❌ ID 登录 Ticket 获取失败: \(error.localizedDescription)")
return .none
case .clearTicketError:
state.ticketError = nil
return .none
case .resetLogin:
state.isLoading = false
state.isTicketLoading = false
state.errorMessage = nil
state.ticketError = nil
state.accessToken = nil
state.ticket = nil
state.uid = nil // uid
state.loginStep = .initial
//
UserInfoManager.clearAllAuthenticationData()
return .none
}
}
}
}

View File

@@ -1,12 +1,6 @@
import Foundation
import ComposableArchitecture
struct LoginResponse: Codable, Equatable {
let status: String
let message: String?
let token: String?
}
@Reducer
struct LoginFeature {
@ObservableState
@@ -15,11 +9,29 @@ struct LoginFeature {
var password: String = ""
var isLoading = false
var error: String?
var idLoginState = IDLoginFeature.State()
// Ticket
var accessToken: String?
var ticket: String?
var isTicketLoading = false
var ticketError: String?
var loginStep: LoginStep = .initial
var uid: Int? // uidInt
enum LoginStep: Equatable {
case initial //
case authenticating // OAuth
case gettingTicket // Ticket
case completed //
case failed //
}
#if DEBUG
init() {
self.account = "3184"
self.password = "a0d5da073d14731cc7a01ecaa17b9174"
//
self.account = ""
self.password = ""
}
#endif
}
@@ -28,56 +40,168 @@ struct LoginFeature {
case updateAccount(String)
case updatePassword(String)
case login
case loginResponse(TaskResult<LoginResponse>)
case loginResponse(TaskResult<IDLoginResponse>)
case idLogin(IDLoginFeature.Action)
// Ticket actions
case requestTicket(accessToken: String)
case ticketResponse(TaskResult<TicketResponse>)
case clearTicketError
case resetLogin
}
@Dependency(\.apiService) var apiService
var body: some ReducerOf<Self> {
// Reduce { state, action in
// switch action {
// case let .updateAccount(account):
// state.account = account
// return .none
//
// case let .updatePassword(password):
// state.password = password
// return .none
//
// case .login:
// state.isLoading = true
// state.error = nil
//
// let loginBody = [
// "account": state.account,
// "password": state.password
// ]
//
// return .run { send in
// do {
// let response: LoginResponse = try await APIClientManager.shared.post(
// path: APIConstants.Endpoints.login,
// body: loginBody,
// headers: APIConstants.defaultHeaders
// )
// await send(.loginResponse(.success(response)))
// } catch {
// await send(.loginResponse(.failure(error)))
// }
// }
//
// case let .loginResponse(.success(response)):
// state.isLoading = false
// if response.status == "success" {
// // TODO: token
// } else {
// state.error = response.message ?? ""
// }
// return .none
//
// case let .loginResponse(.failure(error)):
// state.isLoading = false
// state.error = error.localizedDescription
// return .none
// }
// }
Scope(state: \.idLoginState, action: \.idLogin) {
IDLoginFeature()
}
Reduce { state, action in
switch action {
case let .updateAccount(account):
state.account = account
return .none
case let .updatePassword(password):
state.password = password
return .none
case .login:
state.isLoading = true
state.error = nil
state.ticketError = nil
state.loginStep = .authenticating
// 使accountpassword
return .run { [account = state.account, password = state.password] send in
do {
// 使LoginHelper
guard let loginRequest = LoginHelper.createIDLoginRequest(userID: account, password: password) else {
await send(.loginResponse(.failure(APIError.decodingError("加密失败"))))
return
}
//
let response = try await apiService.request(loginRequest)
await send(.loginResponse(.success(response)))
} catch {
if let apiError = error as? APIError {
await send(.loginResponse(.failure(apiError)))
} else {
await send(.loginResponse(.failure(APIError.unknown(error.localizedDescription))))
}
}
}
case let .loginResponse(.success(response)):
state.isLoading = false
if response.isSuccess {
// OAuth
state.error = nil
state.accessToken = response.data?.accessToken
state.uid = response.data?.uid // uid
print("✅ OAuth 认证成功")
if let accessToken = response.data?.accessToken {
print("🔑 Access Token: \(accessToken)")
// ticket uid
return .send(.requestTicket(accessToken: accessToken))
}
if let userInfo = response.data?.userInfo {
print("👤 用户信息: \(userInfo)")
}
if let uid = response.data?.uid {
print("🆔 用户 UID: \(uid)")
}
} else {
state.error = response.errorMessage
state.loginStep = .failed
}
return .none
case let .loginResponse(.failure(error)):
state.isLoading = false
state.error = error.localizedDescription
state.loginStep = .failed
return .none
case let .requestTicket(accessToken):
state.isTicketLoading = true
state.ticketError = nil
state.loginStep = .gettingTicket
return .run { [uid = state.uid] send in
do {
// 使 TicketHelper uid
let ticketRequest = TicketHelper.createTicketRequest(accessToken: accessToken, uid: uid)
let response = try await apiService.request(ticketRequest)
await send(.ticketResponse(.success(response)))
} catch {
print("❌ Ticket 获取失败: \(error)")
await send(.ticketResponse(.failure(APIError.networkError(error.localizedDescription))))
}
}
case let .ticketResponse(.success(response)):
state.isTicketLoading = false
if response.isSuccess {
state.ticketError = nil
state.ticket = response.ticket
state.loginStep = .completed
print("✅ 完整登录流程成功")
print("🎫 Ticket 获取成功: \(response.ticket ?? "nil")")
//
if let accessToken = state.accessToken,
let ticket = response.ticket {
UserInfoManager.saveCompleteAuthenticationData(
accessToken: accessToken,
ticket: ticket,
uid: state.uid,
userInfo: nil // LoginFeature
)
}
// TODO:
} else {
state.ticketError = response.errorMessage
state.loginStep = .failed
}
return .none
case let .ticketResponse(.failure(error)):
state.isTicketLoading = false
state.ticketError = error.localizedDescription
state.loginStep = .failed
print("❌ Ticket 获取失败: \(error.localizedDescription)")
return .none
case .clearTicketError:
state.ticketError = nil
return .none
case .resetLogin:
state.isLoading = false
state.isTicketLoading = false
state.error = nil
state.ticketError = nil
state.accessToken = nil
state.ticket = nil
state.uid = nil // uid
state.loginStep = .initial
//
UserInfoManager.clearAllAuthenticationData()
return .none
case .idLogin:
// IDLoginfeature
return .none
}
}
}
}

View File

@@ -0,0 +1,39 @@
import Foundation
import ComposableArchitecture
@Reducer
struct SplashFeature {
@ObservableState
struct State: Equatable {
var isLoading = true
var shouldShowMainApp = false
}
enum Action: Equatable {
case onAppear
case splashFinished
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .onAppear:
state.isLoading = true
state.shouldShowMainApp = false
// 1 (iOS 15.5+ )
return .run { send in
try await Task.sleep(nanoseconds: 1_000_000_000) // 1 = 1,000,000,000
await send(.splashFinished)
}
case .splashFinished:
state.isLoading = false
state.shouldShowMainApp = true
//
NotificationCenter.default.post(name: .splashFinished, object: nil)
return .none
}
}
}
}

Binary file not shown.

64
yana/Fonts/README.md Normal file
View File

@@ -0,0 +1,64 @@
# 字体文件使用指南
## 字体文件位置
请将 **Bayon-Regular.ttf** 字体文件放置在此文件夹中。
## 添加步骤
### 1. 获取字体文件
- 从 Google Fonts 下载 Bayon 字体https://fonts.google.com/specimen/Bayon
- 或从设计师提供的字体文件中获取 `Bayon-Regular.ttf`
### 2. 添加到项目
1.`Bayon-Regular.ttf` 文件拖放到此 `Fonts` 文件夹中
2. 在 Xcode 中,确保文件被添加到项目的 Target 中
3. 检查 `Info.plist` 中已经配置了 `UIAppFonts` 数组
### 3. 验证字体是否正确加载
`AppDelegate.swift` 中添加调试代码:
```swift
#if DEBUG
FontManager.printAllAvailableFonts()
// 检查 Bayon 字体是否可用
print("Bayon 字体可用:\(FontManager.isFontAvailable(.bayonRegular))")
#endif
```
## 当前配置状态
### ✅ 已完成:
- [x] Info.plist 配置完成
- [x] FontManager 工具类创建完成
- [x] LoginView 中 E-PARTI 文本已应用 Bayon 字体
- [x] 字体适配与屏幕尺寸兼容
### ⏳ 待完成:
- [ ] 添加 Bayon-Regular.ttf 字体文件到项目中
## 使用方法
### 方法1: 使用 FontManager推荐
```swift
Text("E-PARTI")
.font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width))
```
### 方法2: 使用 View Extension
```swift
Text("E-PARTI")
.adaptedCustomFont(.bayonRegular, designSize: 56)
```
### 方法3: 直接指定大小
```swift
Text("E-PARTI")
.customFont(.bayonRegular, size: 56)
```
## 故障排除
如果字体未生效,请检查:
1. 字体文件是否正确添加到项目 Target 中
2. Info.plist 中的字体文件名是否正确
3. 字体文件名与代码中使用的名称是否一致
4. 运行调试代码确认字体是否被系统识别

View File

@@ -9,5 +9,9 @@
</dict>
<key>NSWiFiUsageDescription</key>
<string>应用需要访问 Wi-Fi 信息以提供网络相关功能</string>
<key>UIAppFonts</key>
<array>
<string>Bayon-Regular.ttf</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="s0d-6b-0kx">
<objects>
<viewController id="Y6W-OH-hqX" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bg" translatesAutoresizingMaskIntoConstraints="NO" id="Mom-Je-A43">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
</imageView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="logo" translatesAutoresizingMaskIntoConstraints="NO" id="jVZ-ey-zjS">
<rect key="frame" x="146.66666666666666" y="200" width="100" height="100"/>
<constraints>
<constraint firstAttribute="width" constant="100" id="61A-Xv-MlD"/>
<constraint firstAttribute="height" constant="100" id="NWJ-mJ-K2O"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="E-Parti" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GrU-nK-tAY">
<rect key="frame" x="138" y="332" width="117" height="48"/>
<fontDescription key="fontDescription" type="system" pointSize="40"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="jVZ-ey-zjS" firstAttribute="centerX" secondItem="Mom-Je-A43" secondAttribute="centerX" id="Atv-LB-aNW"/>
<constraint firstItem="Mom-Je-A43" firstAttribute="leading" secondItem="vDu-zF-Fre" secondAttribute="leading" id="fQ2-yj-nze"/>
<constraint firstItem="Mom-Je-A43" firstAttribute="top" secondItem="5EZ-qb-Rvc" secondAttribute="top" id="gVA-6N-OG4"/>
<constraint firstItem="GrU-nK-tAY" firstAttribute="centerY" secondItem="Mom-Je-A43" secondAttribute="centerY" constant="-70" id="j81-qa-9vS"/>
<constraint firstAttribute="bottom" secondItem="Mom-Je-A43" secondAttribute="bottom" id="lBn-me-aJu"/>
<constraint firstItem="Mom-Je-A43" firstAttribute="trailing" secondItem="vDu-zF-Fre" secondAttribute="trailing" id="lka-KN-fEy"/>
<constraint firstItem="jVZ-ey-zjS" firstAttribute="top" secondItem="Mom-Je-A43" secondAttribute="top" constant="200" id="nCq-oK-mTB"/>
<constraint firstItem="GrU-nK-tAY" firstAttribute="centerX" secondItem="Mom-Je-A43" secondAttribute="centerX" id="sZE-bZ-0Xj"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-208.3969465648855" y="-13.380281690140846"/>
</scene>
</scenes>
<resources>
<image name="bg" width="375" height="812"/>
<image name="logo" width="100" height="100"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View File

@@ -1,35 +0,0 @@
import NIMSDK
import NECoreKit
import NECoreIM2Kit
import NEChatKit
import NEChatUIKit
struct NIMConfigurationManager {
static func setupNimSDK() {
let option = configureNIMSDKOption()
setupSDK(with: option)
setupChatSDK(with: option)
}
static func setupSDK(with option: NIMSDKOption) {
NIMSDK.shared().register(with: option)
NIMSDKConfig.shared().shouldConsiderRevokedMessageUnreadCount = true
NIMSDKConfig.shared().shouldSyncStickTopSessionInfos = true
}
static func setupChatSDK(with option: NIMSDKOption) {
let v2Option = V2NIMSDKOption()
v2Option.enableV2CloudConversation = false
// TODO: IMKitClient API
// IMKitClient.shared.setupIM2(option, v2Option)
print("⚠️ NIM SDK 配置暂时被注释,需要修复 IMKitClient API")
}
static func configureNIMSDKOption() -> NIMSDKOption {
let option = NIMSDKOption()
option.appKey = "79bc37000f4018a2a24ea9dc6ca08d32"
option.apnsCername = "pikoDevelopPush"
return option
}
}

View File

@@ -1,127 +0,0 @@
import Foundation
import NIMSDK
// MARK: -
extension Notification.Name {
static let NIMNetworkStateChanged = Notification.Name("NIMNetworkStateChangedNotification")
static let NIMTokenExpired = Notification.Name("NIMTokenExpiredNotification")
}
@objc
@objcMembers
final class NIMSessionManager: NSObject {
static let shared = NIMSessionManager()
// MARK: -
func autoLogin(account: String, token: String, completion: @escaping (Error?) -> Void) {
NIMSDK.shared().v2LoginService.add(self)
let data = NIMAutoLoginData()
data.account = account
data.token = token
data.forcedMode = false
NIMSDK.shared().loginManager.autoLogin(data)
}
func login(account: String, token: String, completion: @escaping (Error?) -> Void) {
NIMSDK.shared().loginManager.login(account, token: token) { error in
if error == nil {
self.registerObservers()
}
completion(error)
}
}
func logout() {
NIMSDK.shared().loginManager.logout { _ in
self.removeObservers()
}
}
// MARK: -
private func registerObservers() {
// autoLogin
// NIMSDK.shared().v2LoginService.add(self as! V2NIMLoginServiceDelegate)
// registerObservers
// NIMSDK.shared().v2LoginService.add(self as! V2NIMLoginServiceDelegate)
// removeObservers
// NIMSDK.shared().v2LoginService.remove(self as! V2NIMLoginServiceDelegate)
NIMSDK.shared().chatManager.add(self)
NIMSDK.shared().loginManager.add(self)
}
private func removeObservers() {
NIMSDK.shared().v2LoginService.remove(self)
NIMSDK.shared().chatManager.remove(self)
NIMSDK.shared().loginManager.remove(self)
}
}
// MARK: - NIMChatManagerDelegate
extension NIMSessionManager: NIMChatManagerDelegate {
func onRecvMessages(_ messages: [NIMMessage]) {
NotificationCenter.default.post(
name: .NIMDidReceiveMessage,
object: messages
)
}
}
// MARK: - NIMLoginManagerDelegate
extension NIMSessionManager: NIMLoginManagerDelegate {
func onLogin(_ step: NIMLoginStep) {
NotificationCenter.default.post(
name: .NIMLoginStateChanged,
object: step
)
}
func onAutoLoginFailed(_ error: Error) {
if (error as NSError).code == 302 {
NotificationCenter.default.post(name: .NIMTokenExpired, object: nil)
}
}
}
// MARK: -
extension Notification.Name {
static let NIMDidReceiveMessage = Notification.Name("NIMDidReceiveMessageNotification")
static let NIMLoginStateChanged = Notification.Name("NIMLoginStateChangedNotification")
}
// MARK: - NIMV2LoginServiceDelegate
extension NIMSessionManager: V2NIMLoginListener {
func onLoginStatus(_ status: V2NIMLoginStatus) {
}
func onLoginFailed(_ error: V2NIMError) {
}
func onKickedOffline(_ detail: V2NIMKickedOfflineDetail) {
}
func onLoginClientChanged(
_ change: V2NIMLoginClientChange,
clients: [V2NIMLoginClient]?
) {
}
// @objc func onLoginProcess(step: NIMV2LoginStep) {
// NotificationCenter.default.post(
// name: .NIMV2LoginStateChanged,
// object: step
// )
// }
//
// @objc func onKickOut(result: NIMKickOutResult) {
// NotificationCenter.default.post(
// name: .NIMKickOutNotification,
// object: result
// )
// }
}

View File

@@ -0,0 +1,59 @@
/*
Localizable.strings
yana
Created on 2024.
英文本地化文件
*/
// MARK: - 登录界面
"login.id_login" = "ID Login";
"login.email_login" = "Email Login";
"login.app_title" = "E-PARTI";
"login.agreement_policy" = "Agree to the \"User Service Agreement\" and \"Privacy Policy\"";
"login.agreement" = "User Service Agreement";
"login.policy" = "Privacy Policy";
// MARK: - 通用按钮
"common.login" = "Login";
"common.register" = "Register";
"common.cancel" = "Cancel";
"common.confirm" = "Confirm";
"common.ok" = "OK";
// MARK: - 错误信息
"error.network" = "Network Error";
"error.invalid_input" = "Invalid Input";
"error.login_failed" = "Login Failed";
// MARK: - 占位符文本
"placeholder.email" = "Enter your email";
"placeholder.password" = "Enter your password";
"placeholder.username" = "Enter your username";
"placeholder.enter_id" = "Please enter ID";
"placeholder.enter_password" = "Please enter password";
// MARK: - ID登录页面
"id_login.title" = "ID Login";
"id_login.forgot_password" = "Forgot Password?";
"id_login.login_button" = "Login";
"id_login.logging_in" = "Logging in...";
// MARK: - 邮箱登录页面
"email_login.title" = "Email Login";
"email_login.email_required" = "Please enter email";
"email_login.invalid_email" = "Please enter a valid email address";
"email_login.fields_required" = "Please enter email and verification code";
"email_login.get_code" = "Get";
"email_login.resend_code" = "Resend";
"email_login.code_sent" = "Verification code sent";
"email_login.login_button" = "Login";
"email_login.logging_in" = "Logging in...";
"placeholder.enter_email" = "Please enter email";
"placeholder.enter_verification_code" = "Please enter verification code";
// MARK: - 验证和错误信息
"validation.id_required" = "Please enter your ID";
"validation.password_required" = "Please enter your password";
"error.encryption_failed" = "Encryption failed, please try again";
"error.login_failed" = "Login failed, please check your credentials";

View File

@@ -0,0 +1,59 @@
/*
Localizable.strings
yana
Created on 2024.
中文简体本地化文件
*/
// MARK: - 登录界面
"login.id_login" = "ID 登录";
"login.email_login" = "邮箱登录";
"login.app_title" = "E-PARTI";
"login.agreement_policy" = "同意《用戶服務協議》和《隱私政策》";
"login.agreement" = "《用戶服務協議》";
"login.policy" = "《隱私政策》";
// MARK: - 通用按钮
"common.login" = "登录";
"common.register" = "注册";
"common.cancel" = "取消";
"common.confirm" = "确认";
"common.ok" = "确定";
// MARK: - 错误信息
"error.network" = "网络错误";
"error.invalid_input" = "输入无效";
"error.login_failed" = "登录失败";
// MARK: - 占位符文本
"placeholder.email" = "请输入邮箱";
"placeholder.password" = "请输入密码";
"placeholder.username" = "请输入用户名";
"placeholder.enter_id" = "请输入ID";
"placeholder.enter_password" = "请输入密码";
// MARK: - ID登录页面
"id_login.title" = "ID 登录";
"id_login.forgot_password" = "忘记密码?";
"id_login.login_button" = "登录";
"id_login.logging_in" = "登录中...";
// MARK: - 邮箱登录页面
"email_login.title" = "邮箱登录";
"email_login.email_required" = "请输入邮箱";
"email_login.invalid_email" = "请输入有效的邮箱地址";
"email_login.fields_required" = "请输入邮箱和验证码";
"email_login.get_code" = "获取验证码";
"email_login.resend_code" = "重新发送";
"email_login.code_sent" = "验证码已发送";
"email_login.login_button" = "登录";
"email_login.logging_in" = "登录中...";
"placeholder.enter_email" = "请输入邮箱";
"placeholder.enter_verification_code" = "请输入验证码";
// MARK: - 验证和错误信息
"validation.id_required" = "请输入您的ID";
"validation.password_required" = "请输入您的密码";
"error.encryption_failed" = "加密失败,请重试";
"error.login_failed" = "登录失败,请检查您的凭据";

View File

@@ -0,0 +1,26 @@
import SwiftUI
// MARK: - Color Hex Extension
extension Color {
/// 使
/// - Parameter hex: 0xRRGGBB
/// - Example: Color(hex: 0x313131)
init(hex: UInt32) {
let red = Double((hex >> 16) & 0xFF) / 255.0
let green = Double((hex >> 8) & 0xFF) / 255.0
let blue = Double(hex & 0xFF) / 255.0
self.init(red: red, green: green, blue: blue)
}
/// 使
/// - Parameters:
/// - hex: 0xRRGGBB
/// - alpha: 0.0-1.0
/// - Example: Color(hex: 0x313131, alpha: 0.8)
init(hex: UInt32, alpha: Double) {
let red = Double((hex >> 16) & 0xFF) / 255.0
let green = Double((hex >> 8) & 0xFF) / 255.0
let blue = Double(hex & 0xFF) / 255.0
self.init(red: red, green: green, blue: blue, opacity: alpha)
}
}

View File

@@ -0,0 +1,83 @@
import Foundation
///
/// MD5 SHA256
struct StringHashTest {
///
static func runTests() {
print("🧪 开始测试字符串哈希方法...")
let testStrings = [
"hello world",
"test123",
"key=rpbs6us1m8r2j9g6u06ff2bo18orwaya",
"phone=encrypted_phone&password=encrypted_password&client_id=erban-client&key=rpbs6us1m8r2j9g6u06ff2bo18orwaya"
]
for testString in testStrings {
print("\n📝 测试字符串: \"\(testString)\"")
// MD5
let md5Result = testString.md5()
print(" MD5: \(md5Result)")
// SHA256 (iOS 13+)
if #available(iOS 13.0, *) {
let sha256Result = testString.sha256()
print(" SHA256: \(sha256Result)")
} else {
print(" SHA256: 不支持 (需要 iOS 13+)")
}
}
print("\n✅ 哈希方法测试完成")
}
///
static func verifyKnownHashes() {
print("\n🔍 验证已知哈希值...")
// "hello world" MD5 "5d41402abc4b2a76b9719d911017c592"
let testString = "hello world"
let expectedMD5 = "5d41402abc4b2a76b9719d911017c592"
let actualMD5 = testString.md5()
if actualMD5 == expectedMD5 {
print("✅ MD5 验证通过: \(actualMD5)")
} else {
print("❌ MD5 验证失败:")
print(" 期望: \(expectedMD5)")
print(" 实际: \(actualMD5)")
}
// SHA256
if #available(iOS 13.0, *) {
let expectedSHA256 = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
let actualSHA256 = testString.sha256()
if actualSHA256 == expectedSHA256 {
print("✅ SHA256 验证通过: \(actualSHA256)")
} else {
print("❌ SHA256 验证失败:")
print(" 期望: \(expectedSHA256)")
print(" 实际: \(actualSHA256)")
}
}
}
}
// MARK: - 使
/*
//
StringHashTest.runTests()
StringHashTest.verifyKnownHashes()
//
print("Test MD5:", "hello".md5())
if #available(iOS 13.0, *) {
print("Test SHA256:", "hello".sha256())
}
*/

View File

@@ -0,0 +1,39 @@
import Foundation
import CommonCrypto
import CryptoKit
// MARK: - String Hash Extensions
extension String {
/// SHA256使
/// - Returns: SHA256
@available(iOS 13.0, *)
func sha256() -> String {
let data = Data(self.utf8)
let digest = SHA256.hash(data: data)
return digest.compactMap { String(format: "%02x", $0) }.joined()
}
/// MD5
///
/// MD5iOS 13.0
/// 使 sha256()
///
/// - Returns: MD5
func md5() -> String {
if #available(iOS 13.0, *) {
// iOS 13+ 使 CryptoKit Insecure.MD5
let data = Data(self.utf8)
let digest = Insecure.MD5.hash(data: data)
return digest.compactMap { String(format: "%02x", $0) }.joined()
} else {
// iOS 13 使 CommonCrypto
let data = Data(self.utf8)
let hash = data.withUnsafeBytes { bytes -> [UInt8] in
var hash = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
CC_MD5(bytes.baseAddress, CC_LONG(data.count), &hash)
return hash
}
return hash.map { String(format: "%02x", $0) }.joined()
}
}
}

View File

@@ -0,0 +1,21 @@
import SwiftUI
// MARK: - View Extension for Placeholder
extension View {
/// TextFieldSecureField
/// - Parameters:
/// - shouldShow:
/// - alignment:
/// - placeholder:
/// - Returns:
func placeholder<Content: View>(
when shouldShow: Bool,
alignment: Alignment = .leading,
@ViewBuilder placeholder: () -> Content) -> some View {
ZStack(alignment: alignment) {
placeholder().opacity(shouldShow ? 1 : 0)
self
}
}
}

View File

@@ -0,0 +1,110 @@
import SwiftUI
///
/// 使
struct FontManager {
// MARK: -
enum CustomFont: String, CaseIterable {
case bayonRegular = "Bayon-Regular"
///
var displayName: String {
switch self {
case .bayonRegular:
return "Bayon Regular"
}
}
///
var fileName: String {
return self.rawValue
}
}
// MARK: -
///
/// - Parameters:
/// - customFont:
/// - size:
/// - Returns: Font
static func font(_ customFont: CustomFont, size: CGFloat) -> Font {
return Font.custom(customFont.rawValue, size: size)
}
///
/// - Parameters:
/// - customFont:
/// - designSize: 稿
/// - screenWidth:
/// - Returns: Font
static func adaptedFont(_ customFont: CustomFont, designSize: CGFloat, for screenWidth: CGFloat) -> Font {
let adaptedSize = ScreenAdapter.fontSize(designSize, for: screenWidth)
return Font.custom(customFont.rawValue, size: adaptedSize)
}
///
/// - Parameter customFont:
/// - Returns:
static func isFontAvailable(_ customFont: CustomFont) -> Bool {
let fontNames = UIFont.familyNames
.flatMap { UIFont.fontNames(forFamilyName: $0) }
return fontNames.contains(customFont.rawValue)
}
///
/// - Returns:
static func getAllAvailableFonts() -> [String] {
return UIFont.familyNames
.flatMap { family in
UIFont.fontNames(forFamilyName: family)
.map { _ in "\(family): \(String(describing: font))" }
}
.sorted()
}
///
static func printAllAvailableFonts() {
print("=== 所有可用字体 ===")
for font in getAllAvailableFonts() {
print(font)
}
print("==================")
}
}
// MARK: - SwiftUI View Extension
extension View {
///
/// - Parameters:
/// - customFont:
/// - size:
/// - Returns:
func customFont(_ customFont: FontManager.CustomFont, size: CGFloat) -> some View {
self.font(FontManager.font(customFont, size: size))
}
///
/// - Parameters:
/// - customFont:
/// - designSize: 稿
/// - Returns:
func adaptedCustomFont(_ customFont: FontManager.CustomFont, designSize: CGFloat) -> some View {
self.modifier(AdaptedCustomFontModifier(customFont: customFont, designSize: designSize))
}
}
// MARK: - ViewModifier
struct AdaptedCustomFontModifier: ViewModifier {
let customFont: FontManager.CustomFont
let designSize: CGFloat
func body(content: Content) -> some View {
GeometryReader { geometry in
content
.font(FontManager.adaptedFont(customFont, designSize: designSize, for: geometry.size.width))
}
}
}

View File

@@ -0,0 +1,135 @@
import Foundation
import SwiftUI
///
/// 便
///
///
/// -
/// -
/// - UserDefaults
class LocalizationManager: ObservableObject {
// MARK: -
static let shared = LocalizationManager()
// MARK: -
enum SupportedLanguage: String, CaseIterable {
case english = "en"
case chineseSimplified = "zh-Hans"
var displayName: String {
switch self {
case .english:
return "English"
case .chineseSimplified:
return "简体中文"
}
}
var localizedDisplayName: String {
switch self {
case .english:
return "English"
case .chineseSimplified:
return "简体中文"
}
}
}
// MARK: -
@Published var currentLanguage: SupportedLanguage {
didSet {
UserDefaults.standard.set(currentLanguage.rawValue, forKey: "AppLanguage")
//
objectWillChange.send()
}
}
private init() {
// UserDefaults
let savedLanguage = UserDefaults.standard.string(forKey: "AppLanguage") ?? ""
self.currentLanguage = SupportedLanguage(rawValue: savedLanguage) ?? .english
// 使
if savedLanguage.isEmpty {
self.currentLanguage = Self.getSystemPreferredLanguage()
}
}
// MARK: -
///
/// - Parameters:
/// - key: key
/// - arguments:
/// - Returns:
func localizedString(_ key: String, arguments: CVarArg...) -> String {
let format = getLocalizedString(for: key)
if arguments.isEmpty {
return format
} else {
return String(format: format, arguments: arguments)
}
}
///
private func getLocalizedString(for key: String) -> String {
guard let path = Bundle.main.path(forResource: currentLanguage.rawValue, ofType: "lproj"),
let bundle = Bundle(path: path) else {
// key
return NSLocalizedString(key, comment: "")
}
return NSLocalizedString(key, bundle: bundle, comment: "")
}
// MARK: -
///
/// - Parameter language:
func switchLanguage(to language: SupportedLanguage) {
currentLanguage = language
}
///
///
private static func getSystemPreferredLanguage() -> SupportedLanguage {
//
//
return .english
}
}
// MARK: - SwiftUI Extensions
extension View {
///
/// - Parameter key: key
/// - Returns:
func localized(_ key: String) -> some View {
self.modifier(LocalizedTextModifier(key: key))
}
}
///
struct LocalizedTextModifier: ViewModifier {
let key: String
@ObservedObject private var localizationManager = LocalizationManager.shared
func body(content: Content) -> some View {
content
}
}
// MARK: - 便
extension String {
///
var localized: String {
return LocalizationManager.shared.localizedString(self)
}
///
func localized(arguments: CVarArg...) -> String {
return LocalizationManager.shared.localizedString(self, arguments: arguments)
}
}

View File

@@ -0,0 +1,114 @@
import SwiftUI
///
/// 稿
struct ScreenAdapter {
// MARK: - 稿
/// 稿 (iPhone 14 Pro)
static let designWidth: CGFloat = 393
/// 稿 (iPhone 14 Pro)
static let designHeight: CGFloat = 852
// MARK: -
/// 稿
/// - Parameters:
/// - designValue: 稿
/// - screenWidth:
/// - Returns:
static func width(_ designValue: CGFloat, for screenWidth: CGFloat) -> CGFloat {
return designValue * (screenWidth / designWidth)
}
/// 稿
/// - Parameters:
/// - designValue: 稿
/// - screenHeight:
/// - Returns:
static func height(_ designValue: CGFloat, for screenHeight: CGFloat) -> CGFloat {
return designValue * (screenHeight / designHeight)
}
/// 稿
/// - Parameters:
/// - designFontSize: 稿
/// - screenWidth:
/// - Returns:
static func fontSize(_ designFontSize: CGFloat, for screenWidth: CGFloat) -> CGFloat {
return designFontSize * (screenWidth / designWidth)
}
/// ()
/// - Parameter screenWidth:
/// - Returns:
static func widthRatio(for screenWidth: CGFloat) -> CGFloat {
return screenWidth / designWidth
}
/// ()
/// - Parameter screenHeight:
/// - Returns:
static func heightRatio(for screenHeight: CGFloat) -> CGFloat {
return screenHeight / designHeight
}
}
// MARK: - SwiftUI View Extension
extension View {
/// 稿
/// - Parameter designValue: 稿
/// - Returns:
func adaptedWidth(_ designValue: CGFloat) -> some View {
self.modifier(AdaptedWidthModifier(designValue: designValue))
}
/// 稿
/// - Parameter designValue: 稿
/// - Returns:
func adaptedHeight(_ designValue: CGFloat) -> some View {
self.modifier(AdaptedHeightModifier(designValue: designValue))
}
/// 稿
/// - Parameter designFontSize: 稿
/// - Returns:
func adaptedFont(_ designFontSize: CGFloat, weight: Font.Weight = .regular) -> some View {
self.modifier(AdaptedFontModifier(designFontSize: designFontSize, weight: weight))
}
}
// MARK: - ViewModifiers
struct AdaptedWidthModifier: ViewModifier {
let designValue: CGFloat
func body(content: Content) -> some View {
GeometryReader { geometry in
content
.frame(width: ScreenAdapter.width(designValue, for: geometry.size.width))
}
}
}
struct AdaptedHeightModifier: ViewModifier {
let designValue: CGFloat
func body(content: Content) -> some View {
GeometryReader { geometry in
content
.padding(.top, ScreenAdapter.height(designValue, for: geometry.size.height))
}
}
}
struct AdaptedFontModifier: ViewModifier {
let designFontSize: CGFloat
let weight: Font.Weight
func body(content: Content) -> some View {
GeometryReader { geometry in
content
.font(.system(size: ScreenAdapter.fontSize(designFontSize, for: geometry.size.width), weight: weight))
}
}
}

View File

@@ -0,0 +1,54 @@
import SwiftUI
/// ScreenAdapter 使
/// SwiftUI 使
struct ScreenAdapterExample: View {
var body: some View {
GeometryReader { geometry in
VStack(spacing: 20) {
// 1: 使 ScreenAdapter
Text("方法1: 直接调用")
.font(.system(size: ScreenAdapter.fontSize(16, for: geometry.size.width)))
.padding(.leading, ScreenAdapter.width(20, for: geometry.size.width))
.padding(.top, ScreenAdapter.height(50, for: geometry.size.height))
// 2: 使 View Extension ()
Text("方法2: View Extension")
.adaptedFont(16)
.adaptedHeight(50)
// 3: 使
Text("方法3: 比例计算")
.font(.system(size: 16 * ScreenAdapter.widthRatio(for: geometry.size.width)))
.padding(.top, 50 * ScreenAdapter.heightRatio(for: geometry.size.height))
Spacer()
}
}
}
}
// MARK: - 使
/*
使
1. View Extension ()
.adaptedFont(16)
.adaptedHeight(20)
.adaptedWidth(100)
2. ()
.font(.system(size: ScreenAdapter.fontSize(16, for: geometry.size.width)))
.padding(.top, ScreenAdapter.height(20, for: geometry.size.height))
3. ()
let ratio = ScreenAdapter.heightRatio(for: geometry.size.height)
.padding(.top, 20 * ratio)
*/
#Preview {
ScreenAdapterExample()
}

View File

@@ -0,0 +1,19 @@
//
// AESUtils.h
// YUMI
//
// Created by YUMI on 2023/2/13.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface AESUtils : NSObject
//MARK: AES加解密
+ (NSString *)aesEncrypt:(NSString *)sourceStr;
+ (NSString *)aesDecrypt:(NSString *)secretStr;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,151 @@
//
// AESUtils.m
// YUMI
//
// Created by YUMI on 2023/2/13.
//
#import "AESUtils.h"
#import <CommonCrypto/CommonCrypto.h>
#define GL_AES_KEY @"aef01238765abcdeaaageggbeggsded"
#define GL_AES_IV @"edgcdgrtc"
@implementation AESUtils
//MARK: AES start
+ (NSString *)aesEncrypt:(NSString *)sourceStr {
if (!sourceStr) {
return nil;
}
//
char keyPtr[kCCKeySizeAES256 + 1];
bzero(keyPtr, sizeof(keyPtr));
[GL_AES_KEY getCString:keyPtr maxLength:sizeof(keyPtr) encoding:NSUTF8StringEncoding];
//
char ivPtr[kCCBlockSizeAES128 + 1];
bzero(ivPtr, sizeof(ivPtr));
[GL_AES_IV getCString:ivPtr maxLength:sizeof(ivPtr) encoding:NSUTF8StringEncoding];
NSData *sourceData = [sourceStr dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger dataLength = [sourceData length];
size_t buffersize = dataLength + kCCBlockSizeAES128;
void *buffer = malloc(buffersize);
size_t numBytesEncrypted = 0;
/*
//CBC
kCCOptionPKCS7Padding
//ECB
kCCOptionPKCS7Padding | kCCOptionECBMode
*/
CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt,
kCCAlgorithmAES128,
kCCOptionPKCS7Padding,
keyPtr,
kCCBlockSizeAES128,
ivPtr,//ECBNULL
[sourceData bytes],
dataLength,
buffer,
buffersize,
&numBytesEncrypted);
if (cryptStatus == kCCSuccess) {
NSData *encryptData = [NSData dataWithBytesNoCopy:buffer length:numBytesEncrypted];
//base64
//return [encryptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
//16
NSMutableString *output = [NSMutableString stringWithCapacity:encryptData.length * 2];
if (encryptData && encryptData.length > 0) {
Byte *datas = (Byte*)[encryptData bytes];
for(int i = 0; i < encryptData.length; i++){
[output appendFormat:@"%02x", datas[i]];
}
}
return output;
} else {
free(buffer);
return nil;
}
}
+ (NSString *)aesDecrypt:(NSString *)secretStr {
if (!secretStr) {
return nil;
}
// //base64
NSData *decodeData = [[NSData alloc] initWithBase64EncodedString:secretStr options:NSDataBase64DecodingIgnoreUnknownCharacters];
//16
// NSData *decodeData = [self convertHexStrToData:secretStr];
//
char keyPtr[kCCKeySizeAES256 + 1];
bzero(keyPtr, sizeof(keyPtr));
[GL_AES_KEY getCString:keyPtr maxLength:sizeof(keyPtr) encoding:NSUTF8StringEncoding];
//
char ivPtr[kCCBlockSizeAES128 + 1];
bzero(ivPtr, sizeof(ivPtr));
[GL_AES_IV getCString:ivPtr maxLength:sizeof(ivPtr) encoding:NSUTF8StringEncoding];
NSUInteger dataLength = [decodeData length];
size_t bufferSize = dataLength + kCCBlockSizeAES128;
void *buffer = malloc(bufferSize);
size_t numBytesDecrypted = 0;
/*
//CBC
kCCOptionPKCS7Padding
//ECB
kCCOptionPKCS7Padding | kCCOptionECBMode
*/
CCCryptorStatus cryptStatus = CCCrypt(kCCDecrypt,
kCCAlgorithmAES128,
kCCOptionPKCS7Padding,
keyPtr,
kCCBlockSizeAES128,
ivPtr,//ECBNULL
[decodeData bytes],
dataLength,
buffer,
bufferSize,
&numBytesDecrypted);
if (cryptStatus == kCCSuccess) {
NSData *data = [NSData dataWithBytesNoCopy:buffer length:numBytesDecrypted];
NSString *result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
return result;
} else {
free(buffer);
return nil;
}
}
// 16NSData
+ (NSData *)convertHexStrToData:(NSString *)str {
if (!str || [str length] == 0) {
return nil;
}
NSMutableData *hexData = [[NSMutableData alloc] initWithCapacity:20];
NSRange range;
if ([str length] % 2 == 0) {
range = NSMakeRange(0, 2);
} else {
range = NSMakeRange(0, 1);
}
for (NSInteger i = range.location; i < [str length]; i += 2) {
unsigned int anInt;
NSString *hexCharStr = [str substringWithRange:range];
NSScanner *scanner = [[NSScanner alloc] initWithString:hexCharStr];
[scanner scanHexInt:&anInt];
NSData *entity = [[NSData alloc] initWithBytes:&anInt length:1];
[hexData appendData:entity];
range.location += range.length;
range.length = 2;
}
return hexData;
}
@end

View File

@@ -0,0 +1,16 @@
//
// Base64.h
// YMhatFramework
//
// Created by chenran on 2017/5/4.
// Copyright © 2017年 chenran. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface Base64 : NSObject
+(NSString *)encode:(NSData *)data;
+(NSData *)decode:(NSString *)dataString;
@end

View File

@@ -0,0 +1,133 @@
//
// Base64.m
// YMhatFramework
//
// Created by chenran on 2017/5/4.
// Copyright © 2017 chenran. All rights reserved.
//
#import "Base64.h"
@interface Base64()
+(int)char2Int:(char)c;
@end
@implementation Base64
static const char encodingTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+(NSString *)encode:(NSData *)data
{
if (data.length == 0)
return nil;
char *characters = malloc(data.length * 3 / 2);
if (characters == NULL)
return nil;
int end = data.length - 3;
int index = 0;
int charCount = 0;
int n = 0;
while (index <= end) {
int d = (((int)(((char *)[data bytes])[index]) & 0x0ff) << 16)
| (((int)(((char *)[data bytes])[index + 1]) & 0x0ff) << 8)
| ((int)(((char *)[data bytes])[index + 2]) & 0x0ff);
characters[charCount++] = encodingTable[(d >> 18) & 63];
characters[charCount++] = encodingTable[(d >> 12) & 63];
characters[charCount++] = encodingTable[(d >> 6) & 63];
characters[charCount++] = encodingTable[d & 63];
index += 3;
if(n++ >= 14)
{
n = 0;
characters[charCount++] = ' ';
}
}
if(index == data.length - 2)
{
int d = (((int)(((char *)[data bytes])[index]) & 0x0ff) << 16)
| (((int)(((char *)[data bytes])[index + 1]) & 255) << 8);
characters[charCount++] = encodingTable[(d >> 18) & 63];
characters[charCount++] = encodingTable[(d >> 12) & 63];
characters[charCount++] = encodingTable[(d >> 6) & 63];
characters[charCount++] = '=';
}
else if(index == data.length - 1)
{
int d = ((int)(((char *)[data bytes])[index]) & 0x0ff) << 16;
characters[charCount++] = encodingTable[(d >> 18) & 63];
characters[charCount++] = encodingTable[(d >> 12) & 63];
characters[charCount++] = '=';
characters[charCount++] = '=';
}
NSString * rtnStr = [[NSString alloc] initWithBytesNoCopy:characters length:charCount encoding:NSUTF8StringEncoding freeWhenDone:YES];
return rtnStr;
}
+(NSData *)decode:(NSString *)data
{
if(data == nil || data.length <= 0) {
return nil;
}
NSMutableData *rtnData = [[NSMutableData alloc]init];
int slen = data.length;
int index = 0;
while (true) {
while (index < slen && [data characterAtIndex:index] <= ' ') {
index++;
}
if (index >= slen || index + 3 >= slen) {
break;
}
int byte = ([self char2Int:[data characterAtIndex:index]] << 18) + ([self char2Int:[data characterAtIndex:index + 1]] << 12) + ([self char2Int:[data characterAtIndex:index + 2]] << 6) + [self char2Int:[data characterAtIndex:index + 3]];
Byte temp1 = (byte >> 16) & 255;
[rtnData appendBytes:&temp1 length:1];
if([data characterAtIndex:index + 2] == '=') {
break;
}
Byte temp2 = (byte >> 8) & 255;
[rtnData appendBytes:&temp2 length:1];
if([data characterAtIndex:index + 3] == '=') {
break;
}
Byte temp3 = byte & 255;
[rtnData appendBytes:&temp3 length:1];
index += 4;
}
return rtnData;
}
+(int)char2Int:(char)c
{
if (c >= 'A' && c <= 'Z') {
return c - 65;
} else if (c >= 'a' && c <= 'z') {
return c - 97 + 26;
} else if (c >= '0' && c <= '9') {
return c - 48 + 26 + 26;
} else {
switch(c) {
case '+':
return 62;
case '/':
return 63;
case '=':
return 0;
default:
return -1;
}
}
}
@end

View File

@@ -0,0 +1,16 @@
//
// DESEncrypt.h
// YMhatFramework
//
// Created by chenran on 2017/5/4.
// Copyright © 2017年 chenran. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface DESEncrypt : NSObject
//加密方法
+(NSString *) encryptUseDES:(NSString *)plainText key:(NSString *)key;
//解密方法
+(NSString *) decryptUseDES:(NSString *)cipherText key:(NSString *)key;
@end

View File

@@ -0,0 +1,63 @@
//
// DESEncrypt.m
// YMhatFramework
//
// Created by chenran on 2017/5/4.
// Copyright © 2017 chenran. All rights reserved.
//
#import "DESEncrypt.h"
#import <CommonCrypto/CommonCrypto.h>
#import "Base64.h"
@implementation DESEncrypt : NSObject
const Byte iv[] = {1,2,3,4,5,6,7,8};
#pragma mark-
+(NSString *) encryptUseDES:(NSString *)plainText key:(NSString *)key
{
NSString *ciphertext = nil;
NSData *textData = [plainText dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger dataLength = [textData length];
unsigned char buffer[200000];
memset(buffer, 0, sizeof(char));
size_t numBytesEncrypted = 0;
CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt, kCCAlgorithmDES,
kCCOptionPKCS7Padding|kCCOptionECBMode,
[key UTF8String], kCCKeySizeDES,
iv,
[textData bytes], dataLength,
buffer, 200000,
&numBytesEncrypted);
if (cryptStatus == kCCSuccess) {
NSData *data = [NSData dataWithBytes:buffer length:(NSUInteger)numBytesEncrypted];
ciphertext = [Base64 encode:data];
}
return ciphertext;
}
#pragma mark-
+(NSString *)decryptUseDES:(NSString *)cipherText key:(NSString *)key
{
NSString *plaintext = nil;
NSData *cipherdata = [Base64 decode:cipherText];
unsigned char buffer[200000];
memset(buffer, 0, sizeof(char));
size_t numBytesDecrypted = 0;
// kCCOptionPKCS7Padding|kCCOptionECBMode
CCCryptorStatus cryptStatus = CCCrypt(kCCDecrypt, kCCAlgorithmDES,
kCCOptionPKCS7Padding|kCCOptionECBMode,
[key UTF8String], kCCKeySizeDES,
iv,
[cipherdata bytes], [cipherdata length],
buffer, 200000,
&numBytesDecrypted);
if(cryptStatus == kCCSuccess) {
NSData *plaindata = [NSData dataWithBytes:buffer length:(NSUInteger)numBytesDecrypted];
plaintext = [[NSString alloc]initWithData:plaindata encoding:NSUTF8StringEncoding];
}
return plaintext;
}
@end

View File

@@ -0,0 +1,51 @@
import Foundation
/// OCDES
struct DESEncryptOCTest {
/// OC DES
static func testOCDESEncryption() {
print("🧪 开始测试 OC 版本的 DES 加密...")
print(String(repeating: "=", count: 50))
let key = "1ea53d260ecf11e7b56e00163e046a26"
let testCases = [
"test123",
"hello world",
"password123",
"sample_data",
"encrypt_test"
]
for testCase in testCases {
if let encrypted = DESEncrypt.encryptUseDES(testCase, key: key) {
print("✅ 加密成功:")
print(" 原文: \"\(testCase)\"")
print(" 密文: \(encrypted)")
//
if let decrypted = DESEncrypt.decryptUseDES(encrypted, key: key) {
let isMatch = decrypted == testCase
print(" 解密: \"\(decrypted)\" \(isMatch ? "" : "")")
} else {
print(" 解密: 失败 ❌")
}
} else {
print("❌ 加密失败: \"\(testCase)\"")
}
print()
}
print(String(repeating: "=", count: 50))
print("🏁 OC版本DES加密测试完成")
}
}
#if DEBUG
extension DESEncryptOCTest {
/// AppDelegate
static func runInAppDelegate() {
DESEncryptOCTest.testOCDESEncryption()
}
}
#endif

View File

@@ -0,0 +1,43 @@
import SwiftUI
import ComposableArchitecture
struct AppRootView: View {
@State private var shouldShowMainApp = false
let splashStore = Store(
initialState: SplashFeature.State()
) {
SplashFeature()
}
let loginStore = Store(
initialState: LoginFeature.State()
) {
LoginFeature()
}
var body: some View {
Group {
if shouldShowMainApp {
//
LoginView(store: loginStore)
.transition(.opacity.animation(.easeInOut(duration: 0.5)))
} else {
//
SplashView(store: splashStore)
.transition(.opacity.animation(.easeInOut(duration: 0.5)))
.onReceive(NotificationCenter.default.publisher(for: .splashFinished)) { _ in
shouldShowMainApp = true
}
}
}
}
}
extension Notification.Name {
static let splashFinished = Notification.Name("splashFinished")
}
#Preview {
AppRootView()
}

View File

@@ -0,0 +1,59 @@
import SwiftUI
// MARK: - Login Button Component
struct LoginButton: View {
let iconName: String
let iconColor: Color
let title: String
let action: () -> Void
var body: some View {
Button(action: action) {
ZStack {
//
Color.white
.cornerRadius(28)
//
Text(title)
.font(.system(size: 18, weight: .semibold))
.frame(alignment: .center)
.foregroundColor(Color(hex: 0x313131))
//
HStack {
Image(systemName: iconName)
.foregroundColor(iconColor)
.font(.system(size: 30))
.padding(.leading, 33)
Spacer()
}
}
.frame(height: 56)
.padding(.horizontal, 29)
}
}
}
#Preview {
VStack(spacing: 20) {
LoginButton(
iconName: "person.circle.fill",
iconColor: .green,
title: "ID Login"
) {
// Preview action
}
LoginButton(
iconName: "envelope.fill",
iconColor: .blue,
title: "Email Login"
) {
// Preview action
}
}
.padding()
.background(Color.gray.opacity(0.2))
}

View File

@@ -0,0 +1,88 @@
import SwiftUI
// MARK: - User Agreement View Component
struct UserAgreementView: View {
@Binding var isAgreed: Bool
let onUserServiceTapped: () -> Void
let onPrivacyPolicyTapped: () -> Void
var body: some View {
HStack(alignment: .center, spacing: 12) {
//
Button(action: {
isAgreed.toggle()
}) {
Image(systemName: isAgreed ? "checkmark.circle.fill" : "circle")
.font(.system(size: 22))
.foregroundColor(isAgreed ? Color(hex: 0x8A4FFF) : Color(hex: 0x666666))
}
//
Text(createAttributedText())
.font(.system(size: 14))
.multilineTextAlignment(.leading)
.environment(\.openURL, OpenURLAction { url in
if url.absoluteString == "user-service-agreement" {
onUserServiceTapped()
return .handled
} else if url.absoluteString == "privacy-policy" {
onPrivacyPolicyTapped()
return .handled
}
return .systemAction
})
}
.frame(maxWidth: .infinity) //
.padding(.horizontal, 29) //
}
// MARK: - Private Methods
private func createAttributedText() -> AttributedString {
var attributedString = AttributedString("login.agreement_policy".localized)
//
attributedString.foregroundColor = Color(hex: 0x666666)
// ""
if let userServiceRange = attributedString.range(of: "login.agreement".localized) {
attributedString[userServiceRange].foregroundColor = Color(hex: 0x8A4FFF)
attributedString[userServiceRange].underlineStyle = .single
attributedString[userServiceRange].link = URL(string: "user-service-agreement")
}
// ""
if let privacyPolicyRange = attributedString.range(of: "login.policy".localized) {
attributedString[privacyPolicyRange].foregroundColor = Color(hex: 0x8A4FFF)
attributedString[privacyPolicyRange].underlineStyle = .single
attributedString[privacyPolicyRange].link = URL(string: "privacy-policy")
}
return attributedString
}
}
#Preview {
VStack(spacing: 20) {
UserAgreementView(
isAgreed: .constant(true),
onUserServiceTapped: {
print("User Service Agreement tapped")
},
onPrivacyPolicyTapped: {
print("Privacy Policy tapped")
}
)
UserAgreementView(
isAgreed: .constant(true),
onUserServiceTapped: {
print("User Service Agreement tapped")
},
onPrivacyPolicyTapped: {
print("Privacy Policy tapped")
}
)
}
.padding()
.background(Color.gray.opacity(0.1))
}

View File

@@ -0,0 +1,55 @@
import SwiftUI
import SafariServices
// MARK: - Web View Component
struct WebView: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context: Context) -> SFSafariViewController {
let config = SFSafariViewController.Configuration()
config.entersReaderIfAvailable = false
config.barCollapsingEnabled = true
let safariViewController = SFSafariViewController(url: url, configuration: config)
safariViewController.preferredBarTintColor = UIColor.systemBackground
safariViewController.preferredControlTintColor = UIColor.systemBlue
return safariViewController
}
func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) {
// Safari View Controller
}
}
// MARK: - Web View Modifier
extension View {
/// Web
/// - Parameters:
/// - isPresented:
/// - url: URL
/// - Returns:
func webView(isPresented: Binding<Bool>, url: URL?) -> some View {
self.sheet(isPresented: isPresented) {
if let url = url {
WebView(url: url)
} else {
Text("无法加载页面")
.foregroundColor(.red)
.padding()
}
}
}
}
#Preview {
VStack {
Button("打开网页") {
//
}
}
.webView(
isPresented: .constant(true),
url: URL(string: "https://www.apple.com")
)
}

View File

@@ -0,0 +1,214 @@
import SwiftUI
import ComposableArchitecture
struct EMailLoginView: View {
let store: StoreOf<EMailLoginFeature>
let onBack: () -> Void
// 使@StateUI
@State private var email: String = ""
@State private var verificationCode: String = ""
//
private var isLoginButtonEnabled: Bool {
return !store.isLoading && !email.isEmpty && !verificationCode.isEmpty
}
//
private var getCodeButtonText: String {
if store.isCodeLoading {
return ""
} else if store.codeCountdown > 0 {
return "\(store.codeCountdown)S"
} else {
return "email_login.get_code".localized
}
}
var body: some View {
GeometryReader { geometry in
ZStack {
// - 使"bg"
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.all)
VStack(spacing: 0) {
//
HStack {
Button(action: {
onBack()
}) {
Image(systemName: "chevron.left")
.font(.system(size: 24, weight: .medium))
.foregroundColor(.white)
.frame(width: 44, height: 44)
}
Spacer()
}
.padding(.horizontal, 16)
.padding(.top, 8)
Spacer()
.frame(height: 60)
//
Text("email_login.title".localized)
.font(.system(size: 28, weight: .medium))
.foregroundColor(.white)
.padding(.bottom, 80)
//
VStack(spacing: 24) {
//
ZStack {
RoundedRectangle(cornerRadius: 25)
.fill(Color.white.opacity(0.1))
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.white.opacity(0.3), lineWidth: 1)
)
.frame(height: 56)
TextField("", text: $email)
.placeholder(when: email.isEmpty) {
Text("placeholder.enter_email".localized)
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
.padding(.horizontal, 24)
.keyboardType(.emailAddress)
.autocapitalization(.none)
.disableAutocorrection(true)
}
//
ZStack {
RoundedRectangle(cornerRadius: 25)
.fill(Color.white.opacity(0.1))
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.white.opacity(0.3), lineWidth: 1)
)
.frame(height: 56)
HStack {
TextField("", text: $verificationCode)
.placeholder(when: verificationCode.isEmpty) {
Text("placeholder.enter_verification_code".localized)
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
.keyboardType(.numberPad)
//
Button(action: {
store.send(.getVerificationCodeTapped)
}) {
ZStack {
if store.isCodeLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.7)
} else {
Text(getCodeButtonText)
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white)
}
}
.frame(width: 60, height: 36)
.background(
RoundedRectangle(cornerRadius: 18)
.fill(Color.white.opacity(store.isCodeButtonEnabled && !email.isEmpty ? 0.2 : 0.1))
)
}
.disabled(!store.isCodeButtonEnabled || email.isEmpty || store.isCodeLoading)
}
.padding(.horizontal, 24)
}
}
.padding(.horizontal, 32)
Spacer()
.frame(height: 60)
//
Button(action: {
// action
store.send(.loginButtonTapped(email: email, verificationCode: verificationCode))
}) {
ZStack {
//
LinearGradient(
colors: [
Color(red: 0.85, green: 0.37, blue: 1.0), // #D85EFF
Color(red: 0.54, green: 0.31, blue: 1.0) // #8A4FFF
],
startPoint: .leading,
endPoint: .trailing
)
.clipShape(RoundedRectangle(cornerRadius: 28))
HStack {
if store.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
}
Text(store.isLoading ? "email_login.logging_in".localized : "email_login.login_button".localized)
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
}
}
.frame(height: 56)
}
.disabled(store.isLoading || email.isEmpty || verificationCode.isEmpty)
.opacity(isLoginButtonEnabled ? 1.0 : 0.5)
.padding(.horizontal, 32)
//
if let errorMessage = store.errorMessage {
Text(errorMessage)
.font(.system(size: 14))
.foregroundColor(.red)
.padding(.top, 16)
.padding(.horizontal, 32)
}
Spacer()
}
}
}
.onAppear {
// TCA
email = store.email
verificationCode = store.verificationCode
#if DEBUG
// Debug
if email.isEmpty {
email = "85494536@gmail.com"
}
if verificationCode.isEmpty {
verificationCode = "784544"
}
print("🐛 Debug模式: 默认邮箱=\(email), 默认验证码=\(verificationCode)")
#endif
}
}
}
#Preview {
EMailLoginView(
store: Store(
initialState: EMailLoginFeature.State()
) {
EMailLoginFeature()
},
onBack: {}
)
}

View File

@@ -0,0 +1,205 @@
import SwiftUI
import ComposableArchitecture
struct IDLoginView: View {
let store: StoreOf<IDLoginFeature>
let onBack: () -> Void
// 使@StateUI
@State private var userID: String = ""
@State private var password: String = ""
@State private var isPasswordVisible: Bool = false
//
private var isLoginButtonEnabled: Bool {
return !store.isLoading && !userID.isEmpty && !password.isEmpty
}
var body: some View {
GeometryReader { geometry in
ZStack {
// - 使"bg"
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.all)
VStack(spacing: 0) {
//
HStack {
Button(action: {
onBack()
}) {
Image(systemName: "chevron.left")
.font(.system(size: 24, weight: .medium))
.foregroundColor(.white)
.frame(width: 44, height: 44)
}
Spacer()
}
.padding(.horizontal, 16)
.padding(.top, 8)
Spacer()
.frame(height: 60)
//
Text("id_login.title".localized)
.font(.system(size: 28, weight: .medium))
.foregroundColor(.white)
.padding(.bottom, 80)
//
VStack(spacing: 24) {
// ID
ZStack {
RoundedRectangle(cornerRadius: 25)
.fill(Color.white.opacity(0.1))
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.white.opacity(0.3), lineWidth: 1)
)
.frame(height: 56)
TextField("", text: $userID) // 使SwiftUI
.placeholder(when: userID.isEmpty) {
Text("placeholder.enter_id".localized)
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
.padding(.horizontal, 24)
.keyboardType(.numberPad)
}
//
ZStack {
RoundedRectangle(cornerRadius: 25)
.fill(Color.white.opacity(0.1))
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.white.opacity(0.3), lineWidth: 1)
)
.frame(height: 56)
HStack {
if isPasswordVisible {
TextField("", text: $password) // 使SwiftUI
.placeholder(when: password.isEmpty) {
Text("placeholder.enter_password".localized)
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
} else {
SecureField("", text: $password) // 使SwiftUI
.placeholder(when: password.isEmpty) {
Text("placeholder.enter_password".localized)
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
}
Button(action: {
isPasswordVisible.toggle()
}) {
Image(systemName: isPasswordVisible ? "eye.slash" : "eye")
.foregroundColor(.white.opacity(0.7))
.font(.system(size: 18))
}
}
.padding(.horizontal, 24)
}
}
.padding(.horizontal, 32)
// Forgot Password
HStack {
Spacer()
Button(action: {
store.send(.forgotPasswordTapped)
}) {
Text("id_login.forgot_password".localized)
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.8))
}
}
.padding(.horizontal, 32)
.padding(.top, 16)
Spacer()
.frame(height: 60)
//
Button(action: {
// action
store.send(.loginButtonTapped(userID: userID, password: password))
}) {
ZStack {
//
LinearGradient(
colors: [
Color(red: 0.85, green: 0.37, blue: 1.0), // #D85EFF
Color(red: 0.54, green: 0.31, blue: 1.0) // #8A4FFF
],
startPoint: .leading,
endPoint: .trailing
)
.clipShape(RoundedRectangle(cornerRadius: 28))
HStack {
if store.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
}
Text(store.isLoading ? "id_login.logging_in".localized : "id_login.login_button".localized)
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
}
}
.frame(height: 56)
}
.disabled(store.isLoading || userID.isEmpty || password.isEmpty)
.opacity(isLoginButtonEnabled ? 1.0 : 0.5) // 50%
.padding(.horizontal, 32)
//
if let errorMessage = store.errorMessage {
Text(errorMessage)
.font(.system(size: 14))
.foregroundColor(.red)
.padding(.top, 16)
.padding(.horizontal, 32)
}
Spacer()
}
}
}
.onAppear {
// TCA
userID = store.userID
password = store.password
isPasswordVisible = store.isPasswordVisible
#if DEBUG
//
print("🐛 Debug模式: 已移除硬编码测试凭据")
#endif
}
}
}
#Preview {
IDLoginView(
store: Store(
initialState: IDLoginFeature.State()
) {
IDLoginFeature()
},
onBack: {}
)
}

View File

@@ -0,0 +1,96 @@
import SwiftUI
import ComposableArchitecture
struct LanguageSettingsView: View {
@ObservedObject private var localizationManager = LocalizationManager.shared
@Binding var isPresented: Bool
init(isPresented: Binding<Bool> = .constant(true)) {
self._isPresented = isPresented
}
var body: some View {
NavigationView {
List {
Section {
ForEach(LocalizationManager.SupportedLanguage.allCases, id: \.rawValue) { language in
LanguageRow(
language: language,
isSelected: localizationManager.currentLanguage == language
) {
localizationManager.switchLanguage(to: language)
}
}
} header: {
Text("选择语言 / Select Language")
.font(.caption)
.foregroundColor(.secondary)
}
Section {
HStack {
Text("当前语言 / Current Language")
.font(.body)
Spacer()
Text(localizationManager.currentLanguage.localizedDisplayName)
.font(.body)
.foregroundColor(.blue)
}
} header: {
Text("语言信息 / Language Info")
.font(.caption)
.foregroundColor(.secondary)
}
}
.navigationTitle("语言设置 / Language")
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("返回 / Back") {
isPresented = false
}
}
}
}
}
}
struct LanguageRow: View {
let language: LocalizationManager.SupportedLanguage
let isSelected: Bool
let onTap: () -> Void
var body: some View {
Button(action: onTap) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(language.localizedDisplayName)
.font(.body)
.foregroundColor(.primary)
Text(language.displayName)
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
if isSelected {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.blue)
.font(.system(size: 20))
}
}
.contentShape(Rectangle())
}
.buttonStyle(PlainButtonStyle())
}
}
// MARK: - Preview
#Preview {
LanguageSettingsView(isPresented: .constant(true))
}

160
yana/Views/LoginView.swift Normal file
View File

@@ -0,0 +1,160 @@
import SwiftUI
import ComposableArchitecture
// PreferenceKey
struct ImageHeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
struct LoginView: View {
let store: StoreOf<LoginFeature>
@State private var topImageHeight: CGFloat = 120 //
@ObservedObject private var localizationManager = LocalizationManager.shared
@State private var showLanguageSettings = false
@State private var isAgreedToTerms = true
@State private var showUserAgreement = false
@State private var showPrivacyPolicy = false
@State private var showIDLogin = false // 使SwiftUI@State
var body: some View {
NavigationView {
GeometryReader { geometry in
ZStack {
// 使 splash
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.all)
VStack(spacing: 0) {
// "top"
ZStack {
Image("top")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity)
.padding(.top, -100)
.background(
GeometryReader { topImageGeometry in
Color.clear.preference(key: ImageHeightPreferenceKey.self, value: topImageGeometry.size.height)
}
)
// E-PARTI "top"20
HStack {
Text("login.app_title".localized)
.font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width))
.foregroundColor(.white)
.padding(.leading, 20)
Spacer()
}
.padding(.top, max(0, topImageHeight - 100)) // top - 140
// - Debug
#if DEBUG
VStack {
HStack {
Spacer()
Button(action: {
showLanguageSettings = true
}) {
Image(systemName: "globe")
.frame(width: 40, height: 40)
.font(.system(size: 20))
.foregroundColor(.white)
.background(Color.black.opacity(0.3))
.clipShape(Circle())
}
.padding(.trailing, 16)
}
Spacer()
}
#endif
VStack(spacing: 24) {
// ID Login
LoginButton(
iconName: "person.circle.fill",
iconColor: .green,
title: "login.id_login".localized
) {
showIDLogin = true // SwiftUI
}
// Email Login
LoginButton(
iconName: "envelope.fill",
iconColor: .blue,
title: "login.email_login".localized
) {
// TODO: Email
}
}.padding(.top, max(0, topImageHeight+140))
}
.onPreferenceChange(ImageHeightPreferenceKey.self) { imageHeight in
topImageHeight = imageHeight
}
// 使"top"40pt
Spacer()
.frame(height: 120)
//
UserAgreementView(
isAgreed: $isAgreedToTerms,
onUserServiceTapped: {
showUserAgreement = true
},
onPrivacyPolicyTapped: {
showPrivacyPolicy = true
}
)
.padding(.horizontal, 28)
.padding(.bottom, 140)
}
// NavigationLink - 使SwiftUI
NavigationLink(
destination: IDLoginView(
store: store.scope(
state: \.idLoginState,
action: \.idLogin
),
onBack: {
showIDLogin = false // SwiftUI
}
)
.navigationBarHidden(true),
isActive: $showIDLogin // 使SwiftUI
) {
EmptyView()
}
.hidden()
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
.sheet(isPresented: $showLanguageSettings) {
LanguageSettingsView(isPresented: $showLanguageSettings)
}
.webView(
isPresented: $showUserAgreement,
url: APIConfiguration.webURL(for: .userAgreement)
)
.webView(
isPresented: $showPrivacyPolicy,
url: APIConfiguration.webURL(for: .privacyPolicy)
)
}
}
#Preview {
LoginView(
store: Store(
initialState: LoginFeature.State()
) {
LoginFeature()
}
)
}

View File

@@ -0,0 +1,49 @@
import SwiftUI
import ComposableArchitecture
struct SplashView: View {
let store: StoreOf<SplashFeature>
var body: some View {
WithPerceptionTracking {
ZStack {
// -
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.all)
VStack(spacing: 32) {
Spacer()
.frame(height: 200) // storyboard
// Logo - 100x100
Image("logo")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100)
// - 40pt
Text("E-Parti")
.font(.system(size: 40, weight: .regular))
.foregroundColor(.white)
Spacer()
}
}
.onAppear {
store.send(.onAppear)
}
}
}
}
#Preview {
SplashView(
store: Store(
initialState: SplashFeature.State()
) {
SplashFeature()
}
)
}

View File

@@ -2,3 +2,10 @@
// Use this file to import your target's public headers that you would like to expose to Swift.
//
// DES 加密相关 OC 文件
#import "DESEncrypt.h"
#import "Base64.h"
// AES 加密相关 OC 文件
#import "AESUtils.h"

View File

@@ -1,8 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.external-accessory.wireless-configuration</key>
<true/>
</dict>
<dict/>
</plist>

View File

@@ -12,25 +12,21 @@ import ComposableArchitecture
struct yanaApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
init() {
// SwiftUI Previews (DEBUG)
#if DEBUG
// SwiftUI Previews
if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == nil {
// Previews
}
#endif
print("🛠 原生URLSession测试开始")
}
var body: some Scene {
WindowGroup {
ContentView(
store: Store(
initialState: LoginFeature.State()
) {
LoginFeature()
},
initStore: Store(
initialState: InitFeature.State()
) {
InitFeature()
},
configStore: Store(
initialState: ConfigFeature.State()
) {
ConfigFeature()
}
)
AppRootView()
}
}
}