feat: 更新项目配置和功能模块
- 修改Package.swift以支持iOS 15和macOS 12。 - 更新swift-tca-architecture-guidelines.mdc中的alwaysApply设置为false。 - 注释掉AppDelegate中的NIMSDK导入,移除不再使用的NIMConfigurationManager和NIMSessionManager文件。 - 添加新的API相关文件,包括EMailLoginFeature、IDLoginFeature和相关视图,增强登录功能。 - 更新APIConstants和APIEndpoints以反映新的API路径。 - 添加本地化支持文件,包含英文和中文简体的本地化字符串。 - 新增字体管理和安全工具类,支持AES和DES加密。 - 更新Xcode项目配置,调整版本号和启动画面设置。
This commit is contained in:
43
yana/Views/AppRootView.swift
Normal file
43
yana/Views/AppRootView.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct AppRootView: View {
|
||||
@State private var shouldShowMainApp = false
|
||||
|
||||
let splashStore = Store(
|
||||
initialState: SplashFeature.State()
|
||||
) {
|
||||
SplashFeature()
|
||||
}
|
||||
|
||||
let loginStore = Store(
|
||||
initialState: LoginFeature.State()
|
||||
) {
|
||||
LoginFeature()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if shouldShowMainApp {
|
||||
// 登录界面
|
||||
LoginView(store: loginStore)
|
||||
.transition(.opacity.animation(.easeInOut(duration: 0.5)))
|
||||
} else {
|
||||
// 启动画面
|
||||
SplashView(store: splashStore)
|
||||
.transition(.opacity.animation(.easeInOut(duration: 0.5)))
|
||||
.onReceive(NotificationCenter.default.publisher(for: .splashFinished)) { _ in
|
||||
shouldShowMainApp = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Notification.Name {
|
||||
static let splashFinished = Notification.Name("splashFinished")
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AppRootView()
|
||||
}
|
59
yana/Views/Components/LoginButton.swift
Normal file
59
yana/Views/Components/LoginButton.swift
Normal file
@@ -0,0 +1,59 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Login Button Component
|
||||
struct LoginButton: View {
|
||||
let iconName: String
|
||||
let iconColor: Color
|
||||
let title: String
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
ZStack {
|
||||
// 背景
|
||||
Color.white
|
||||
.cornerRadius(28)
|
||||
|
||||
// 居中的文本
|
||||
Text(title)
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.frame(alignment: .center)
|
||||
.foregroundColor(Color(hex: 0x313131))
|
||||
|
||||
// 左侧图标
|
||||
HStack {
|
||||
Image(systemName: iconName)
|
||||
.foregroundColor(iconColor)
|
||||
.font(.system(size: 30))
|
||||
.padding(.leading, 33)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.frame(height: 56)
|
||||
.padding(.horizontal, 29)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
VStack(spacing: 20) {
|
||||
LoginButton(
|
||||
iconName: "person.circle.fill",
|
||||
iconColor: .green,
|
||||
title: "ID Login"
|
||||
) {
|
||||
// Preview action
|
||||
}
|
||||
|
||||
LoginButton(
|
||||
iconName: "envelope.fill",
|
||||
iconColor: .blue,
|
||||
title: "Email Login"
|
||||
) {
|
||||
// Preview action
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.gray.opacity(0.2))
|
||||
}
|
88
yana/Views/Components/UserAgreementView.swift
Normal file
88
yana/Views/Components/UserAgreementView.swift
Normal file
@@ -0,0 +1,88 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - User Agreement View Component
|
||||
struct UserAgreementView: View {
|
||||
@Binding var isAgreed: Bool
|
||||
let onUserServiceTapped: () -> Void
|
||||
let onPrivacyPolicyTapped: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 12) {
|
||||
// 左侧勾选按钮
|
||||
Button(action: {
|
||||
isAgreed.toggle()
|
||||
}) {
|
||||
Image(systemName: isAgreed ? "checkmark.circle.fill" : "circle")
|
||||
.font(.system(size: 22))
|
||||
.foregroundColor(isAgreed ? Color(hex: 0x8A4FFF) : Color(hex: 0x666666))
|
||||
}
|
||||
|
||||
// 右侧富文本
|
||||
Text(createAttributedText())
|
||||
.font(.system(size: 14))
|
||||
.multilineTextAlignment(.leading)
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
if url.absoluteString == "user-service-agreement" {
|
||||
onUserServiceTapped()
|
||||
return .handled
|
||||
} else if url.absoluteString == "privacy-policy" {
|
||||
onPrivacyPolicyTapped()
|
||||
return .handled
|
||||
}
|
||||
return .systemAction
|
||||
})
|
||||
}
|
||||
.frame(maxWidth: .infinity) // 占满可用宽度
|
||||
.padding(.horizontal, 29) // 与登录按钮保持一致的边距
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
private func createAttributedText() -> AttributedString {
|
||||
var attributedString = AttributedString("login.agreement_policy".localized)
|
||||
|
||||
// 设置默认颜色
|
||||
attributedString.foregroundColor = Color(hex: 0x666666)
|
||||
|
||||
// 找到并设置 "用户协议" 的样式和链接
|
||||
if let userServiceRange = attributedString.range(of: "login.agreement".localized) {
|
||||
attributedString[userServiceRange].foregroundColor = Color(hex: 0x8A4FFF)
|
||||
attributedString[userServiceRange].underlineStyle = .single
|
||||
attributedString[userServiceRange].link = URL(string: "user-service-agreement")
|
||||
}
|
||||
|
||||
// 找到并设置 "隐私政策" 的样式和链接
|
||||
if let privacyPolicyRange = attributedString.range(of: "login.policy".localized) {
|
||||
attributedString[privacyPolicyRange].foregroundColor = Color(hex: 0x8A4FFF)
|
||||
attributedString[privacyPolicyRange].underlineStyle = .single
|
||||
attributedString[privacyPolicyRange].link = URL(string: "privacy-policy")
|
||||
}
|
||||
|
||||
return attributedString
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
VStack(spacing: 20) {
|
||||
UserAgreementView(
|
||||
isAgreed: .constant(true),
|
||||
onUserServiceTapped: {
|
||||
print("User Service Agreement tapped")
|
||||
},
|
||||
onPrivacyPolicyTapped: {
|
||||
print("Privacy Policy tapped")
|
||||
}
|
||||
)
|
||||
|
||||
UserAgreementView(
|
||||
isAgreed: .constant(true),
|
||||
onUserServiceTapped: {
|
||||
print("User Service Agreement tapped")
|
||||
},
|
||||
onPrivacyPolicyTapped: {
|
||||
print("Privacy Policy tapped")
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.gray.opacity(0.1))
|
||||
}
|
55
yana/Views/Components/WebView.swift
Normal file
55
yana/Views/Components/WebView.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
import SwiftUI
|
||||
import SafariServices
|
||||
|
||||
// MARK: - Web View Component
|
||||
struct WebView: UIViewControllerRepresentable {
|
||||
let url: URL
|
||||
|
||||
func makeUIViewController(context: Context) -> SFSafariViewController {
|
||||
let config = SFSafariViewController.Configuration()
|
||||
config.entersReaderIfAvailable = false
|
||||
config.barCollapsingEnabled = true
|
||||
|
||||
let safariViewController = SFSafariViewController(url: url, configuration: config)
|
||||
safariViewController.preferredBarTintColor = UIColor.systemBackground
|
||||
safariViewController.preferredControlTintColor = UIColor.systemBlue
|
||||
|
||||
return safariViewController
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) {
|
||||
// Safari View Controller 不需要更新
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Web View Modifier
|
||||
extension View {
|
||||
/// 显示 Web 页面的修饰符
|
||||
/// - Parameters:
|
||||
/// - isPresented: 是否显示的绑定变量
|
||||
/// - url: 要显示的 URL
|
||||
/// - Returns: 修饰后的视图
|
||||
func webView(isPresented: Binding<Bool>, url: URL?) -> some View {
|
||||
self.sheet(isPresented: isPresented) {
|
||||
if let url = url {
|
||||
WebView(url: url)
|
||||
} else {
|
||||
Text("无法加载页面")
|
||||
.foregroundColor(.red)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
VStack {
|
||||
Button("打开网页") {
|
||||
// 预览时不执行任何操作
|
||||
}
|
||||
}
|
||||
.webView(
|
||||
isPresented: .constant(true),
|
||||
url: URL(string: "https://www.apple.com")
|
||||
)
|
||||
}
|
214
yana/Views/EMailLoginView.swift
Normal file
214
yana/Views/EMailLoginView.swift
Normal file
@@ -0,0 +1,214 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct EMailLoginView: View {
|
||||
let store: StoreOf<EMailLoginFeature>
|
||||
let onBack: () -> Void
|
||||
|
||||
// 使用本地@State管理UI状态
|
||||
@State private var email: String = ""
|
||||
@State private var verificationCode: String = ""
|
||||
|
||||
// 计算登录按钮是否可用
|
||||
private var isLoginButtonEnabled: Bool {
|
||||
return !store.isLoading && !email.isEmpty && !verificationCode.isEmpty
|
||||
}
|
||||
|
||||
// 计算获取验证码按钮文本
|
||||
private var getCodeButtonText: String {
|
||||
if store.isCodeLoading {
|
||||
return ""
|
||||
} else if store.codeCountdown > 0 {
|
||||
return "\(store.codeCountdown)S"
|
||||
} else {
|
||||
return "email_login.get_code".localized
|
||||
}
|
||||
}
|
||||
|
||||
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("email_login.title".localized)
|
||||
.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("placeholder.enter_email".localized)
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 16))
|
||||
.padding(.horizontal, 24)
|
||||
.keyboardType(.emailAddress)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
}
|
||||
|
||||
// 验证码输入框
|
||||
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("placeholder.enter_verification_code".localized)
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 16))
|
||||
.keyboardType(.numberPad)
|
||||
|
||||
// 获取验证码按钮
|
||||
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(store.isCodeButtonEnabled && !email.isEmpty ? 0.2 : 0.1))
|
||||
)
|
||||
}
|
||||
.disabled(!store.isCodeButtonEnabled || email.isEmpty || store.isCodeLoading)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
|
||||
Spacer()
|
||||
.frame(height: 60)
|
||||
|
||||
// 登录按钮
|
||||
Button(action: {
|
||||
// 发送登录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 ? "email_login.logging_in".localized : "email_login.login_button".localized)
|
||||
.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// 初始化时同步TCA状态到本地状态
|
||||
email = store.email
|
||||
verificationCode = store.verificationCode
|
||||
|
||||
#if DEBUG
|
||||
// Debug环境下,确保默认数据已加载
|
||||
if email.isEmpty {
|
||||
email = "85494536@gmail.com"
|
||||
}
|
||||
if verificationCode.isEmpty {
|
||||
verificationCode = "784544"
|
||||
}
|
||||
print("🐛 Debug模式: 默认邮箱=\(email), 默认验证码=\(verificationCode)")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
EMailLoginView(
|
||||
store: Store(
|
||||
initialState: EMailLoginFeature.State()
|
||||
) {
|
||||
EMailLoginFeature()
|
||||
},
|
||||
onBack: {}
|
||||
)
|
||||
}
|
205
yana/Views/IDLoginView.swift
Normal file
205
yana/Views/IDLoginView.swift
Normal file
@@ -0,0 +1,205 @@
|
||||
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
|
||||
|
||||
// 计算登录按钮是否可用
|
||||
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: {
|
||||
store.send(.forgotPasswordTapped)
|
||||
}) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// 初始化时同步TCA状态到本地状态
|
||||
userID = store.userID
|
||||
password = store.password
|
||||
isPasswordVisible = store.isPasswordVisible
|
||||
|
||||
#if DEBUG
|
||||
// 移除测试用的硬编码凭据
|
||||
print("🐛 Debug模式: 已移除硬编码测试凭据")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
IDLoginView(
|
||||
store: Store(
|
||||
initialState: IDLoginFeature.State()
|
||||
) {
|
||||
IDLoginFeature()
|
||||
},
|
||||
onBack: {}
|
||||
)
|
||||
}
|
96
yana/Views/LanguageSettingsView.swift
Normal file
96
yana/Views/LanguageSettingsView.swift
Normal file
@@ -0,0 +1,96 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct LanguageSettingsView: View {
|
||||
@ObservedObject private var localizationManager = LocalizationManager.shared
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
init(isPresented: Binding<Bool> = .constant(true)) {
|
||||
self._isPresented = isPresented
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
Section {
|
||||
ForEach(LocalizationManager.SupportedLanguage.allCases, id: \.rawValue) { language in
|
||||
LanguageRow(
|
||||
language: language,
|
||||
isSelected: localizationManager.currentLanguage == language
|
||||
) {
|
||||
localizationManager.switchLanguage(to: language)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("选择语言 / Select Language")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Section {
|
||||
HStack {
|
||||
Text("当前语言 / Current Language")
|
||||
.font(.body)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(localizationManager.currentLanguage.localizedDisplayName)
|
||||
.font(.body)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
} header: {
|
||||
Text("语言信息 / Language Info")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.navigationTitle("语言设置 / Language")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("返回 / Back") {
|
||||
isPresented = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LanguageRow: View {
|
||||
let language: LocalizationManager.SupportedLanguage
|
||||
let isSelected: Bool
|
||||
let onTap: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: onTap) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(language.localizedDisplayName)
|
||||
.font(.body)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text(language.displayName)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.blue)
|
||||
.font(.system(size: 20))
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
#Preview {
|
||||
LanguageSettingsView(isPresented: .constant(true))
|
||||
}
|
160
yana/Views/LoginView.swift
Normal file
160
yana/Views/LoginView.swift
Normal file
@@ -0,0 +1,160 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
// PreferenceKey 用于传递图片高度
|
||||
struct ImageHeightPreferenceKey: PreferenceKey {
|
||||
static var defaultValue: CGFloat = 0
|
||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||
value = max(value, nextValue())
|
||||
}
|
||||
}
|
||||
|
||||
struct LoginView: View {
|
||||
let store: StoreOf<LoginFeature>
|
||||
@State private var topImageHeight: CGFloat = 120 // 默认值
|
||||
@ObservedObject private var localizationManager = LocalizationManager.shared
|
||||
@State private var showLanguageSettings = false
|
||||
@State private var isAgreedToTerms = true
|
||||
@State private var showUserAgreement = false
|
||||
@State private var showPrivacyPolicy = false
|
||||
@State private var showIDLogin = false // 使用SwiftUI的@State管理导航
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// 使用与 splash 相同的背景图片
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.ignoresSafeArea(.all)
|
||||
VStack(spacing: 0) {
|
||||
// 上半部分的"top"图片
|
||||
ZStack {
|
||||
Image("top")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, -100)
|
||||
.background(
|
||||
GeometryReader { topImageGeometry in
|
||||
Color.clear.preference(key: ImageHeightPreferenceKey.self, value: topImageGeometry.size.height)
|
||||
}
|
||||
)
|
||||
// E-PARTI 文本,底部对齐"top"图片底部,间距20
|
||||
HStack {
|
||||
Text("login.app_title".localized)
|
||||
.font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width))
|
||||
.foregroundColor(.white)
|
||||
.padding(.leading, 20)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, max(0, topImageHeight - 100)) // top图片高度 - 140
|
||||
|
||||
// 语言切换按钮(右上角)- 仅在 Debug 环境下显示
|
||||
#if DEBUG
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: {
|
||||
showLanguageSettings = true
|
||||
}) {
|
||||
Image(systemName: "globe")
|
||||
.frame(width: 40, height: 40)
|
||||
.font(.system(size: 20))
|
||||
.foregroundColor(.white)
|
||||
.background(Color.black.opacity(0.3))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.padding(.trailing, 16)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
#endif
|
||||
|
||||
VStack(spacing: 24) {
|
||||
// ID Login 按钮
|
||||
LoginButton(
|
||||
iconName: "person.circle.fill",
|
||||
iconColor: .green,
|
||||
title: "login.id_login".localized
|
||||
) {
|
||||
showIDLogin = true // 直接设置SwiftUI状态
|
||||
}
|
||||
// Email Login 按钮
|
||||
LoginButton(
|
||||
iconName: "envelope.fill",
|
||||
iconColor: .blue,
|
||||
title: "login.email_login".localized
|
||||
) {
|
||||
// TODO: 处理Email登录
|
||||
}
|
||||
}.padding(.top, max(0, topImageHeight+140))
|
||||
}
|
||||
.onPreferenceChange(ImageHeightPreferenceKey.self) { imageHeight in
|
||||
topImageHeight = imageHeight
|
||||
}
|
||||
|
||||
// 间距,使登录按钮区域顶部距离"top"图片底部40pt
|
||||
Spacer()
|
||||
.frame(height: 120)
|
||||
|
||||
// 用户协议组件
|
||||
UserAgreementView(
|
||||
isAgreed: $isAgreedToTerms,
|
||||
onUserServiceTapped: {
|
||||
showUserAgreement = true
|
||||
},
|
||||
onPrivacyPolicyTapped: {
|
||||
showPrivacyPolicy = true
|
||||
}
|
||||
)
|
||||
.padding(.horizontal, 28)
|
||||
.padding(.bottom, 140)
|
||||
}
|
||||
|
||||
// 隐藏的NavigationLink - 使用纯SwiftUI方式
|
||||
NavigationLink(
|
||||
destination: IDLoginView(
|
||||
store: store.scope(
|
||||
state: \.idLoginState,
|
||||
action: \.idLogin
|
||||
),
|
||||
onBack: {
|
||||
showIDLogin = false // 直接设置SwiftUI状态
|
||||
}
|
||||
)
|
||||
.navigationBarHidden(true),
|
||||
isActive: $showIDLogin // 使用SwiftUI的绑定
|
||||
) {
|
||||
EmptyView()
|
||||
}
|
||||
.hidden()
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.sheet(isPresented: $showLanguageSettings) {
|
||||
LanguageSettingsView(isPresented: $showLanguageSettings)
|
||||
}
|
||||
.webView(
|
||||
isPresented: $showUserAgreement,
|
||||
url: APIConfiguration.webURL(for: .userAgreement)
|
||||
)
|
||||
.webView(
|
||||
isPresented: $showPrivacyPolicy,
|
||||
url: APIConfiguration.webURL(for: .privacyPolicy)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LoginView(
|
||||
store: Store(
|
||||
initialState: LoginFeature.State()
|
||||
) {
|
||||
LoginFeature()
|
||||
}
|
||||
)
|
||||
}
|
49
yana/Views/SplashView.swift
Normal file
49
yana/Views/SplashView.swift
Normal file
@@ -0,0 +1,49 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct SplashView: View {
|
||||
let store: StoreOf<SplashFeature>
|
||||
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
ZStack {
|
||||
// 背景图片 - 全屏显示
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.ignoresSafeArea(.all)
|
||||
|
||||
VStack(spacing: 32) {
|
||||
Spacer()
|
||||
.frame(height: 200) // 与 storyboard 中的约束对应
|
||||
|
||||
// Logo 图片 - 100x100
|
||||
Image("logo")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 100, height: 100)
|
||||
|
||||
// 应用标题 - 白色,40pt字体
|
||||
Text("E-Parti")
|
||||
.font(.system(size: 40, weight: .regular))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
store.send(.onAppear)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SplashView(
|
||||
store: Store(
|
||||
initialState: SplashFeature.State()
|
||||
) {
|
||||
SplashFeature()
|
||||
}
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user