Files
e-party-iOS/yana/Views/IDLoginView.swift
edwinQQQ 3d00e459e3 feat: 更新文档和视图以支持iOS 17及优化用户体验
- 更新Yana项目文档,调整适用版本至iOS 17,确保与最新开发环境兼容。
- 在多个视图中重构代码,优化状态管理和视图逻辑,提升用户体验。
- 添加默认初始化器以简化状态管理,确保各个Feature的状态一致性。
- 更新视图组件,移除不必要的硬编码,增强代码可读性和维护性。
- 修复多个视图中的逻辑错误,确保功能正常运行。
2025-07-29 17:57:42 +08:00

303 lines
10 KiB
Swift

import SwiftUI
import ComposableArchitecture
import Perception
// MARK: -
struct IDLoginBackgroundView: View {
var body: some View {
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.all)
}
}
// MARK: -
struct IDLoginHeaderView: View {
let onBack: () -> Void
var body: some View {
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)
}
}
// MARK: -
struct IDLoginInputFieldView: View {
let iconName: String
let title: String
let text: Binding<String>
let onChange: (String) -> Void
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(iconName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
Text(title)
.font(.system(size: 16))
.foregroundColor(.white)
}
TextField("", text: text)
.textFieldStyle(PlainTextFieldStyle())
.font(.system(size: 16))
.foregroundColor(.white)
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(Color.white.opacity(0.1))
.cornerRadius(8)
.onChange(of: text.wrappedValue) { newValue in
onChange(newValue)
}
}
}
}
// MARK: -
struct IDLoginPasswordFieldView: View {
let password: Binding<String>
let isPasswordVisible: Binding<Bool>
let onChange: (String) -> Void
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Image("email icon")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
Text(LocalizedString("id_login.password", comment: ""))
.font(.system(size: 16))
.foregroundColor(.white)
}
HStack {
Group {
if isPasswordVisible.wrappedValue {
TextField("", text: password)
.textFieldStyle(PlainTextFieldStyle())
} else {
SecureField("", text: password)
.textFieldStyle(PlainTextFieldStyle())
}
}
Button(action: {
isPasswordVisible.wrappedValue.toggle()
}) {
Image(systemName: isPasswordVisible.wrappedValue ? "eye.slash" : "eye")
.foregroundColor(.white.opacity(0.7))
}
}
.font(.system(size: 16))
.foregroundColor(.white)
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(Color.white.opacity(0.1))
.cornerRadius(8)
.onChange(of: password.wrappedValue) { newValue in
onChange(newValue)
}
}
}
}
// MARK: -
struct IDLoginButtonView: View {
let isLoading: Bool
let isEnabled: Bool
let onTap: () -> Void
var body: some View {
Button(action: onTap) {
Group {
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(1.2)
} else {
Text(LocalizedString("id_login.login", comment: ""))
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
}
}
}
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.background(isEnabled ? Color.blue : Color.gray)
.cornerRadius(8)
.disabled(!isEnabled)
.padding(.top, 20)
}
}
// MARK: -
struct IDLoginErrorView: View {
let errorMessage: String?
var body: some View {
if let errorMessage = errorMessage {
Text(errorMessage)
.font(.system(size: 14))
.foregroundColor(.red)
.padding(.top, 16)
.padding(.horizontal, 32)
}
}
}
// MARK: -
struct IDLoginView: View {
let store: StoreOf<IDLoginFeature>
let onBack: () -> Void
@Binding var showIDLogin: Bool
// 使@StateUI
@State private var userID: String = ""
@State private var password: String = ""
@State private var isPasswordVisible: Bool = false
//
@State private var showRecoverPassword: Bool = false
//
private var isLoginButtonEnabled: Bool {
return !store.isLoading && !userID.isEmpty && !password.isEmpty
}
var body: some View {
WithPerceptionTracking {
GeometryReader { geometry in
ZStack {
//
IDLoginBackgroundView()
VStack(spacing: 0) {
//
IDLoginHeaderView(onBack: onBack)
Spacer()
.frame(height: 60)
//
Text(LocalizedString("id_login.title", comment: ""))
.font(.system(size: 28, weight: .medium))
.foregroundColor(.white)
.padding(.bottom, 80)
//
VStack(spacing: 20) {
// ID
IDLoginInputFieldView(
iconName: "id icon",
title: LocalizedString("id_login.user_id", comment: ""),
text: $userID,
onChange: { newValue in
store.send(.userIDChanged(newValue))
}
)
//
IDLoginPasswordFieldView(
password: $password,
isPasswordVisible: $isPasswordVisible,
onChange: { newValue in
store.send(.passwordChanged(newValue))
}
)
//
HStack {
Spacer()
Button(action: {
showRecoverPassword = true
}) {
Text(LocalizedString("id_login.forgot_password", comment: ""))
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.8))
}
}
//
IDLoginButtonView(
isLoading: store.isLoading,
isEnabled: isLoginButtonEnabled,
onTap: {
store.send(.loginButtonTapped(userID: userID, password: password))
}
)
//
IDLoginErrorView(errorMessage: store.errorMessage)
Spacer()
}
// API Loading
APILoadingEffectView()
}
}
}
.navigationBarHidden(true)
.navigationDestination(isPresented: $showRecoverPassword) {
WithPerceptionTracking {
RecoverPasswordView(
store: Store(
initialState: RecoverPasswordFeature.State()
) {
RecoverPasswordFeature()
},
onBack: {
showRecoverPassword = false
}
)
.navigationBarHidden(true)
}
}
.onAppear {
let _ = WithPerceptionTracking {
// TCA
userID = store.userID
password = store.password
isPasswordVisible = store.isPasswordVisible
#if DEBUG
debugInfoSync("🐛 Debug模式: 已移除硬编码测试凭据")
#endif
}
}
.onChange(of: store.loginStep) { newStep in
debugInfoSync("🔄 IDLoginView: loginStep 变化为 \(newStep)")
if newStep == .completed {
debugInfoSync("✅ IDLoginView: 登录成功,准备关闭自身")
showIDLogin = false
}
}
}
}
}
//#Preview {
// IDLoginView(
// store: Store(
// initialState: IDLoginFeature.State()
// ) {
// IDLoginFeature()
// },
// onBack: {},
// showIDLogin: .constant(true)
// )
//}