feat: 更新.gitignore,删除需求文档,优化API调试信息
- 在.gitignore中添加忽略项以排除不必要的文件。 - 删除架构分析需求文档以简化项目文档。 - 在APIEndpoints.swift和LoginModels.swift中移除调试信息的异步调用,提升代码简洁性。 - 在EMailLoginFeature.swift和HomeFeature.swift中新增登录流程状态管理,优化用户体验。 - 在多个视图中调整状态管理和导航逻辑,确保一致性和可维护性。 - 更新Xcode项目配置以增强调试信息的输出格式。
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,3 +10,5 @@ yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkp
|
||||
yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
|
||||
Doc
|
||||
DerivedData
|
||||
.kiro
|
||||
yana.xcworkspace/xcuserdata
|
||||
|
@@ -1,51 +0,0 @@
|
||||
# Requirements Document
|
||||
|
||||
## Introduction
|
||||
|
||||
This document outlines the requirements for analyzing the architecture of the Yana iOS application and providing recommendations for improvements. The analysis will focus on evaluating the current architecture, identifying strengths and weaknesses, and suggesting enhancements to improve code quality, maintainability, and scalability.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1
|
||||
|
||||
**User Story:** As a developer, I want to understand the current architecture of the Yana iOS application, so that I can identify areas for improvement.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN analyzing the project structure THEN the system SHALL identify the main architectural patterns used
|
||||
2. WHEN reviewing the codebase THEN the system SHALL document the key components and their relationships
|
||||
3. WHEN examining the dependencies THEN the system SHALL list all major frameworks and libraries used
|
||||
4. WHEN evaluating the project organization THEN the system SHALL assess the folder structure and file organization
|
||||
|
||||
### Requirement 2
|
||||
|
||||
**User Story:** As a developer, I want to identify strengths and weaknesses in the current architecture, so that I can leverage strengths and address weaknesses.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN reviewing the architecture THEN the system SHALL highlight positive architectural decisions
|
||||
2. WHEN analyzing the code structure THEN the system SHALL identify potential architectural issues
|
||||
3. WHEN examining the codebase THEN the system SHALL evaluate code consistency and adherence to best practices
|
||||
4. WHEN assessing the architecture THEN the system SHALL identify potential bottlenecks or scalability concerns
|
||||
|
||||
### Requirement 3
|
||||
|
||||
**User Story:** As a developer, I want specific recommendations for architectural improvements, so that I can enhance the application's maintainability and scalability.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN providing recommendations THEN the system SHALL suggest specific architectural improvements
|
||||
2. WHEN suggesting changes THEN the system SHALL explain the benefits of each recommendation
|
||||
3. WHEN recommending improvements THEN the system SHALL consider the existing technology stack and constraints
|
||||
4. WHEN proposing architectural changes THEN the system SHALL prioritize recommendations based on impact and effort
|
||||
|
||||
### Requirement 4
|
||||
|
||||
**User Story:** As a developer, I want to understand how to implement the recommended architectural improvements, so that I can effectively enhance the application.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN recommending architectural changes THEN the system SHALL provide implementation guidance
|
||||
2. WHEN suggesting improvements THEN the system SHALL include code examples where appropriate
|
||||
3. WHEN proposing architectural changes THEN the system SHALL outline a phased approach for implementation
|
||||
4. WHEN recommending improvements THEN the system SHALL consider backward compatibility and migration strategies
|
@@ -471,6 +471,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = Z7UCRF23F3;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
|
@@ -1,178 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Bucket
|
||||
uuid = "A60FAB2A-3184-45B2-920F-A3D7A086CF95"
|
||||
type = "0"
|
||||
version = "2.0">
|
||||
<Breakpoints>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "55EE8FC9-7BA5-40D2-B71E-7199EFC95B12"
|
||||
shouldBeEnabled = "Yes"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "yana/Views/FeedView.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "102"
|
||||
endingLineNumber = "102"
|
||||
landmarkName = "body"
|
||||
landmarkType = "24">
|
||||
<Locations>
|
||||
<Location
|
||||
uuid = "55EE8FC9-7BA5-40D2-B71E-7199EFC95B12 - 6293d947a1803ee3"
|
||||
shouldBeEnabled = "Yes"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
symbolName = "closure #1 (SwiftUI.GeometryProxy) -> <<opaque return type of SwiftUI.View.sheet<τ_0_0 where τ_1_0: SwiftUI.View>(isPresented: SwiftUI.Binding<Swift.Bool>, onDismiss: Swift.Optional<() -> ()>, content: () -> τ_1_0) -> some>>.0 in closure #1 () -> SwiftUI.GeometryReader<<<opaque return type of SwiftUI.View.sheet<τ_0_0 where τ_1_0: SwiftUI.View>(isPresented: SwiftUI.Binding<Swift.Bool>, onDismiss: Swift.Optional<() -> ()>, content: () -> τ_1_0) -> some>>.0> in yana.FeedView.body.getter : some"
|
||||
moduleName = "yana.debug.dylib"
|
||||
usesParentBreakpointCondition = "Yes"
|
||||
urlString = "file:///Users/edwinqqq/Local/Company%20Projects/yana/yana/Views/FeedView.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "102"
|
||||
endingLineNumber = "102">
|
||||
</Location>
|
||||
<Location
|
||||
uuid = "55EE8FC9-7BA5-40D2-B71E-7199EFC95B12 - ba104df0a01f94b"
|
||||
shouldBeEnabled = "Yes"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
symbolName = "closure #4 @Sendable () -> Swift.Bool in closure #1 (SwiftUI.GeometryProxy) -> <<opaque return type of SwiftUI.View.sheet<τ_0_0 where τ_1_0: SwiftUI.View>(isPresented: SwiftUI.Binding<Swift.Bool>, onDismiss: Swift.Optional<() -> ()>, content: () -> τ_1_0) -> some>>.0 in closure #1 () -> SwiftUI.GeometryReader<<<opaque return type of SwiftUI.View.sheet<τ_0_0 where τ_1_0: SwiftUI.View>(isPresented: SwiftUI.Binding<Swift.Bool>, onDismiss: Swift.Optional<() -> ()>, content: () -> τ_1_0) -> some>>.0> in yana.FeedView.body.getter : some"
|
||||
moduleName = "yana.debug.dylib"
|
||||
usesParentBreakpointCondition = "Yes"
|
||||
urlString = "file:///Users/edwinqqq/Local/Company%20Projects/yana/yana/Views/FeedView.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "102"
|
||||
endingLineNumber = "102">
|
||||
</Location>
|
||||
</Locations>
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "3E663F1F-E6A0-45A6-87FC-B05E919ADDEB"
|
||||
shouldBeEnabled = "Yes"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "yana/Features/SplashFeature.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "52"
|
||||
endingLineNumber = "52"
|
||||
landmarkName = "body"
|
||||
landmarkType = "24">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "B1F260B9-69B0-4607-AB2D-F9ECEC954EDF"
|
||||
shouldBeEnabled = "Yes"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "yana/Features/IDLoginFeature.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "65"
|
||||
endingLineNumber = "65"
|
||||
landmarkName = "body"
|
||||
landmarkType = "24">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "2308DE52-487A-4A72-9377-A7C0C09DACD4"
|
||||
shouldBeEnabled = "Yes"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "yana/Features/IDLoginFeature.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "134"
|
||||
endingLineNumber = "134"
|
||||
landmarkName = "body"
|
||||
landmarkType = "24">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "44D54396-6B42-4B2E-8621-CB59559FCDB1"
|
||||
shouldBeEnabled = "Yes"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "yana/Features/LoginFeature.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "168"
|
||||
endingLineNumber = "168"
|
||||
landmarkName = "body"
|
||||
landmarkType = "24">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "8732BF66-8904-4DD4-9844-B30786433A70"
|
||||
shouldBeEnabled = "Yes"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "yana/Features/IDLoginFeature.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "69"
|
||||
endingLineNumber = "69"
|
||||
landmarkName = "body"
|
||||
landmarkType = "24">
|
||||
<Locations>
|
||||
<Location
|
||||
uuid = "8732BF66-8904-4DD4-9844-B30786433A70 - 33fd8ff0f3f68ab7"
|
||||
shouldBeEnabled = "Yes"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
symbolName = "(1) suspend resume partial function for closure #1 @Sendable (ComposableArchitecture.Send<yana.IDLoginFeature.Action>) async -> () in closure #1 (inout yana.IDLoginFeature.State, yana.IDLoginFeature.Action) -> ComposableArchitecture.Effect<yana.IDLoginFeature.Action> in yana.IDLoginFeature.body.getter : some"
|
||||
moduleName = "yana.debug.dylib"
|
||||
usesParentBreakpointCondition = "Yes"
|
||||
urlString = "file:///Users/edwinqqq/Local/Company%20Projects/yana/yana/Features/IDLoginFeature.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "69"
|
||||
endingLineNumber = "69">
|
||||
</Location>
|
||||
<Location
|
||||
uuid = "8732BF66-8904-4DD4-9844-B30786433A70 - 8f264cf5d91c4ec9"
|
||||
shouldBeEnabled = "Yes"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
symbolName = "(3) suspend resume partial function for closure #1 @Sendable (ComposableArchitecture.Send<yana.IDLoginFeature.Action>) async -> () in closure #1 (inout yana.IDLoginFeature.State, yana.IDLoginFeature.Action) -> ComposableArchitecture.Effect<yana.IDLoginFeature.Action> in yana.IDLoginFeature.body.getter : some"
|
||||
moduleName = "yana.debug.dylib"
|
||||
usesParentBreakpointCondition = "Yes"
|
||||
urlString = "file:///Users/edwinqqq/Local/Company%20Projects/yana/yana/Features/IDLoginFeature.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "69"
|
||||
endingLineNumber = "69">
|
||||
</Location>
|
||||
<Location
|
||||
uuid = "8732BF66-8904-4DD4-9844-B30786433A70 - 8f264cf5d91c4ec9"
|
||||
shouldBeEnabled = "Yes"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
symbolName = "(3) suspend resume partial function for closure #1 @Sendable (ComposableArchitecture.Send<yana.IDLoginFeature.Action>) async -> () in closure #1 (inout yana.IDLoginFeature.State, yana.IDLoginFeature.Action) -> ComposableArchitecture.Effect<yana.IDLoginFeature.Action> in yana.IDLoginFeature.body.getter : some"
|
||||
moduleName = "yana.debug.dylib"
|
||||
usesParentBreakpointCondition = "Yes"
|
||||
urlString = "file:///Users/edwinqqq/Local/Company%20Projects/yana/yana/Features/IDLoginFeature.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "69"
|
||||
endingLineNumber = "69">
|
||||
</Location>
|
||||
</Locations>
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
</Breakpoints>
|
||||
</Bucket>
|
@@ -100,20 +100,14 @@ struct APIConfiguration {
|
||||
// 添加用户认证相关 headers(仅在 AccountModel 有效时)
|
||||
if let userId = await UserInfoManager.getCurrentUserId() {
|
||||
headers["pub_uid"] = userId
|
||||
#if DEBUG
|
||||
debugInfoSync("🔐 添加认证 header: pub_uid = \(userId)")
|
||||
#endif
|
||||
}
|
||||
if let userTicket = await UserInfoManager.getCurrentUserTicket() {
|
||||
headers["pub_ticket"] = userTicket
|
||||
#if DEBUG
|
||||
debugInfoSync("🔐 添加认证 header: pub_ticket = \(userTicket.prefix(20))...")
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
#if DEBUG
|
||||
debugInfoSync("🔐 跳过认证 header 添加 - 认证状态: \(authStatus.description)")
|
||||
#endif
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
@@ -188,11 +188,11 @@ struct LoginHelper {
|
||||
return nil
|
||||
}
|
||||
|
||||
await debugInfoSync("🔐 DES加密成功")
|
||||
await debugInfoSync(" 原始ID: \(userID)")
|
||||
await debugInfoSync(" 加密后ID: \(encryptedID)")
|
||||
await debugInfoSync(" 原始密码: \(password)")
|
||||
await debugInfoSync(" 加密后密码: \(encryptedPassword)")
|
||||
debugInfoSync("🔐 DES加密成功")
|
||||
debugInfoSync(" 原始ID: \(userID)")
|
||||
debugInfoSync(" 加密后ID: \(encryptedID)")
|
||||
debugInfoSync(" 原始密码: \(password)")
|
||||
debugInfoSync(" 加密后密码: \(encryptedPassword)")
|
||||
|
||||
return IDLoginAPIRequest(
|
||||
phone: userID,
|
||||
@@ -405,10 +405,10 @@ extension LoginHelper {
|
||||
return nil
|
||||
}
|
||||
|
||||
await debugInfoSync("🔐 邮箱验证码登录DES加密成功")
|
||||
await debugInfoSync(" 原始邮箱: \(email)")
|
||||
await debugInfoSync(" 加密邮箱: \(encryptedEmail)")
|
||||
await debugInfoSync(" 验证码: \(code)")
|
||||
debugInfoSync("🔐 邮箱验证码登录DES加密成功")
|
||||
debugInfoSync(" 原始邮箱: \(email)")
|
||||
debugInfoSync(" 加密邮箱: \(encryptedEmail)")
|
||||
debugInfoSync(" 验证码: \(code)")
|
||||
|
||||
return EmailLoginRequest(email: encryptedEmail, code: code)
|
||||
}
|
||||
|
@@ -11,11 +11,20 @@ struct EMailLoginFeature {
|
||||
var isCodeLoading: Bool = false
|
||||
var errorMessage: String? = nil
|
||||
var isCodeSent: Bool = false
|
||||
// 新增:登录流程状态
|
||||
var loginStep: LoginStep = .initial
|
||||
enum LoginStep: Equatable {
|
||||
case initial
|
||||
case authenticating
|
||||
case completed
|
||||
case failed
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
init() {
|
||||
self.email = "exzero@126.com"
|
||||
self.verificationCode = ""
|
||||
self.loginStep = .initial
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -109,6 +118,7 @@ struct EMailLoginFeature {
|
||||
|
||||
state.isLoading = true
|
||||
state.errorMessage = nil
|
||||
state.loginStep = .authenticating
|
||||
|
||||
return .run { send in
|
||||
do {
|
||||
@@ -149,14 +159,16 @@ struct EMailLoginFeature {
|
||||
|
||||
case .loginResponse(.success(let accountModel)):
|
||||
state.isLoading = false
|
||||
state.loginStep = .completed
|
||||
// Effect 保存AccountModel并发送通知
|
||||
return .run { _ in
|
||||
await UserInfoManager.saveAccountModel(accountModel)
|
||||
NotificationCenter.default.post(name: .ticketSuccess, object: nil)
|
||||
// 移除:NotificationCenter.default.post(name: .ticketSuccess, object: nil)
|
||||
}
|
||||
|
||||
case .loginResponse(.failure(let error)):
|
||||
state.isLoading = false
|
||||
state.loginStep = .failed
|
||||
if let apiError = error as? APIError {
|
||||
state.errorMessage = apiError.localizedDescription
|
||||
} else {
|
||||
@@ -174,6 +186,7 @@ struct EMailLoginFeature {
|
||||
state.isCodeLoading = false
|
||||
state.errorMessage = nil
|
||||
state.isCodeSent = false
|
||||
state.loginStep = .initial
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
@@ -43,7 +43,6 @@ struct FeedFeature {
|
||||
#if DEBUG
|
||||
return .none
|
||||
#endif
|
||||
|
||||
// 只在首次出现时触发加载
|
||||
guard !state.isInitialized else { return .none }
|
||||
state.isInitialized = true
|
||||
@@ -168,7 +167,7 @@ struct FeedFeature {
|
||||
}
|
||||
}
|
||||
// 子模块作用域 reducer
|
||||
ifLet(\State.createFeedState, action: /Action.createFeed) {
|
||||
self.ifLet(\.createFeedState, action: \.createFeed) {
|
||||
CreateFeedFeature()
|
||||
}
|
||||
}
|
||||
|
@@ -16,6 +16,9 @@ struct HomeFeature {
|
||||
|
||||
// 新增:Feed 状态
|
||||
var feedState = FeedFeature.State()
|
||||
|
||||
// 新增:登出状态
|
||||
var isLoggedOut = false
|
||||
}
|
||||
|
||||
enum Action {
|
||||
@@ -33,6 +36,9 @@ struct HomeFeature {
|
||||
|
||||
// 新增:Feed actions
|
||||
case feed(FeedFeature.Action)
|
||||
|
||||
// 新增:登出完成
|
||||
case logoutCompleted
|
||||
}
|
||||
|
||||
var body: some ReducerOf<Self> {
|
||||
@@ -48,6 +54,9 @@ struct HomeFeature {
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
case .onAppear:
|
||||
#if DEBUG
|
||||
return .none
|
||||
#endif
|
||||
state.isInitialized = true
|
||||
return .concatenate(
|
||||
.send(.loadUserInfo),
|
||||
@@ -80,12 +89,16 @@ struct HomeFeature {
|
||||
return .send(.logout)
|
||||
|
||||
case .logout:
|
||||
// 清除所有认证数据并发送通知
|
||||
return .run { _ in
|
||||
// 清除所有认证数据并设置登出状态
|
||||
return .run { send in
|
||||
await UserInfoManager.clearAllAuthenticationData()
|
||||
NotificationCenter.default.post(name: .homeLogout, object: nil)
|
||||
await send(.logoutCompleted)
|
||||
}
|
||||
|
||||
case .logoutCompleted:
|
||||
state.isLoggedOut = true
|
||||
return .none
|
||||
|
||||
case .settingDismissed:
|
||||
state.isSettingPresented = false
|
||||
return .none
|
||||
@@ -101,7 +114,7 @@ struct HomeFeature {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notification Extension
|
||||
extension Notification.Name {
|
||||
static let homeLogout = Notification.Name("homeLogout")
|
||||
}
|
||||
// 移除:未使用的通知名称定义
|
||||
// extension Notification.Name {
|
||||
// static let homeLogout = Notification.Name("homeLogout")
|
||||
// }
|
||||
|
@@ -11,6 +11,8 @@ struct LoginFeature {
|
||||
var error: String?
|
||||
var idLoginState = IDLoginFeature.State()
|
||||
var emailLoginState = EMailLoginFeature.State() // 新增:邮箱登录状态
|
||||
// 新增:HomeFeature 状态
|
||||
var homeState = HomeFeature.State()
|
||||
|
||||
// 新增:Account Model 和 Ticket 相关状态
|
||||
var accountModel: AccountModel?
|
||||
@@ -18,6 +20,11 @@ struct LoginFeature {
|
||||
var ticketError: String?
|
||||
var loginStep: LoginStep = .initial
|
||||
|
||||
// 新增:任一登录方式完成时为 true
|
||||
var isAnyLoginCompleted: Bool {
|
||||
idLoginState.loginStep == .completed || emailLoginState.loginStep == .completed
|
||||
}
|
||||
|
||||
enum LoginStep: Equatable {
|
||||
case initial // 初始状态
|
||||
case authenticating // 正在进行 OAuth 认证
|
||||
@@ -42,7 +49,8 @@ struct LoginFeature {
|
||||
case loginResponse(TaskResult<IDLoginResponse>)
|
||||
case idLogin(IDLoginFeature.Action)
|
||||
case emailLogin(EMailLoginFeature.Action) // 新增:邮箱登录action
|
||||
|
||||
// 新增:HomeFeature action
|
||||
case home(HomeFeature.Action)
|
||||
// 新增:Ticket 相关 actions
|
||||
case requestTicket(accessToken: String)
|
||||
case ticketResponse(TaskResult<TicketResponse>)
|
||||
@@ -60,6 +68,10 @@ struct LoginFeature {
|
||||
Scope(state: \.emailLoginState, action: \.emailLogin) {
|
||||
EMailLoginFeature()
|
||||
}
|
||||
// 新增:HomeFeature 作用域
|
||||
Scope(state: \.homeState, action: \.home) {
|
||||
HomeFeature()
|
||||
}
|
||||
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
@@ -165,7 +177,6 @@ struct LoginFeature {
|
||||
// Effect 保存完整的 AccountModel
|
||||
return .run { _ in
|
||||
await UserInfoManager.saveAccountModel(newAccountModel)
|
||||
NotificationCenter.default.post(name: .ticketSuccess, object: nil)
|
||||
}
|
||||
} else {
|
||||
debugErrorSync("❌ AccountModel 不存在,无法保存 ticket")
|
||||
@@ -211,7 +222,14 @@ struct LoginFeature {
|
||||
case .emailLogin:
|
||||
// EmailLogin动作由子feature处理
|
||||
return .none
|
||||
case .home(_):
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移除:未使用的通知名称定义
|
||||
// extension Notification.Name {
|
||||
// static let ticketSuccess = Notification.Name("ticketSuccess")
|
||||
// }
|
||||
|
@@ -58,19 +58,18 @@ struct SettingFeature {
|
||||
state.isLoading = true
|
||||
return .run { _ in
|
||||
await UserInfoManager.clearAllAuthenticationData()
|
||||
NotificationCenter.default.post(name: .homeLogout, object: nil)
|
||||
}
|
||||
|
||||
case .dismissTapped:
|
||||
return .run { _ in
|
||||
NotificationCenter.default.post(name: .settingsDismiss, object: nil)
|
||||
}
|
||||
// 移除:NotificationCenter.default.post(name: .settingsDismiss, object: nil)
|
||||
// 直接通过父级 action 关闭设置页面
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notification Extension
|
||||
extension Notification.Name {
|
||||
static let settingsDismiss = Notification.Name("settingsDismiss")
|
||||
}
|
||||
// 移除:未使用的通知名称定义
|
||||
// extension Notification.Name {
|
||||
// static let settingsDismiss = Notification.Name("settingsDismiss")
|
||||
// }
|
@@ -24,7 +24,7 @@ struct SplashFeature {
|
||||
case .onAppear:
|
||||
state.isLoading = true
|
||||
state.shouldShowMainApp = false
|
||||
state.authenticationStatus = .notFound
|
||||
state.authenticationStatus = UserInfoManager.AuthenticationStatus.notFound
|
||||
state.isCheckingAuthentication = false
|
||||
|
||||
// 1秒延迟后显示主应用 (iOS 15.5+ 兼容)
|
||||
@@ -51,7 +51,6 @@ struct SplashFeature {
|
||||
case let .authenticationChecked(status):
|
||||
#if DEBUG
|
||||
debugInfoSync("🔑 需要手动登录")
|
||||
NotificationCenter.default.post(name: .autoLoginFailed, object: nil)
|
||||
return .none
|
||||
#endif
|
||||
state.isCheckingAuthentication = false
|
||||
@@ -60,10 +59,8 @@ struct SplashFeature {
|
||||
// 根据认证状态发送相应的导航通知
|
||||
if status.canAutoLogin {
|
||||
debugInfoSync("🎉 自动登录成功,进入主页")
|
||||
NotificationCenter.default.post(name: .autoLoginSuccess, object: nil)
|
||||
} else {
|
||||
debugInfoSync("🔑 需要手动登录")
|
||||
NotificationCenter.default.post(name: .autoLoginFailed, object: nil)
|
||||
}
|
||||
|
||||
return .none
|
||||
|
@@ -2,81 +2,33 @@ import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct AppRootView: View {
|
||||
@State private var shouldShowMainApp = false
|
||||
@State private var shouldShowHomePage = false
|
||||
@State private var isLoggedIn = false
|
||||
|
||||
let splashStore = Store(
|
||||
initialState: SplashFeature.State()
|
||||
) {
|
||||
SplashFeature()
|
||||
}
|
||||
|
||||
let loginStore = Store(
|
||||
initialState: LoginFeature.State()
|
||||
) {
|
||||
LoginFeature()
|
||||
}
|
||||
|
||||
let homeStore = Store(
|
||||
var body: some View {
|
||||
if isLoggedIn {
|
||||
HomeView(
|
||||
store: Store(
|
||||
initialState: HomeFeature.State()
|
||||
) {
|
||||
HomeFeature()
|
||||
},
|
||||
onLogout: {
|
||||
isLoggedIn = false
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Group {
|
||||
if shouldShowHomePage {
|
||||
// 主页
|
||||
HomeView(store: homeStore)
|
||||
.transition(.opacity.animation(.easeInOut(duration: 0.5)))
|
||||
} else if shouldShowMainApp {
|
||||
// 登录界面
|
||||
LoginView(store: loginStore)
|
||||
.transition(.opacity.animation(.easeInOut(duration: 0.5)))
|
||||
)
|
||||
} else {
|
||||
// 启动画面
|
||||
SplashView(store: splashStore)
|
||||
.transition(.opacity.animation(.easeInOut(duration: 0.5)))
|
||||
LoginView(
|
||||
store: Store(
|
||||
initialState: LoginFeature.State()
|
||||
) {
|
||||
LoginFeature()
|
||||
},
|
||||
onLoginSuccess: {
|
||||
isLoggedIn = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .autoLoginSuccess)) { _ in
|
||||
// 自动登录成功,直接进入主页
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
shouldShowHomePage = true
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .autoLoginFailed)) { _ in
|
||||
// 自动登录失败,进入登录页面
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
shouldShowMainApp = true
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .ticketSuccess)) { _ in
|
||||
// 手动登录成功,切换到主页
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
shouldShowHomePage = true
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .homeLogout)) { _ in
|
||||
// 从主页登出,返回登录页面
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
shouldShowHomePage = false
|
||||
shouldShowMainApp = true
|
||||
}
|
||||
}
|
||||
|
||||
// 全局 API Loading 效果视图 - 显示在最顶层
|
||||
APILoadingEffectView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Notification.Name {
|
||||
static let splashFinished = Notification.Name("splashFinished")
|
||||
static let ticketSuccess = Notification.Name("ticketSuccess")
|
||||
static let autoLoginSuccess = Notification.Name("autoLoginSuccess")
|
||||
static let autoLoginFailed = Notification.Name("autoLoginFailed")
|
||||
}
|
||||
|
||||
#Preview {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
import Combine
|
||||
|
||||
struct EMailLoginView: View {
|
||||
let store: StoreOf<EMailLoginFeature>
|
||||
@@ -9,7 +10,7 @@ struct EMailLoginView: View {
|
||||
@State private var email: String = ""
|
||||
@State private var verificationCode: String = ""
|
||||
@State private var codeCountdown: Int = 0
|
||||
@State private var timer: Timer?
|
||||
@State private var timerCancellable: AnyCancellable?
|
||||
|
||||
// 管理输入框焦点状态
|
||||
@FocusState private var focusedField: Field?
|
||||
@@ -41,7 +42,9 @@ struct EMailLoginView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// WithViewStore(store, observe: { $0 }) { _ in
|
||||
GeometryReader { geometry in
|
||||
WithPerceptionTracking {
|
||||
ZStack {
|
||||
// 背景图片
|
||||
Image("bg")
|
||||
@@ -202,7 +205,10 @@ struct EMailLoginView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// }
|
||||
.onAppear {
|
||||
let _ = WithPerceptionTracking {
|
||||
// 每次进入页面都重置状态
|
||||
store.send(.resetState)
|
||||
|
||||
@@ -216,16 +222,24 @@ struct EMailLoginView: View {
|
||||
store.send(.emailChanged(email))
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
let _ = WithPerceptionTracking {
|
||||
stopCountdown()
|
||||
}
|
||||
}
|
||||
.onChange(of: email) { newEmail in
|
||||
let _ = WithPerceptionTracking {
|
||||
store.send(.emailChanged(newEmail))
|
||||
}
|
||||
}
|
||||
.onChange(of: verificationCode) { newCode in
|
||||
let _ = WithPerceptionTracking {
|
||||
store.send(.verificationCodeChanged(newCode))
|
||||
}
|
||||
}
|
||||
.onChange(of: store.isCodeLoading) { isCodeLoading in
|
||||
let _ = WithPerceptionTracking {
|
||||
// 当API请求完成且成功时,自动将焦点切换到验证码输入框
|
||||
if !isCodeLoading && store.errorMessage == nil {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
@@ -234,6 +248,7 @@ struct EMailLoginView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 倒计时管理
|
||||
private func startCountdown() {
|
||||
@@ -242,8 +257,10 @@ struct EMailLoginView: View {
|
||||
// 立即设置倒计时
|
||||
codeCountdown = 60
|
||||
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
|
||||
DispatchQueue.main.async {
|
||||
// 使用 SwiftUI 原生的 Timer.publish 方式
|
||||
timerCancellable = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.sink { _ in
|
||||
if codeCountdown > 0 {
|
||||
codeCountdown -= 1
|
||||
} else {
|
||||
@@ -251,21 +268,20 @@ struct EMailLoginView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopCountdown() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
timerCancellable?.cancel()
|
||||
timerCancellable = nil
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
EMailLoginView(
|
||||
store: Store(
|
||||
initialState: EMailLoginFeature.State()
|
||||
) {
|
||||
EMailLoginFeature()
|
||||
},
|
||||
onBack: {}
|
||||
)
|
||||
}
|
||||
//#Preview {
|
||||
// EMailLoginView(
|
||||
// store: Store(
|
||||
// initialState: EMailLoginFeature.State()
|
||||
// ) {
|
||||
// EMailLoginFeature()
|
||||
// },
|
||||
// onBack: {}
|
||||
// )
|
||||
//}
|
||||
|
@@ -96,7 +96,7 @@ struct FeedView: View {
|
||||
store.send(.loadLatestMoments)
|
||||
}
|
||||
.onAppear {
|
||||
store.send(.onAppear)
|
||||
// store.send(.onAppear)
|
||||
}
|
||||
.sheet(isPresented: .init(
|
||||
get: { store.isShowingCreateFeed },
|
||||
|
@@ -3,6 +3,7 @@ import ComposableArchitecture
|
||||
|
||||
struct HomeView: View {
|
||||
let store: StoreOf<HomeFeature>
|
||||
let onLogout: () -> Void // 新增:登出回调
|
||||
@ObservedObject private var localizationManager = LocalizationManager.shared
|
||||
@State private var selectedTab: Tab = .feed
|
||||
|
||||
@@ -27,7 +28,7 @@ struct HomeView: View {
|
||||
)
|
||||
.transition(.opacity)
|
||||
case .me:
|
||||
MeView()
|
||||
MeView(onLogout: onLogout)
|
||||
.transition(.opacity)
|
||||
}
|
||||
}
|
||||
@@ -60,6 +61,6 @@ struct HomeView: View {
|
||||
initialState: HomeFeature.State()
|
||||
) {
|
||||
HomeFeature()
|
||||
}
|
||||
}, onLogout: {}
|
||||
)
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
import Perception
|
||||
|
||||
struct IDLoginView: View {
|
||||
let store: StoreOf<IDLoginFeature>
|
||||
@@ -9,6 +10,8 @@ struct IDLoginView: View {
|
||||
@State private var userID: String = ""
|
||||
@State private var password: String = ""
|
||||
@State private var isPasswordVisible: Bool = false
|
||||
|
||||
// 导航状态管理 - 与 LoginView 保持一致
|
||||
@State private var showRecoverPassword: Bool = false
|
||||
|
||||
// 计算登录按钮是否可用
|
||||
@@ -18,6 +21,7 @@ struct IDLoginView: View {
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
WithPerceptionTracking {
|
||||
ZStack {
|
||||
// 背景图片 - 使用与登录页面相同的"bg"
|
||||
Image("bg")
|
||||
@@ -178,10 +182,14 @@ struct IDLoginView: View {
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
// 隐藏的NavigationLink - 导航到密码恢复页面
|
||||
NavigationLink(
|
||||
destination: RecoverPasswordView(
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
// 使用与 LoginView 一致的 navigationDestination 方式
|
||||
.navigationDestination(isPresented: $showRecoverPassword) {
|
||||
WithPerceptionTracking {
|
||||
RecoverPasswordView(
|
||||
store: Store(
|
||||
initialState: RecoverPasswordFeature.State()
|
||||
) {
|
||||
@@ -191,15 +199,11 @@ struct IDLoginView: View {
|
||||
showRecoverPassword = false
|
||||
}
|
||||
)
|
||||
.navigationBarHidden(true),
|
||||
isActive: $showRecoverPassword
|
||||
) {
|
||||
EmptyView()
|
||||
}
|
||||
.hidden()
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
let _ = WithPerceptionTracking {
|
||||
// 初始化时同步TCA状态到本地状态
|
||||
userID = store.userID
|
||||
password = store.password
|
||||
@@ -211,15 +215,16 @@ struct IDLoginView: View {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
IDLoginView(
|
||||
store: Store(
|
||||
initialState: IDLoginFeature.State()
|
||||
) {
|
||||
IDLoginFeature()
|
||||
},
|
||||
onBack: {}
|
||||
)
|
||||
}
|
||||
//#Preview {
|
||||
// IDLoginView(
|
||||
// store: Store(
|
||||
// initialState: IDLoginFeature.State()
|
||||
// ) {
|
||||
// IDLoginFeature()
|
||||
// },
|
||||
// onBack: {}
|
||||
// )
|
||||
//}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
import Perception
|
||||
|
||||
// PreferenceKey 用于传递图片高度
|
||||
struct ImageHeightPreferenceKey: PreferenceKey {
|
||||
@@ -11,8 +12,9 @@ struct ImageHeightPreferenceKey: PreferenceKey {
|
||||
|
||||
struct LoginView: View {
|
||||
let store: StoreOf<LoginFeature>
|
||||
let onLoginSuccess: () -> Void // 新增:登录成功回调
|
||||
@State private var topImageHeight: CGFloat = 120 // 默认值
|
||||
@ObservedObject private var localizationManager = LocalizationManager.shared
|
||||
// @ObservedObject private var localizationManager = LocalizationManager.shared
|
||||
@State private var showLanguageSettings = false
|
||||
@State private var isAgreedToTerms = true
|
||||
@State private var showUserAgreement = false
|
||||
@@ -21,8 +23,10 @@ struct LoginView: View {
|
||||
@State private var showEmailLogin = false // 新增:邮箱登录导航状态
|
||||
|
||||
var body: some View {
|
||||
WithViewStore(self.store, observe: { $0.isAnyLoginCompleted }) { viewStore in
|
||||
NavigationStack {
|
||||
GeometryReader { geometry in
|
||||
WithPerceptionTracking {
|
||||
ZStack {
|
||||
// 使用与 splash 相同的背景图片
|
||||
Image("bg")
|
||||
@@ -114,48 +118,47 @@ struct LoginView: View {
|
||||
.padding(.bottom, 140)
|
||||
}
|
||||
|
||||
// 隐藏的NavigationLink - 使用纯SwiftUI方式
|
||||
NavigationLink(
|
||||
destination: IDLoginView(
|
||||
// 移除旧的 NavigationLink,改用 navigationDestination
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
// 新增:适配 iOS 16 的 navigationDestination
|
||||
.navigationDestination(isPresented: $showIDLogin) {
|
||||
WithPerceptionTracking {
|
||||
IDLoginView(
|
||||
store: store.scope(
|
||||
state: \.idLoginState,
|
||||
action: \.idLogin
|
||||
),
|
||||
onBack: {
|
||||
showIDLogin = false // 直接设置SwiftUI状态
|
||||
showIDLogin = false
|
||||
}
|
||||
)
|
||||
.navigationBarHidden(true),
|
||||
isActive: $showIDLogin // 使用SwiftUI的绑定
|
||||
) {
|
||||
EmptyView()
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.hidden()
|
||||
|
||||
// 新增:邮箱登录的NavigationLink
|
||||
NavigationLink(
|
||||
destination: EMailLoginView(
|
||||
}
|
||||
.navigationDestination(isPresented: $showEmailLogin) {
|
||||
WithPerceptionTracking {
|
||||
EMailLoginView(
|
||||
store: store.scope(
|
||||
state: \.emailLoginState,
|
||||
action: \.emailLogin
|
||||
),
|
||||
onBack: {
|
||||
showEmailLogin = false // 直接设置SwiftUI状态
|
||||
showEmailLogin = false
|
||||
}
|
||||
)
|
||||
.navigationBarHidden(true),
|
||||
isActive: $showEmailLogin // 使用SwiftUI的绑定
|
||||
) {
|
||||
EmptyView()
|
||||
}
|
||||
.hidden()
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
}
|
||||
// 移除:HomeView 的 navigationDestination
|
||||
}
|
||||
.sheet(isPresented: $showLanguageSettings) {
|
||||
WithPerceptionTracking {
|
||||
LanguageSettingsView(isPresented: $showLanguageSettings)
|
||||
}
|
||||
}
|
||||
.webView(
|
||||
isPresented: $showUserAgreement,
|
||||
url: APIConfiguration.webURL(for: .userAgreement)
|
||||
@@ -164,15 +167,25 @@ struct LoginView: View {
|
||||
isPresented: $showPrivacyPolicy,
|
||||
url: APIConfiguration.webURL(for: .privacyPolicy)
|
||||
)
|
||||
// 新增:监听登录成功,调用回调
|
||||
.onChange(of: viewStore.state) { completed in
|
||||
WithPerceptionTracking {
|
||||
if completed {
|
||||
onLoginSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LoginView(
|
||||
store: Store(
|
||||
initialState: LoginFeature.State()
|
||||
) {
|
||||
LoginFeature()
|
||||
}
|
||||
)
|
||||
}
|
||||
//#Preview {
|
||||
// LoginView(
|
||||
// store: Store(
|
||||
// initialState: LoginFeature.State()
|
||||
// ) {
|
||||
// LoginFeature()
|
||||
// },
|
||||
// onLoginSuccess: {}
|
||||
// )
|
||||
//}
|
||||
|
@@ -2,6 +2,8 @@ import SwiftUI
|
||||
|
||||
struct MeView: View {
|
||||
@State private var showLogoutConfirmation = false
|
||||
let onLogout: () -> Void // 新增:登出回调
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ScrollView {
|
||||
@@ -90,8 +92,8 @@ struct MeView: View {
|
||||
debugInfoSync("🔓 开始执行退出登录...")
|
||||
// 清除所有认证数据(包括 keychain 中的内容)
|
||||
await UserInfoManager.clearAllAuthenticationData()
|
||||
// 发送通知重置 window root 为 login view
|
||||
NotificationCenter.default.post(name: .homeLogout, object: nil)
|
||||
// 调用登出回调,通知父级切换视图
|
||||
onLogout()
|
||||
debugInfoSync("✅ 退出登录完成")
|
||||
}
|
||||
}
|
||||
@@ -131,5 +133,5 @@ struct MenuItemView: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MeView()
|
||||
MeView(onLogout: {})
|
||||
}
|
||||
|
@@ -14,16 +14,41 @@ struct RecoverPasswordView: View {
|
||||
|
||||
// 验证码倒计时状态
|
||||
@State private var countdown: Int = 0
|
||||
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
@State private var timerCancellable: AnyCancellable?
|
||||
|
||||
// 简化的计算属性
|
||||
private var isEmailValid: Bool {
|
||||
!email.isEmpty
|
||||
}
|
||||
|
||||
private var isVerificationCodeValid: Bool {
|
||||
!verificationCode.isEmpty
|
||||
}
|
||||
|
||||
private var isNewPasswordValid: Bool {
|
||||
!newPassword.isEmpty
|
||||
}
|
||||
|
||||
private var isStoreNotLoading: Bool {
|
||||
!store.isResetLoading
|
||||
}
|
||||
|
||||
private var isCodeNotLoading: Bool {
|
||||
!store.isCodeLoading
|
||||
}
|
||||
|
||||
private var isCountdownFinished: Bool {
|
||||
countdown == 0
|
||||
}
|
||||
|
||||
// 计算确认按钮是否可用
|
||||
private var isConfirmButtonEnabled: Bool {
|
||||
return !store.isResetLoading && !email.isEmpty && !verificationCode.isEmpty && !newPassword.isEmpty
|
||||
isStoreNotLoading && isEmailValid && isVerificationCodeValid && isNewPasswordValid
|
||||
}
|
||||
|
||||
// 计算获取验证码按钮是否可用
|
||||
private var isGetCodeButtonEnabled: Bool {
|
||||
return !store.isCodeLoading && !email.isEmpty && countdown == 0
|
||||
isCodeNotLoading && isEmailValid && isCountdownFinished
|
||||
}
|
||||
|
||||
// 计算获取验证码按钮文本
|
||||
@@ -75,6 +100,60 @@ struct RecoverPasswordView: View {
|
||||
// 输入框区域
|
||||
VStack(spacing: 24) {
|
||||
// 邮箱输入框
|
||||
emailInputField
|
||||
|
||||
// 验证码输入框(带获取按钮)
|
||||
verificationCodeInputField
|
||||
|
||||
// 新密码输入框
|
||||
newPasswordInputField
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
|
||||
Spacer()
|
||||
.frame(height: 80)
|
||||
|
||||
// 确认按钮
|
||||
confirmButton
|
||||
|
||||
// 错误信息
|
||||
if let errorMessage = store.errorMessage {
|
||||
Text(errorMessage)
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.red)
|
||||
.padding(.top, 16)
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
resetState()
|
||||
}
|
||||
.onDisappear {
|
||||
stopCountdown()
|
||||
}
|
||||
.onChange(of: email) { newEmail in
|
||||
store.send(.emailChanged(newEmail))
|
||||
}
|
||||
.onChange(of: verificationCode) { newCode in
|
||||
store.send(.verificationCodeChanged(newCode))
|
||||
}
|
||||
.onChange(of: newPassword) { newPassword in
|
||||
store.send(.newPasswordChanged(newPassword))
|
||||
}
|
||||
.onChange(of: store.isResetSuccess) { isResetSuccess in
|
||||
if isResetSuccess {
|
||||
onBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UI Components
|
||||
|
||||
private var emailInputField: some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 25)
|
||||
.fill(Color.white.opacity(0.1))
|
||||
@@ -95,8 +174,9 @@ struct RecoverPasswordView: View {
|
||||
.keyboardType(.emailAddress)
|
||||
.autocapitalization(.none)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证码输入框(带获取按钮)
|
||||
private var verificationCodeInputField: some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 25)
|
||||
.fill(Color.white.opacity(0.1))
|
||||
@@ -118,9 +198,7 @@ struct RecoverPasswordView: View {
|
||||
|
||||
// 获取验证码按钮
|
||||
Button(action: {
|
||||
// 立即开始倒计时
|
||||
startCountdown()
|
||||
// 发送API请求
|
||||
store.send(.getVerificationCodeTapped)
|
||||
}) {
|
||||
ZStack {
|
||||
@@ -144,8 +222,9 @@ struct RecoverPasswordView: View {
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
}
|
||||
|
||||
// 新密码输入框
|
||||
private var newPasswordInputField: some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 25)
|
||||
.fill(Color.white.opacity(0.1))
|
||||
@@ -185,12 +264,8 @@ struct RecoverPasswordView: View {
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
|
||||
Spacer()
|
||||
.frame(height: 80)
|
||||
|
||||
// 确认按钮
|
||||
private var confirmButton: some View {
|
||||
Button(action: {
|
||||
store.send(.resetPasswordTapped)
|
||||
}) {
|
||||
@@ -222,22 +297,11 @@ struct RecoverPasswordView: View {
|
||||
.disabled(!isConfirmButtonEnabled)
|
||||
.opacity(isConfirmButtonEnabled ? 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 {
|
||||
// 每次进入页面都重置状态
|
||||
// MARK: - Private Methods
|
||||
|
||||
private func resetState() {
|
||||
store.send(.resetState)
|
||||
|
||||
email = ""
|
||||
@@ -245,60 +309,42 @@ struct RecoverPasswordView: View {
|
||||
newPassword = ""
|
||||
isNewPasswordVisible = false
|
||||
countdown = 0
|
||||
|
||||
#if DEBUG
|
||||
email = "exzero@126.com"
|
||||
store.send(.emailChanged(email))
|
||||
#endif
|
||||
}
|
||||
.onDisappear {
|
||||
countdown = 0
|
||||
}
|
||||
.onChange(of: email) { newEmail in
|
||||
store.send(.emailChanged(newEmail))
|
||||
}
|
||||
.onChange(of: verificationCode) { newCode in
|
||||
store.send(.verificationCodeChanged(newCode))
|
||||
}
|
||||
.onChange(of: newPassword) { newPassword in
|
||||
store.send(.newPasswordChanged(newPassword))
|
||||
}
|
||||
.onChange(of: store.isCodeLoading) { isCodeLoading in
|
||||
// 当API请求完成且成功时,自动将焦点切换到验证码输入框
|
||||
if !isCodeLoading && store.errorMessage == nil {
|
||||
// 可以在这里添加焦点切换逻辑
|
||||
}
|
||||
}
|
||||
.onChange(of: store.isResetSuccess) { isResetSuccess in
|
||||
// 密码重置成功后自动返回上一页
|
||||
if isResetSuccess {
|
||||
onBack()
|
||||
}
|
||||
}
|
||||
.onReceive(timer) { _ in
|
||||
if countdown > 0 {
|
||||
countdown -= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
private func startCountdown() {
|
||||
stopCountdown()
|
||||
countdown = 60
|
||||
|
||||
timerCancellable = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.sink { _ in
|
||||
if countdown > 0 {
|
||||
countdown -= 1
|
||||
} else {
|
||||
stopCountdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopCountdown() {
|
||||
timerCancellable?.cancel()
|
||||
timerCancellable = nil
|
||||
countdown = 0
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
RecoverPasswordView(
|
||||
store: Store(
|
||||
initialState: RecoverPasswordFeature.State()
|
||||
) {
|
||||
RecoverPasswordFeature()
|
||||
},
|
||||
onBack: {}
|
||||
)
|
||||
}
|
||||
//#Preview {
|
||||
// RecoverPasswordView(
|
||||
// store: Store(
|
||||
// initialState: RecoverPasswordFeature.State()
|
||||
// ) {
|
||||
// RecoverPasswordFeature()
|
||||
// },
|
||||
// onBack: {}
|
||||
// )
|
||||
//}
|
||||
|
Reference in New Issue
Block a user