diff --git a/.gitignore b/.gitignore
index 4134c21..e16d741 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/.kiro/specs/architecture-analysis/requirements.md b/.kiro/specs/architecture-analysis/requirements.md
deleted file mode 100644
index 8cd53d3..0000000
--- a/.kiro/specs/architecture-analysis/requirements.md
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/yana.xcodeproj/project.pbxproj b/yana.xcodeproj/project.pbxproj
index d0d80fa..1de99c3 100644
--- a/yana.xcodeproj/project.pbxproj
+++ b/yana.xcodeproj/project.pbxproj
@@ -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;
diff --git a/yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
deleted file mode 100644
index 90819c1..0000000
--- a/yana.xcworkspace/xcuserdata/edwinqqq.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
+++ /dev/null
@@ -1,178 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/yana/APIs/APIEndpoints.swift b/yana/APIs/APIEndpoints.swift
index 0dd47da..e6483a3 100644
--- a/yana/APIs/APIEndpoints.swift
+++ b/yana/APIs/APIEndpoints.swift
@@ -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
}
diff --git a/yana/APIs/LoginModels.swift b/yana/APIs/LoginModels.swift
index b170f0d..52a96e2 100644
--- a/yana/APIs/LoginModels.swift
+++ b/yana/APIs/LoginModels.swift
@@ -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)
}
diff --git a/yana/Features/EMailLoginFeature.swift b/yana/Features/EMailLoginFeature.swift
index 24fabc4..0d5e808 100644
--- a/yana/Features/EMailLoginFeature.swift
+++ b/yana/Features/EMailLoginFeature.swift
@@ -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
}
}
diff --git a/yana/Features/FeedFeature.swift b/yana/Features/FeedFeature.swift
index 3e87a9f..7f0a0bf 100644
--- a/yana/Features/FeedFeature.swift
+++ b/yana/Features/FeedFeature.swift
@@ -42,8 +42,7 @@ struct FeedFeature {
case .onAppear:
#if DEBUG
return .none
- #endif
-
+ #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()
}
}
diff --git a/yana/Features/HomeFeature.swift b/yana/Features/HomeFeature.swift
index a0c2bd4..8677843 100644
--- a/yana/Features/HomeFeature.swift
+++ b/yana/Features/HomeFeature.swift
@@ -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 {
@@ -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")
+// }
diff --git a/yana/Features/LoginFeature.swift b/yana/Features/LoginFeature.swift
index 177ffaa..d491956 100644
--- a/yana/Features/LoginFeature.swift
+++ b/yana/Features/LoginFeature.swift
@@ -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)
case idLogin(IDLoginFeature.Action)
case emailLogin(EMailLoginFeature.Action) // 新增:邮箱登录action
-
+ // 新增:HomeFeature action
+ case home(HomeFeature.Action)
// 新增:Ticket 相关 actions
case requestTicket(accessToken: String)
case ticketResponse(TaskResult)
@@ -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")
+// }
diff --git a/yana/Features/SettingFeature.swift b/yana/Features/SettingFeature.swift
index 546c0e4..68525e5 100644
--- a/yana/Features/SettingFeature.swift
+++ b/yana/Features/SettingFeature.swift
@@ -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")
-}
\ No newline at end of file
+// 移除:未使用的通知名称定义
+// extension Notification.Name {
+// static let settingsDismiss = Notification.Name("settingsDismiss")
+// }
\ No newline at end of file
diff --git a/yana/Features/SplashFeature.swift b/yana/Features/SplashFeature.swift
index feac106..a1b1e03 100644
--- a/yana/Features/SplashFeature.swift
+++ b/yana/Features/SplashFeature.swift
@@ -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
diff --git a/yana/Views/AppRootView.swift b/yana/Views/AppRootView.swift
index a6e2309..912b0ce 100644
--- a/yana/Views/AppRootView.swift
+++ b/yana/Views/AppRootView.swift
@@ -2,83 +2,35 @@ import SwiftUI
import ComposableArchitecture
struct AppRootView: View {
- @State private var shouldShowMainApp = false
- @State private var shouldShowHomePage = false
-
- let splashStore = Store(
- initialState: SplashFeature.State()
- ) {
- SplashFeature()
- }
-
- let loginStore = Store(
- initialState: LoginFeature.State()
- ) {
- LoginFeature()
- }
-
- let homeStore = Store(
- initialState: HomeFeature.State()
- ) {
- HomeFeature()
- }
+ @State private var 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)))
+ if isLoggedIn {
+ HomeView(
+ store: Store(
+ initialState: HomeFeature.State()
+ ) {
+ HomeFeature()
+ },
+ onLogout: {
+ isLoggedIn = false
}
- }
- .onReceive(NotificationCenter.default.publisher(for: .autoLoginSuccess)) { _ in
- // 自动登录成功,直接进入主页
- withAnimation(.easeInOut(duration: 0.5)) {
- shouldShowHomePage = true
+ )
+ } else {
+ LoginView(
+ store: Store(
+ initialState: LoginFeature.State()
+ ) {
+ LoginFeature()
+ },
+ onLoginSuccess: {
+ isLoggedIn = 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 {
AppRootView()
}
\ No newline at end of file
diff --git a/yana/Views/EMailLoginView.swift b/yana/Views/EMailLoginView.swift
index adf3b48..00a922b 100644
--- a/yana/Views/EMailLoginView.swift
+++ b/yana/Views/EMailLoginView.swift
@@ -1,5 +1,6 @@
import SwiftUI
import ComposableArchitecture
+import Combine
struct EMailLoginView: View {
let store: StoreOf
@@ -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,8 +42,10 @@ struct EMailLoginView: View {
}
var body: some View {
- GeometryReader { geometry in
- ZStack {
+// WithViewStore(store, observe: { $0 }) { _ in
+ GeometryReader { geometry in
+ WithPerceptionTracking {
+ ZStack {
// 背景图片
Image("bg")
.resizable()
@@ -200,36 +203,48 @@ struct EMailLoginView: View {
Spacer()
}
+ }
}
}
+// }
.onAppear {
- // 每次进入页面都重置状态
- store.send(.resetState)
-
- email = ""
- verificationCode = ""
- codeCountdown = 0
- stopCountdown()
-
- #if DEBUG
- email = "exzero@126.com"
- store.send(.emailChanged(email))
- #endif
+ let _ = WithPerceptionTracking {
+ // 每次进入页面都重置状态
+ store.send(.resetState)
+
+ email = ""
+ verificationCode = ""
+ codeCountdown = 0
+ stopCountdown()
+
+ #if DEBUG
+ email = "exzero@126.com"
+ store.send(.emailChanged(email))
+ #endif
+ }
}
.onDisappear {
- stopCountdown()
+ let _ = WithPerceptionTracking {
+ stopCountdown()
+ }
}
.onChange(of: email) { newEmail in
- store.send(.emailChanged(newEmail))
+ let _ = WithPerceptionTracking {
+ store.send(.emailChanged(newEmail))
+ }
}
.onChange(of: verificationCode) { newCode in
- store.send(.verificationCodeChanged(newCode))
+ let _ = WithPerceptionTracking {
+ store.send(.verificationCodeChanged(newCode))
+ }
}
.onChange(of: store.isCodeLoading) { isCodeLoading in
- // 当API请求完成且成功时,自动将焦点切换到验证码输入框
- if !isCodeLoading && store.errorMessage == nil {
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
- focusedField = .verificationCode
+ let _ = WithPerceptionTracking {
+ // 当API请求完成且成功时,自动将焦点切换到验证码输入框
+ if !isCodeLoading && store.errorMessage == nil {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ focusedField = .verificationCode
+ }
}
}
}
@@ -242,30 +257,31 @@ 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 {
stopCountdown()
}
}
- }
}
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: {}
+// )
+//}
diff --git a/yana/Views/FeedView.swift b/yana/Views/FeedView.swift
index a873f84..2ffb0fa 100644
--- a/yana/Views/FeedView.swift
+++ b/yana/Views/FeedView.swift
@@ -96,7 +96,7 @@ struct FeedView: View {
store.send(.loadLatestMoments)
}
.onAppear {
- store.send(.onAppear)
+// store.send(.onAppear)
}
.sheet(isPresented: .init(
get: { store.isShowingCreateFeed },
diff --git a/yana/Views/HomeView.swift b/yana/Views/HomeView.swift
index 355a947..bcde1ee 100644
--- a/yana/Views/HomeView.swift
+++ b/yana/Views/HomeView.swift
@@ -3,6 +3,7 @@ import ComposableArchitecture
struct HomeView: View {
let store: StoreOf
+ 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: {}
)
}
diff --git a/yana/Views/IDLoginView.swift b/yana/Views/IDLoginView.swift
index 0e0a8e7..f6831e7 100644
--- a/yana/Views/IDLoginView.swift
+++ b/yana/Views/IDLoginView.swift
@@ -1,5 +1,6 @@
import SwiftUI
import ComposableArchitecture
+import Perception
struct IDLoginView: View {
let store: StoreOf
@@ -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,48 +182,49 @@ struct IDLoginView: View {
Spacer()
}
-
- // 隐藏的NavigationLink - 导航到密码恢复页面
- NavigationLink(
- destination: RecoverPasswordView(
- store: Store(
- initialState: RecoverPasswordFeature.State()
- ) {
- RecoverPasswordFeature()
- },
- onBack: {
- showRecoverPassword = false
- }
- )
- .navigationBarHidden(true),
- isActive: $showRecoverPassword
- ) {
- EmptyView()
- }
- .hidden()
+ }
+ }
+ }
+ .navigationBarHidden(true)
+ // 使用与 LoginView 一致的 navigationDestination 方式
+ .navigationDestination(isPresented: $showRecoverPassword) {
+ WithPerceptionTracking {
+ RecoverPasswordView(
+ store: Store(
+ initialState: RecoverPasswordFeature.State()
+ ) {
+ RecoverPasswordFeature()
+ },
+ onBack: {
+ showRecoverPassword = false
+ }
+ )
+ .navigationBarHidden(true)
}
}
.onAppear {
- // 初始化时同步TCA状态到本地状态
- userID = store.userID
- password = store.password
- isPasswordVisible = store.isPasswordVisible
-
- #if DEBUG
- // 移除测试用的硬编码凭据
- debugInfoSync("🐛 Debug模式: 已移除硬编码测试凭据")
- #endif
+ let _ = WithPerceptionTracking {
+ // 初始化时同步TCA状态到本地状态
+ userID = store.userID
+ password = store.password
+ isPasswordVisible = store.isPasswordVisible
+
+ #if DEBUG
+ // 移除测试用的硬编码凭据
+ debugInfoSync("🐛 Debug模式: 已移除硬编码测试凭据")
+ #endif
+ }
}
}
}
-#Preview {
- IDLoginView(
- store: Store(
- initialState: IDLoginFeature.State()
- ) {
- IDLoginFeature()
- },
- onBack: {}
- )
-}
+//#Preview {
+// IDLoginView(
+// store: Store(
+// initialState: IDLoginFeature.State()
+// ) {
+// IDLoginFeature()
+// },
+// onBack: {}
+// )
+//}
diff --git a/yana/Views/LoginView.swift b/yana/Views/LoginView.swift
index 77e57e7..5b19874 100644
--- a/yana/Views/LoginView.swift
+++ b/yana/Views/LoginView.swift
@@ -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
+ 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,158 +23,169 @@ struct LoginView: View {
@State private var showEmailLogin = false // 新增:邮箱登录导航状态
var body: some View {
- NavigationStack {
- 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(NSLocalizedString("login.app_title", comment: ""))
- .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 {
+ WithViewStore(self.store, observe: { $0.isAnyLoginCompleted }) { viewStore in
+ NavigationStack {
+ GeometryReader { geometry in
+ WithPerceptionTracking {
+ 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(NSLocalizedString("login.app_title", comment: ""))
+ .font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width))
+ .foregroundColor(.white)
+ .padding(.leading, 20)
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(.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)
}
- .padding(.trailing, 16)
+ Spacer()
}
- Spacer()
+ #endif
+
+ VStack(spacing: 24) {
+ // ID Login 按钮
+ LoginButton(
+ iconName: "person.circle.fill",
+ iconColor: .green,
+ title: NSLocalizedString("login.id_login", comment: "")
+ ) {
+ showIDLogin = true // 直接设置SwiftUI状态
+ }
+ // Email Login 按钮
+ LoginButton(
+ iconName: "envelope.fill",
+ iconColor: .blue,
+ title: NSLocalizedString("login.email_login", comment: "")
+ ) {
+ showEmailLogin = true // 显示邮箱登录界面
+ }
+ }.padding(.top, max(0, topImageHeight+140))
+ }
+ .onPreferenceChange(ImageHeightPreferenceKey.self) { imageHeight in
+ topImageHeight = imageHeight
}
- #endif
-
- VStack(spacing: 24) {
- // ID Login 按钮
- LoginButton(
- iconName: "person.circle.fill",
- iconColor: .green,
- title: NSLocalizedString("login.id_login", comment: "")
- ) {
- showIDLogin = true // 直接设置SwiftUI状态
- }
- // Email Login 按钮
- LoginButton(
- iconName: "envelope.fill",
- iconColor: .blue,
- title: NSLocalizedString("login.email_login", comment: "")
- ) {
- showEmailLogin = true // 显示邮箱登录界面
- }
- }.padding(.top, max(0, topImageHeight+140))
- }
- .onPreferenceChange(ImageHeightPreferenceKey.self) { imageHeight in
- topImageHeight = imageHeight
- }
- // 间距,使登录按钮区域顶部距离"top"图片底部40pt
- Spacer()
- .frame(height: 120)
+ // 间距,使登录按钮区域顶部距离"top"图片底部40pt
+ Spacer()
+ .frame(height: 120)
+
+ // 用户协议组件
+ UserAgreementView(
+ isAgreed: $isAgreedToTerms,
+ onUserServiceTapped: {
+ showUserAgreement = true
+ },
+ onPrivacyPolicyTapped: {
+ showPrivacyPolicy = true
+ }
+ )
+ .padding(.horizontal, 28)
+ .padding(.bottom, 140)
+ }
- // 用户协议组件
- UserAgreementView(
- isAgreed: $isAgreedToTerms,
- onUserServiceTapped: {
- showUserAgreement = true
- },
- onPrivacyPolicyTapped: {
- showPrivacyPolicy = true
- }
- )
- .padding(.horizontal, 28)
- .padding(.bottom, 140)
+ // 移除旧的 NavigationLink,改用 navigationDestination
}
-
- // 隐藏的NavigationLink - 使用纯SwiftUI方式
- NavigationLink(
- destination: IDLoginView(
+ }
+ }
+ .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()
+ .navigationBarHidden(true)
+ }
+ }
+ // 移除:HomeView 的 navigationDestination
+ }
+ .sheet(isPresented: $showLanguageSettings) {
+ WithPerceptionTracking {
+ LanguageSettingsView(isPresented: $showLanguageSettings)
+ }
+ }
+ .webView(
+ isPresented: $showUserAgreement,
+ url: APIConfiguration.webURL(for: .userAgreement)
+ )
+ .webView(
+ isPresented: $showPrivacyPolicy,
+ url: APIConfiguration.webURL(for: .privacyPolicy)
+ )
+ // 新增:监听登录成功,调用回调
+ .onChange(of: viewStore.state) { completed in
+ WithPerceptionTracking {
+ if completed {
+ onLoginSuccess()
}
- .hidden()
}
}
- .navigationBarHidden(true)
}
- .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()
- }
- )
-}
+//#Preview {
+// LoginView(
+// store: Store(
+// initialState: LoginFeature.State()
+// ) {
+// LoginFeature()
+// },
+// onLoginSuccess: {}
+// )
+//}
diff --git a/yana/Views/MeView.swift b/yana/Views/MeView.swift
index 27d951d..b5bc99c 100644
--- a/yana/Views/MeView.swift
+++ b/yana/Views/MeView.swift
@@ -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: {})
+}
diff --git a/yana/Views/RecoverPasswordView.swift b/yana/Views/RecoverPasswordView.swift
index 3d048fd..8523c9b 100644
--- a/yana/Views/RecoverPasswordView.swift
+++ b/yana/Views/RecoverPasswordView.swift
@@ -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,115 +100,13 @@ struct RecoverPasswordView: View {
// 输入框区域
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(NSLocalizedString("recover_password.placeholder_email", comment: ""))
- .foregroundColor(.white.opacity(0.6))
- }
- .foregroundColor(.white)
- .font(.system(size: 16))
- .padding(.horizontal, 24)
- .keyboardType(.emailAddress)
- .autocapitalization(.none)
- }
+ emailInputField
// 验证码输入框(带获取按钮)
- 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(NSLocalizedString("recover_password.placeholder_verification_code", comment: ""))
- .foregroundColor(.white.opacity(0.6))
- }
- .foregroundColor(.white)
- .font(.system(size: 16))
- .keyboardType(.numberPad)
-
- // 获取验证码按钮
- Button(action: {
- // 立即开始倒计时
- startCountdown()
- // 发送API请求
- 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: 15)
- .fill(Color.white.opacity(isGetCodeButtonEnabled ? 0.2 : 0.1))
- )
- }
- .disabled(!isGetCodeButtonEnabled || store.isCodeLoading)
- }
- .padding(.horizontal, 24)
- }
+ verificationCodeInputField
// 新密码输入框
- 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 isNewPasswordVisible {
- TextField("", text: $newPassword)
- .placeholder(when: newPassword.isEmpty) {
- Text(NSLocalizedString("recover_password.placeholder_new_password", comment: ""))
- .foregroundColor(.white.opacity(0.6))
- }
- .foregroundColor(.white)
- .font(.system(size: 16))
- } else {
- SecureField("", text: $newPassword)
- .placeholder(when: newPassword.isEmpty) {
- Text(NSLocalizedString("recover_password.placeholder_new_password", comment: ""))
- .foregroundColor(.white.opacity(0.6))
- }
- .foregroundColor(.white)
- .font(.system(size: 16))
- }
-
- Button(action: {
- isNewPasswordVisible.toggle()
- }) {
- Image(systemName: isNewPasswordVisible ? "eye.slash" : "eye")
- .foregroundColor(.white.opacity(0.7))
- .font(.system(size: 18))
- }
- }
- .padding(.horizontal, 24)
- }
+ newPasswordInputField
}
.padding(.horizontal, 32)
@@ -191,37 +114,7 @@ struct RecoverPasswordView: View {
.frame(height: 80)
// 确认按钮
- Button(action: {
- store.send(.resetPasswordTapped)
- }) {
- 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.isResetLoading {
- ProgressView()
- .progressViewStyle(CircularProgressViewStyle(tint: .white))
- .scaleEffect(0.8)
- }
- Text(store.isResetLoading ? NSLocalizedString("recover_password.resetting", comment: "") : NSLocalizedString("recover_password.confirm_button", comment: ""))
- .font(.system(size: 18, weight: .semibold))
- .foregroundColor(.white)
- }
- }
- .frame(height: 56)
- }
- .disabled(!isConfirmButtonEnabled)
- .opacity(isConfirmButtonEnabled ? 1.0 : 0.5)
- .padding(.horizontal, 32)
+ confirmButton
// 错误信息
if let errorMessage = store.errorMessage {
@@ -237,21 +130,10 @@ struct RecoverPasswordView: View {
}
}
.onAppear {
- // 每次进入页面都重置状态
- store.send(.resetState)
-
- email = ""
- verificationCode = ""
- newPassword = ""
- isNewPasswordVisible = false
- countdown = 0
- #if DEBUG
- email = "exzero@126.com"
- store.send(.emailChanged(email))
- #endif
+ resetState()
}
.onDisappear {
- countdown = 0
+ stopCountdown()
}
.onChange(of: email) { newEmail in
store.send(.emailChanged(newEmail))
@@ -262,43 +144,207 @@ struct RecoverPasswordView: View {
.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: - UI Components
+
+ private var emailInputField: some View {
+ 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(NSLocalizedString("recover_password.placeholder_email", comment: ""))
+ .foregroundColor(.white.opacity(0.6))
+ }
+ .foregroundColor(.white)
+ .font(.system(size: 16))
+ .padding(.horizontal, 24)
+ .keyboardType(.emailAddress)
+ .autocapitalization(.none)
}
}
+ private var verificationCodeInputField: some View {
+ 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(NSLocalizedString("recover_password.placeholder_verification_code", comment: ""))
+ .foregroundColor(.white.opacity(0.6))
+ }
+ .foregroundColor(.white)
+ .font(.system(size: 16))
+ .keyboardType(.numberPad)
+
+ // 获取验证码按钮
+ Button(action: {
+ startCountdown()
+ 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: 15)
+ .fill(Color.white.opacity(isGetCodeButtonEnabled ? 0.2 : 0.1))
+ )
+ }
+ .disabled(!isGetCodeButtonEnabled || store.isCodeLoading)
+ }
+ .padding(.horizontal, 24)
+ }
+ }
+
+ private var newPasswordInputField: some View {
+ 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 isNewPasswordVisible {
+ TextField("", text: $newPassword)
+ .placeholder(when: newPassword.isEmpty) {
+ Text(NSLocalizedString("recover_password.placeholder_new_password", comment: ""))
+ .foregroundColor(.white.opacity(0.6))
+ }
+ .foregroundColor(.white)
+ .font(.system(size: 16))
+ } else {
+ SecureField("", text: $newPassword)
+ .placeholder(when: newPassword.isEmpty) {
+ Text(NSLocalizedString("recover_password.placeholder_new_password", comment: ""))
+ .foregroundColor(.white.opacity(0.6))
+ }
+ .foregroundColor(.white)
+ .font(.system(size: 16))
+ }
+
+ Button(action: {
+ isNewPasswordVisible.toggle()
+ }) {
+ Image(systemName: isNewPasswordVisible ? "eye.slash" : "eye")
+ .foregroundColor(.white.opacity(0.7))
+ .font(.system(size: 18))
+ }
+ }
+ .padding(.horizontal, 24)
+ }
+ }
+
+ private var confirmButton: some View {
+ Button(action: {
+ store.send(.resetPasswordTapped)
+ }) {
+ 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.isResetLoading {
+ ProgressView()
+ .progressViewStyle(CircularProgressViewStyle(tint: .white))
+ .scaleEffect(0.8)
+ }
+ Text(store.isResetLoading ? NSLocalizedString("recover_password.resetting", comment: "") : NSLocalizedString("recover_password.confirm_button", comment: ""))
+ .font(.system(size: 18, weight: .semibold))
+ .foregroundColor(.white)
+ }
+ }
+ .frame(height: 56)
+ }
+ .disabled(!isConfirmButtonEnabled)
+ .opacity(isConfirmButtonEnabled ? 1.0 : 0.5)
+ .padding(.horizontal, 32)
+ }
+
// MARK: - Private Methods
+ private func resetState() {
+ store.send(.resetState)
+
+ email = ""
+ verificationCode = ""
+ newPassword = ""
+ isNewPasswordVisible = false
+ countdown = 0
+
+ #if DEBUG
+ email = "exzero@126.com"
+ store.send(.emailChanged(email))
+ #endif
+ }
+
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: {}
- )
-}
\ No newline at end of file
+//#Preview {
+// RecoverPasswordView(
+// store: Store(
+// initialState: RecoverPasswordFeature.State()
+// ) {
+// RecoverPasswordFeature()
+// },
+// onBack: {}
+// )
+//}