
- 注释掉Podfile中的Alamofire依赖,更新Podfile.lock以反映更改。 - 在yana/APIs/API-README.md中新增自动认证Header机制的详细文档,描述其工作原理、实现细节及最佳实践。 - 在yana/yanaApp.swift中将print语句替换为debugInfo以增强调试信息的输出。 - 在API相关文件中实现用户认证状态检查和相关header的自动添加逻辑,提升API请求的安全性和用户体验。 - 更新多个文件中的日志输出,确保在DEBUG模式下提供详细的调试信息。
226 lines
9.5 KiB
Swift
226 lines
9.5 KiB
Swift
import SwiftUI
|
|
import ComposableArchitecture
|
|
|
|
struct IDLoginView: View {
|
|
let store: StoreOf<IDLoginFeature>
|
|
let onBack: () -> Void
|
|
|
|
// 使用本地@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 {
|
|
GeometryReader { geometry in
|
|
ZStack {
|
|
// 背景图片 - 使用与登录页面相同的"bg"
|
|
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("id_login.title".localized)
|
|
.font(.system(size: 28, weight: .medium))
|
|
.foregroundColor(.white)
|
|
.padding(.bottom, 80)
|
|
|
|
// 输入框区域
|
|
VStack(spacing: 24) {
|
|
// ID 输入框
|
|
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: $userID) // 使用SwiftUI的绑定
|
|
.placeholder(when: userID.isEmpty) {
|
|
Text("placeholder.enter_id".localized)
|
|
.foregroundColor(.white.opacity(0.6))
|
|
}
|
|
.foregroundColor(.white)
|
|
.font(.system(size: 16))
|
|
.padding(.horizontal, 24)
|
|
.keyboardType(.numberPad)
|
|
}
|
|
|
|
// 密码输入框
|
|
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 {
|
|
if isPasswordVisible {
|
|
TextField("", text: $password) // 使用SwiftUI的绑定
|
|
.placeholder(when: password.isEmpty) {
|
|
Text("placeholder.enter_password".localized)
|
|
.foregroundColor(.white.opacity(0.6))
|
|
}
|
|
.foregroundColor(.white)
|
|
.font(.system(size: 16))
|
|
} else {
|
|
SecureField("", text: $password) // 使用SwiftUI的绑定
|
|
.placeholder(when: password.isEmpty) {
|
|
Text("placeholder.enter_password".localized)
|
|
.foregroundColor(.white.opacity(0.6))
|
|
}
|
|
.foregroundColor(.white)
|
|
.font(.system(size: 16))
|
|
}
|
|
|
|
Button(action: {
|
|
isPasswordVisible.toggle()
|
|
}) {
|
|
Image(systemName: isPasswordVisible ? "eye.slash" : "eye")
|
|
.foregroundColor(.white.opacity(0.7))
|
|
.font(.system(size: 18))
|
|
}
|
|
}
|
|
.padding(.horizontal, 24)
|
|
}
|
|
}
|
|
.padding(.horizontal, 32)
|
|
|
|
// Forgot Password 链接
|
|
HStack {
|
|
Spacer()
|
|
Button(action: {
|
|
showRecoverPassword = true
|
|
}) {
|
|
Text("id_login.forgot_password".localized)
|
|
.font(.system(size: 14))
|
|
.foregroundColor(.white.opacity(0.8))
|
|
}
|
|
}
|
|
.padding(.horizontal, 32)
|
|
.padding(.top, 16)
|
|
|
|
Spacer()
|
|
.frame(height: 60)
|
|
|
|
// 登录按钮
|
|
Button(action: {
|
|
// 发送登录action时传递本地状态
|
|
store.send(.loginButtonTapped(userID: userID, password: password))
|
|
}) {
|
|
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 ? "id_login.logging_in".localized : "id_login.login_button".localized)
|
|
.font(.system(size: 18, weight: .semibold))
|
|
.foregroundColor(.white)
|
|
}
|
|
}
|
|
.frame(height: 56)
|
|
}
|
|
.disabled(store.isLoading || userID.isEmpty || password.isEmpty)
|
|
.opacity(isLoginButtonEnabled ? 1.0 : 0.5) // 透明度50%当条件不满足时
|
|
.padding(.horizontal, 32)
|
|
|
|
// 错误信息
|
|
if let errorMessage = store.errorMessage {
|
|
Text(errorMessage)
|
|
.font(.system(size: 14))
|
|
.foregroundColor(.red)
|
|
.padding(.top, 16)
|
|
.padding(.horizontal, 32)
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
|
|
// 隐藏的NavigationLink - 导航到密码恢复页面
|
|
NavigationLink(
|
|
destination: RecoverPasswordView(
|
|
store: Store(
|
|
initialState: RecoverPasswordFeature.State()
|
|
) {
|
|
RecoverPasswordFeature()
|
|
},
|
|
onBack: {
|
|
showRecoverPassword = false
|
|
}
|
|
)
|
|
.navigationBarHidden(true),
|
|
isActive: $showRecoverPassword
|
|
) {
|
|
EmptyView()
|
|
}
|
|
.hidden()
|
|
}
|
|
}
|
|
.onAppear {
|
|
// 初始化时同步TCA状态到本地状态
|
|
userID = store.userID
|
|
password = store.password
|
|
isPasswordVisible = store.isPasswordVisible
|
|
|
|
#if DEBUG
|
|
// 移除测试用的硬编码凭据
|
|
debugInfo("🐛 Debug模式: 已移除硬编码测试凭据")
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
IDLoginView(
|
|
store: Store(
|
|
initialState: IDLoginFeature.State()
|
|
) {
|
|
IDLoginFeature()
|
|
},
|
|
onBack: {}
|
|
)
|
|
}
|