
- 将LocalizedString替换为硬编码字符串,提升代码可读性。 - 重构输入框组件,使用CustomInputField以统一输入框样式和逻辑。 - 更新按钮文本和样式,确保一致性和视觉效果。 - 调整布局和间距,优化用户界面体验。 - 增加验证码输入框的获取按钮功能,提升交互性。
324 lines
12 KiB
Swift
324 lines
12 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: - 通用输入框组件
|
||
enum InputFieldType {
|
||
case text
|
||
case number
|
||
case password
|
||
case verificationCode
|
||
}
|
||
|
||
struct CustomInputField: View {
|
||
let type: InputFieldType
|
||
let placeholder: String
|
||
let text: Binding<String>
|
||
let isPasswordVisible: Binding<Bool>?
|
||
let onGetCode: (() -> Void)?
|
||
let isCodeButtonEnabled: Bool
|
||
let isCodeLoading: Bool
|
||
let getCodeButtonText: String
|
||
|
||
init(
|
||
type: InputFieldType,
|
||
placeholder: String,
|
||
text: Binding<String>,
|
||
isPasswordVisible: Binding<Bool>? = nil,
|
||
onGetCode: (() -> Void)? = nil,
|
||
isCodeButtonEnabled: Bool = false,
|
||
isCodeLoading: Bool = false,
|
||
getCodeButtonText: String = ""
|
||
) {
|
||
self.type = type
|
||
self.placeholder = placeholder
|
||
self.text = text
|
||
self.isPasswordVisible = isPasswordVisible
|
||
self.onGetCode = onGetCode
|
||
self.isCodeButtonEnabled = isCodeButtonEnabled
|
||
self.isCodeLoading = isCodeLoading
|
||
self.getCodeButtonText = getCodeButtonText
|
||
}
|
||
|
||
var body: 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 {
|
||
// 输入框
|
||
Group {
|
||
switch type {
|
||
case .text, .number:
|
||
TextField("", text: text)
|
||
.placeholder(when: text.wrappedValue.isEmpty) {
|
||
Text(placeholder)
|
||
.foregroundColor(.white.opacity(0.6))
|
||
}
|
||
.keyboardType(type == .number ? .numberPad : .default)
|
||
case .password:
|
||
if let isPasswordVisible = isPasswordVisible {
|
||
if isPasswordVisible.wrappedValue {
|
||
TextField("", text: text)
|
||
.placeholder(when: text.wrappedValue.isEmpty) {
|
||
Text(placeholder)
|
||
.foregroundColor(.white.opacity(0.6))
|
||
}
|
||
} else {
|
||
SecureField("", text: text)
|
||
.placeholder(when: text.wrappedValue.isEmpty) {
|
||
Text(placeholder)
|
||
.foregroundColor(.white.opacity(0.6))
|
||
}
|
||
}
|
||
}
|
||
case .verificationCode:
|
||
TextField("", text: text)
|
||
.placeholder(when: text.wrappedValue.isEmpty) {
|
||
Text(placeholder)
|
||
.foregroundColor(.white.opacity(0.6))
|
||
}
|
||
.keyboardType(.numberPad)
|
||
}
|
||
}
|
||
.foregroundColor(.white)
|
||
.font(.system(size: 16))
|
||
|
||
// 右侧按钮
|
||
if type == .password, let isPasswordVisible = isPasswordVisible {
|
||
Button(action: {
|
||
isPasswordVisible.wrappedValue.toggle()
|
||
}) {
|
||
Image(systemName: isPasswordVisible.wrappedValue ? "eye.slash" : "eye")
|
||
.foregroundColor(.white.opacity(0.7))
|
||
.font(.system(size: 18))
|
||
}
|
||
} else if type == .verificationCode, let onGetCode = onGetCode {
|
||
Button(action: onGetCode) {
|
||
ZStack {
|
||
if 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(isCodeButtonEnabled ? 0.2 : 0.1))
|
||
)
|
||
}
|
||
.disabled(!isCodeButtonEnabled || isCodeLoading)
|
||
}
|
||
}
|
||
.padding(.horizontal, 24)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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("Login")
|
||
.font(.system(size: 16, weight: .medium))
|
||
.foregroundColor(.white)
|
||
}
|
||
}
|
||
}
|
||
.frame(maxWidth: .infinity)
|
||
.padding(.vertical, 16)
|
||
.background(isEnabled ? Color(red: 0.5, green: 0.3, blue: 0.8) : Color.gray)
|
||
.cornerRadius(8)
|
||
.disabled(!isEnabled)
|
||
}
|
||
}
|
||
|
||
// 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("ID Login")
|
||
.font(.system(size: 28, weight: .bold))
|
||
.foregroundColor(.white)
|
||
.padding(.bottom, 60)
|
||
|
||
// 输入框区域
|
||
VStack(spacing: 24) {
|
||
// 用户ID输入框(只允许数字)
|
||
CustomInputField(
|
||
type: .number,
|
||
placeholder: "Please enter ID",
|
||
text: $userID
|
||
)
|
||
|
||
// 密码输入框(带眼睛按钮)
|
||
CustomInputField(
|
||
type: .password,
|
||
placeholder: "Please enter password",
|
||
text: $password,
|
||
isPasswordVisible: $isPasswordVisible
|
||
)
|
||
}
|
||
.padding(.horizontal, 32)
|
||
|
||
Spacer()
|
||
.frame(height: 80)
|
||
|
||
// 忘记密码按钮
|
||
HStack {
|
||
Spacer()
|
||
Button(action: {
|
||
showRecoverPassword = true
|
||
}) {
|
||
Text("Forgot Password?")
|
||
.font(.system(size: 14))
|
||
.foregroundColor(.white.opacity(0.8))
|
||
}
|
||
}
|
||
.padding(.horizontal, 32)
|
||
.padding(.bottom, 20)
|
||
|
||
// 登录按钮
|
||
IDLoginButtonView(
|
||
isLoading: store.isLoading,
|
||
isEnabled: isLoginButtonEnabled,
|
||
onTap: {
|
||
store.send(.loginButtonTapped(userID: userID, password: password))
|
||
}
|
||
)
|
||
.padding(.horizontal, 32)
|
||
|
||
Spacer()
|
||
}
|
||
}
|
||
}
|
||
.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)
|
||
// )
|
||
//}
|