
- 在APIEndpoints.swift中新增tcToken端点以支持腾讯云COS Token获取。 - 在APIModels.swift中新增TcTokenRequest和TcTokenResponse模型,处理Token请求和响应。 - 在COSManager.swift中实现Token的获取、缓存和过期管理逻辑,提升API请求的安全性。 - 在LanguageSettingsView中添加调试功能,允许测试COS Token获取。 - 在多个视图中更新状态管理和导航逻辑,确保用户体验一致性。 - 在FeedFeature和HomeFeature中优化状态管理,简化视图逻辑。
274 lines
12 KiB
Swift
274 lines
12 KiB
Swift
import SwiftUI
|
|
import ComposableArchitecture
|
|
import Combine
|
|
|
|
struct EMailLoginView: View {
|
|
let store: StoreOf<EMailLoginFeature>
|
|
let onBack: () -> Void
|
|
@Binding var showEmailLogin: Bool
|
|
|
|
@State private var email: String = ""
|
|
@State private var verificationCode: String = ""
|
|
@State private var codeCountdown: Int = 0
|
|
@State private var timerCancellable: AnyCancellable?
|
|
@FocusState private var focusedField: Field?
|
|
|
|
enum Field {
|
|
case email
|
|
case verificationCode
|
|
}
|
|
|
|
private var isLoginButtonEnabled: Bool {
|
|
return !store.isLoading && !email.isEmpty && !verificationCode.isEmpty
|
|
}
|
|
private var getCodeButtonText: String {
|
|
if store.isCodeLoading {
|
|
return ""
|
|
} else if codeCountdown > 0 {
|
|
return "\(codeCountdown)S"
|
|
} else {
|
|
return NSLocalizedString("email_login.get_code", comment: "")
|
|
}
|
|
}
|
|
private var isCodeButtonEnabled: Bool {
|
|
return !store.isCodeLoading && codeCountdown == 0
|
|
}
|
|
|
|
var body: some View {
|
|
WithViewStore(store, observe: { $0.loginStep }) { viewStore in
|
|
LoginContentView(
|
|
store: store,
|
|
onBack: onBack,
|
|
email: $email,
|
|
verificationCode: $verificationCode,
|
|
codeCountdown: $codeCountdown,
|
|
focusedField: $focusedField,
|
|
isLoginButtonEnabled: isLoginButtonEnabled,
|
|
getCodeButtonText: getCodeButtonText,
|
|
isCodeButtonEnabled: isCodeButtonEnabled
|
|
)
|
|
.onChange(of: viewStore.state) { newStep in
|
|
debugInfoSync("🔄 EMailLoginView: loginStep 变化为 \(newStep)")
|
|
if newStep == .completed {
|
|
debugInfoSync("✅ EMailLoginView: 登录成功,准备关闭自身")
|
|
showEmailLogin = false
|
|
}
|
|
}
|
|
}
|
|
.onAppear {
|
|
let _ = WithPerceptionTracking {
|
|
store.send(.resetState)
|
|
email = ""
|
|
verificationCode = ""
|
|
codeCountdown = 0
|
|
stopCountdown()
|
|
#if DEBUG
|
|
email = "exzero@126.com"
|
|
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 {
|
|
if !isCodeLoading && store.errorMessage == nil {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
focusedField = .verificationCode
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func startCountdown() {
|
|
stopCountdown()
|
|
codeCountdown = 60
|
|
timerCancellable = Timer.publish(every: 1.0, on: .main, in: .common)
|
|
.autoconnect()
|
|
.sink { _ in
|
|
if codeCountdown > 0 {
|
|
codeCountdown -= 1
|
|
} else {
|
|
stopCountdown()
|
|
}
|
|
}
|
|
}
|
|
private func stopCountdown() {
|
|
timerCancellable?.cancel()
|
|
timerCancellable = nil
|
|
}
|
|
}
|
|
|
|
private struct LoginContentView: View {
|
|
let store: StoreOf<EMailLoginFeature>
|
|
let onBack: () -> Void
|
|
@Binding var email: String
|
|
@Binding var verificationCode: String
|
|
@Binding var codeCountdown: Int
|
|
@FocusState.Binding var focusedField: EMailLoginView.Field?
|
|
let isLoginButtonEnabled: Bool
|
|
let getCodeButtonText: String
|
|
let isCodeButtonEnabled: Bool
|
|
|
|
var body: some View {
|
|
GeometryReader { geometry in
|
|
WithPerceptionTracking {
|
|
ZStack {
|
|
Image("bg")
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fill)
|
|
.ignoresSafeArea(.all)
|
|
VStack(spacing: 0) {
|
|
HStack {
|
|
Button(action: {
|
|
onBack()
|
|
}) {
|
|
Image(systemName: "chevron.left")
|
|
.font(.system(size: 24, weight: .medium))
|
|
.foregroundColor(.white)
|
|
.frame(width: 44, height: 44)
|
|
}
|
|
Spacer()
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.top, 8)
|
|
Spacer().frame(height: 60)
|
|
Text(NSLocalizedString("email_login.title", comment: ""))
|
|
.font(.system(size: 28, weight: .medium))
|
|
.foregroundColor(.white)
|
|
.padding(.bottom, 80)
|
|
VStack(spacing: 24) {
|
|
ZStack {
|
|
RoundedRectangle(cornerRadius: 25)
|
|
.fill(Color.white.opacity(0.1))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 25)
|
|
.stroke(Color.white.opacity(0.3), lineWidth: 1)
|
|
)
|
|
.frame(height: 56)
|
|
TextField("", text: $email)
|
|
.placeholder(when: email.isEmpty) {
|
|
Text(NSLocalizedString("placeholder.enter_email", comment: ""))
|
|
.foregroundColor(.white.opacity(0.6))
|
|
}
|
|
.foregroundColor(.white)
|
|
.font(.system(size: 16))
|
|
.padding(.horizontal, 24)
|
|
.keyboardType(.emailAddress)
|
|
.focused($focusedField, equals: .email)
|
|
}
|
|
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("placeholder.enter_code", comment: ""))
|
|
.foregroundColor(.white.opacity(0.6))
|
|
}
|
|
.foregroundColor(.white)
|
|
.font(.system(size: 16))
|
|
.keyboardType(.numberPad)
|
|
.focused($focusedField, equals: .verificationCode)
|
|
Button(action: {
|
|
store.send(.getVerificationCodeTapped)
|
|
}) {
|
|
ZStack {
|
|
if store.isCodeLoading {
|
|
ProgressView()
|
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
|
.scaleEffect(0.7)
|
|
} else {
|
|
Text(getCodeButtonText)
|
|
.font(.system(size: 14, weight: .medium))
|
|
.foregroundColor(.white)
|
|
}
|
|
}
|
|
.frame(width: 60, height: 36)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 18)
|
|
.fill(Color.white.opacity(isCodeButtonEnabled && !email.isEmpty ? 0.2 : 0.1))
|
|
)
|
|
}
|
|
.disabled(!isCodeButtonEnabled || email.isEmpty || store.isCodeLoading)
|
|
}
|
|
.padding(.horizontal, 24)
|
|
}
|
|
}
|
|
.padding(.horizontal, 32)
|
|
Spacer().frame(height: 60)
|
|
Button(action: {
|
|
store.send(.loginButtonTapped(email: email, verificationCode: verificationCode))
|
|
}) {
|
|
ZStack {
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.85, green: 0.37, blue: 1.0),
|
|
Color(red: 0.54, green: 0.31, blue: 1.0)
|
|
],
|
|
startPoint: .leading,
|
|
endPoint: .trailing
|
|
)
|
|
.clipShape(RoundedRectangle(cornerRadius: 28))
|
|
HStack {
|
|
if store.isLoading {
|
|
ProgressView()
|
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
|
.scaleEffect(0.8)
|
|
}
|
|
Text(store.isLoading ? NSLocalizedString("email_login.logging_in", comment: "") : NSLocalizedString("email_login.login_button", comment: ""))
|
|
.font(.system(size: 18, weight: .semibold))
|
|
.foregroundColor(.white)
|
|
}
|
|
}
|
|
.frame(height: 56)
|
|
}
|
|
.disabled(store.isLoading || email.isEmpty || verificationCode.isEmpty)
|
|
.opacity(isLoginButtonEnabled ? 1.0 : 0.5)
|
|
.padding(.horizontal, 32)
|
|
if let errorMessage = store.errorMessage {
|
|
Text(errorMessage)
|
|
.font(.system(size: 14))
|
|
.foregroundColor(.red)
|
|
.padding(.top, 16)
|
|
.padding(.horizontal, 32)
|
|
}
|
|
Spacer()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//#Preview {
|
|
// EMailLoginView(
|
|
// store: Store(
|
|
// initialState: EMailLoginFeature.State()
|
|
// ) {
|
|
// EMailLoginFeature()
|
|
// },
|
|
// onBack: {},
|
|
// showEmailLogin: .constant(true)
|
|
// )
|
|
//}
|