import SwiftUI // MARK: - Login ViewModel @MainActor class LoginViewModel: ObservableObject { // MARK: - Published Properties @Published var showIDLogin: Bool = false @Published var showEmailLogin: Bool = false @Published var showLanguageSettings: Bool = false @Published var showUserAgreement: Bool = false @Published var showPrivacyPolicy: Bool = false @Published var isAgreementAccepted: Bool = true // 默认选中 @Published var showAgreementAlert: Bool = false @Published var isAnyLoginCompleted: Bool = false // MARK: - Callbacks var onLoginSuccess: (() -> Void)? // MARK: - Public Methods func onIDLoginTapped() { if isAgreementAccepted { showIDLogin = true } else { showAgreementAlert = true } } func onEmailLoginTapped() { if isAgreementAccepted { showEmailLogin = true } else { showAgreementAlert = true } } func onLanguageSettingsTapped() { showLanguageSettings = true } func onUserAgreementTapped() { showUserAgreement = true } func onPrivacyPolicyTapped() { showPrivacyPolicy = true } func onLoginCompleted() { isAnyLoginCompleted = true onLoginSuccess?() } func onBackFromIDLogin() { showIDLogin = false if isAnyLoginCompleted { onLoginSuccess?() } } func onBackFromEmailLogin() { showEmailLogin = false if isAnyLoginCompleted { onLoginSuccess?() } } } // MARK: - Login View struct LoginPage: View { @StateObject private var viewModel = LoginViewModel() let onLoginSuccess: () -> Void var body: some View { NavigationStack { GeometryReader { geometry in ZStack { backgroundView VStack(spacing: 0) { Image("top") .resizable() .aspectRatio(375/400, contentMode: .fit) .frame(maxWidth: .infinity) HStack { Text(LocalizedString("login.app_title", comment: "")) .font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width)) .foregroundColor(.white) .padding(.leading, 20) Spacer() } .padding(.bottom, 20) // 距离 top 图片底部的间距 Spacer() bottomSection } // 语言设置按钮 - 固定在页面右上角 languageSettingsButton .position(x: geometry.size.width - 40, y: 60) APILoadingEffectView() } } .ignoresSafeArea() .navigationBarHidden(true) .navigationDestination(isPresented: $viewModel.showIDLogin) { IDLoginPage( onBack: { viewModel.onBackFromIDLogin() }, onLoginSuccess: { viewModel.onLoginCompleted() } ) .navigationBarHidden(true) } .navigationDestination(isPresented: $viewModel.showEmailLogin) { EMailLoginPage( onBack: { viewModel.onBackFromEmailLogin() }, onLoginSuccess: { viewModel.onLoginCompleted() } ) .navigationBarHidden(true) } .sheet(isPresented: $viewModel.showLanguageSettings) { LanguageSettingsView(isPresented: $viewModel.showLanguageSettings) } .webView( isPresented: $viewModel.showUserAgreement, url: APIConfiguration.webURL(for: .userAgreement) ) .webView( isPresented: $viewModel.showPrivacyPolicy, url: APIConfiguration.webURL(for: .privacyPolicy) ) .alert(LocalizedString("login.agreement_alert_title", comment: ""), isPresented: $viewModel.showAgreementAlert) { Button(LocalizedString("login.agreement_alert_confirm", comment: "")) { } } message: { Text(LocalizedString("login.agreement_alert_message", comment: "")) } } .onAppear { viewModel.onLoginSuccess = onLoginSuccess } } // MARK: - 子视图 private var backgroundView: some View { LoginBackgroundView() } private var bottomSection: some View { VStack(spacing: 20) { loginButtons userAgreementComponent } .padding(.horizontal, 28) .padding(.bottom, 48) } private var loginButtons: some View { VStack(spacing: 20) { LoginButton( iconName: "person.circle", iconColor: .blue, title: LocalizedString("login.id_login", comment: ""), action: { viewModel.onIDLoginTapped() } ) LoginButton( iconName: "envelope", iconColor: .green, title: LocalizedString("login.email_login", comment: ""), action: { viewModel.onEmailLoginTapped() } ) } } private var languageSettingsButton: some View { Button(action: { viewModel.onLanguageSettingsTapped() }) { Image(systemName: "globe") .font(.system(size: 20)) .foregroundColor(.white.opacity(0.8)) } } private var userAgreementComponent: some View { UserAgreementComponent( isAgreed: $viewModel.isAgreementAccepted, onAgreementTap: { Task { @MainActor in viewModel.onUserAgreementTapped() } }, onPolicyTap: { Task { @MainActor in viewModel.onPrivacyPolicyTapped() } } ) .frame(height: 40) .padding(.horizontal, -20) } } #Preview { LoginPage(onLoginSuccess: {}) }