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

@@ -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
}
}
}
}