feat: 添加腾讯云COS Token管理功能及相关视图更新

- 在APIEndpoints.swift中新增tcToken端点以支持腾讯云COS Token获取。
- 在APIModels.swift中新增TcTokenRequest和TcTokenResponse模型,处理Token请求和响应。
- 在COSManager.swift中实现Token的获取、缓存和过期管理逻辑,提升API请求的安全性。
- 在LanguageSettingsView中添加调试功能,允许测试COS Token获取。
- 在多个视图中更新状态管理和导航逻辑,确保用户体验一致性。
- 在FeedFeature和HomeFeature中优化状态管理,简化视图逻辑。
This commit is contained in:
edwinQQQ
2025-07-18 20:50:25 +08:00
parent fb7ae9e0ad
commit 9a49d591c3
17 changed files with 1090 additions and 767 deletions

View File

@@ -5,14 +5,12 @@ import Combine
struct EMailLoginView: View {
let store: StoreOf<EMailLoginFeature>
let onBack: () -> Void
@Binding var showEmailLogin: Bool
// 使@StateUI
@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 {
@@ -20,12 +18,9 @@ struct EMailLoginView: View {
case verificationCode
}
//
private var isLoginButtonEnabled: Bool {
return !store.isLoading && !email.isEmpty && !verificationCode.isEmpty
}
//
private var getCodeButtonText: String {
if store.isCodeLoading {
return ""
@@ -35,188 +30,38 @@ struct EMailLoginView: View {
return NSLocalizedString("email_login.get_code", comment: "")
}
}
//
private var isCodeButtonEnabled: Bool {
return !store.isCodeLoading && codeCountdown == 0
}
var body: some View {
// WithViewStore(store, observe: { $0 }) { _ in
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)
.autocapitalization(.none)
.disableAutocorrection(true)
.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_verification_code", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
.keyboardType(.numberPad)
.focused($focusedField, equals: .verificationCode)
//
Button(action: {
// API
store.send(.getVerificationCodeTapped)
//
startCountdown()
}) {
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), // #D85EFF
Color(red: 0.54, green: 0.31, blue: 1.0) // #8A4FFF
],
startPoint: .leading,
endPoint: .trailing
)
.clipShape(RoundedRectangle(cornerRadius: 28))
HStack {
if store.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
}
Text(store.isLoading ? 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()
}
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))
@@ -240,7 +85,6 @@ struct EMailLoginView: View {
}
.onChange(of: store.isCodeLoading) { isCodeLoading in
let _ = WithPerceptionTracking {
// API
if !isCodeLoading && store.errorMessage == nil {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
focusedField = .verificationCode
@@ -250,14 +94,9 @@ struct EMailLoginView: View {
}
}
// MARK: -
private func startCountdown() {
stopCountdown()
//
codeCountdown = 60
// 使 SwiftUI Timer.publish
timerCancellable = Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
.sink { _ in
@@ -268,13 +107,159 @@ struct EMailLoginView: View {
}
}
}
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(
@@ -282,6 +267,7 @@ struct EMailLoginView: View {
// ) {
// EMailLoginFeature()
// },
// onBack: {}
// onBack: {},
// showEmailLogin: .constant(true)
// )
//}