
- 更新Yana项目文档,调整适用版本至iOS 17,确保与最新开发环境兼容。 - 在多个视图中重构代码,优化状态管理和视图逻辑,提升用户体验。 - 添加默认初始化器以简化状态管理,确保各个Feature的状态一致性。 - 更新视图组件,移除不必要的硬编码,增强代码可读性和维护性。 - 修复多个视图中的逻辑错误,确保功能正常运行。
303 lines
10 KiB
Swift
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
|
|
|
|
// 使用本地@State管理UI状态
|
|
@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)
|
|
// )
|
|
//}
|