diff --git a/wake/Utils/Router.swift b/wake/Utils/Router.swift new file mode 100644 index 0000000..18e7231 --- /dev/null +++ b/wake/Utils/Router.swift @@ -0,0 +1,41 @@ +import SwiftUI + +enum AppRoute: Hashable { + case avatarBox + case feedbackView + case feedbackDetail(type: FeedbackView.FeedbackType) + // Add other routes here as needed + + @ViewBuilder + var view: some View { + switch self { + case .avatarBox: + AvatarBoxView() + case .feedbackView: + FeedbackView() + case .feedbackDetail(let type): + FeedbackDetailView(feedbackType: type) + } + } +} + +@MainActor +class Router: ObservableObject { + static let shared = Router() + + @Published var path = NavigationPath() + + private init() {} + + func navigate(to destination: AppRoute) { + path.append(destination) + } + + func pop() { + path.removeLast() + } + + func popToRoot() { + path = NavigationPath() + } +} diff --git a/wake/View/Blind/AvatarBox.swift b/wake/View/Blind/AvatarBox.swift new file mode 100644 index 0000000..de7f62a --- /dev/null +++ b/wake/View/Blind/AvatarBox.swift @@ -0,0 +1,105 @@ +import SwiftUI + +struct AvatarBoxView: View { + @Environment(\.dismiss) private var dismiss + @EnvironmentObject private var router: Router + @State private var isAnimating = false + + var body: some View { + ZStack { + // Background color + Color.white + .ignoresSafeArea() + + VStack(spacing: 0) { + // Navigation Bar + HStack { + Button(action: { + dismiss() + }) { + Image(systemName: "chevron.left") + .font(.system(size: 17, weight: .medium)) + .foregroundColor(.black) + .padding() + } + + Spacer() + + Text("动画页面") + .font(.headline) + .foregroundColor(.black) + + Spacer() + + // Invisible spacer to center the title + Color.clear + .frame(width: 44, height: 44) + } + .frame(height: 44) + .background(Color.white) + + Spacer() + + // Animated Content + ZStack { + // Pulsing circle animation + Circle() + .fill(Color.blue.opacity(0.2)) + .frame(width: 200, height: 200) + .scaleEffect(isAnimating ? 1.5 : 1.0) + .opacity(isAnimating ? 0.5 : 1.0) + .animation( + Animation.easeInOut(duration: 1.5) + .repeatForever(autoreverses: true), + value: isAnimating + ) + + // Center icon + Image(systemName: "sparkles") + .font(.system(size: 60)) + .foregroundColor(.blue) + .rotationEffect(.degrees(isAnimating ? 360 : 0)) + .animation( + Animation.linear(duration: 8) + .repeatForever(autoreverses: false), + value: isAnimating + ) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + Spacer() + + // Bottom Button + Button(action: { + router.navigate(to: .feedbackView) + }) { + Text("Continue") + .font(.headline) + .foregroundColor(.themeTextMessageMain) + .frame(maxWidth: .infinity) + .frame(height: 56) + .background(Color.themePrimary) + .cornerRadius(25) + .padding(.horizontal, 24) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .navigationBarBackButtonHidden(true) + .navigationBarHidden(true) + .onAppear { + isAnimating = true + } + } +} + +// MARK: - Preview +struct AvatarBoxView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + AvatarBoxView() + .environmentObject(Router.shared) + } + .navigationViewStyle(StackNavigationViewStyle()) + } +} diff --git a/wake/View/Feedback.swift b/wake/View/Feedback.swift new file mode 100644 index 0000000..920ffb1 --- /dev/null +++ b/wake/View/Feedback.swift @@ -0,0 +1,273 @@ +import SwiftUI + +struct FeedbackView: View { + @Environment(\.dismiss) private var dismiss + @EnvironmentObject private var router: Router + + @State private var selectedFeedback: FeedbackType? = FeedbackType.allCases.first + @State private var showNextScreen = false + + enum FeedbackType: String, CaseIterable, Identifiable { + case excellent = "Excellent" + case good = "Good" + case okay = "Okay" + case bad = "Bad" + + var id: String { self.rawValue } + var icon: String { + switch self { + case .excellent: return "😘" + case .good: return "😊" + case .okay: return "😐" + case .bad: return "😞" + } + } + } + + var body: some View { + VStack(spacing: 0) { + // Custom Navigation Bar + HStack { + // Back Button + Button(action: { dismiss() }) { + Image(systemName: "chevron.left") + .font(.system(size: 17, weight: .semibold)) + .foregroundColor(.primary) + .frame(width: 44, height: 44) + } + + // Title + Text("Feedback") + .font(Typography.font(for: .title2, family: .quicksandBold)) + .frame(maxWidth: .infinity) + + // Spacer to balance the HStack + Spacer() + .frame(width: 44, height: 44) + } + .frame(height: 44) + .background(Color.themeTextWhiteSecondary) + + // Main Content + GeometryReader { geometry in + ScrollView { + VStack(spacing: 24) { + // Top spacing for vertical centering + Spacer(minLength: 0) + + VStack(spacing: 24) { + Text("How are you feeling?") + .font(Typography.font(for: .title2, family: .quicksandBold)) + .fontWeight(.semibold) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding(.bottom, 50) + + // Feedback Type Selection + VStack(spacing: 12) { + ForEach(FeedbackType.allCases) { type in + Button(action: { + selectedFeedback = type + }) { + let isSelected = selectedFeedback == type + HStack { + Text(type.icon) + .font(.body) + .foregroundColor(isSelected ? .white : .primary) + + Text(type.rawValue) + .font(.body) + .foregroundColor(Color.themeTextMessageMain) + + Spacer() + } + .padding() + .background( + RoundedRectangle(cornerRadius: 12) + .fill(isSelected ? Color.themePrimary : Color.themePrimaryLight) + ) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(isSelected ? Color.themePrimary : Color.themePrimaryLight, lineWidth: 1) + ) + } + .buttonStyle(PlainButtonStyle()) + } + } + .padding(.bottom, 24) + } + .padding(.horizontal, 20) + .padding(.vertical, 24) + .background(Color.white) + .cornerRadius(16) + .shadow(color: Color.black.opacity(0.2), radius: 12, x: 0, y: 4) + .padding(.horizontal, 16) + .frame(minHeight: geometry.size.height - 120) // Subtract navigation bar and bottom button height + + // Bottom spacing for vertical centering + Spacer(minLength: 0) + } + .frame(maxWidth: .infinity, minHeight: geometry.size.height - 44) // Subtract navigation bar height + } + .background(Color.themeTextWhiteSecondary) + } + + // Continue Button + Button(action: { + if let selected = selectedFeedback { + router.navigate(to: .feedbackDetail(type: selected)) + } + }) { + Text("Continue") + .font(.headline) + .foregroundColor(selectedFeedback != nil ? .white : .gray) + .frame(maxWidth: .infinity) + .frame(height: 56) + .background( + RoundedRectangle(cornerRadius: 25) + .fill(selectedFeedback != nil ? + Color.themePrimary : Color(.systemGray5)) + ) + .padding(.horizontal, 24) + } + .disabled(selectedFeedback == nil) + } + .navigationBarHidden(true) + } +} + +// Feedback Detail View +struct FeedbackDetailView: View { + let feedbackType: FeedbackView.FeedbackType + @State private var feedbackText = "" + @State private var contactInfo = "" + @Environment(\.dismiss) private var dismiss + + var body: some View { + VStack(spacing: 0) { + // Navigation Bar + HStack { + // Back Button + Button(action: { dismiss() }) { + Image(systemName: "chevron.left") + .font(.system(size: 17, weight: .semibold)) + .foregroundColor(.primary) + .frame(width: 44, height: 44) + } + + // Title + Text(feedbackType.rawValue) + .font(.headline) + .frame(maxWidth: .infinity) + + // Spacer to balance the HStack + Spacer() + .frame(width: 44, height: 44) + } + .frame(height: 44) + .background(Color.themeTextWhiteSecondary) + + // Form + ScrollView { + VStack(spacing: 24) { + // Feedback Type + HStack { + Image(systemName: feedbackType.icon) + .foregroundColor(.blue) + Text(feedbackType.rawValue) + .font(.headline) + Spacer() + } + .padding() + .background(Color.blue.opacity(0.1)) + .cornerRadius(12) + + // Feedback Text + VStack(alignment: .leading, spacing: 8) { + Text("Describe your \(feedbackType.rawValue.lowercased())") + .font(.subheadline) + .foregroundColor(.secondary) + + TextEditor(text: $feedbackText) + .frame(minHeight: 150) + .padding() + .background(Color(.systemGray6)) + .cornerRadius(12) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color(.systemGray4), lineWidth: 1) + ) + } + + // Contact Info + VStack(alignment: .leading, spacing: 8) { + Text("Contact Information (Optional)") + .font(.subheadline) + .foregroundColor(.secondary) + + TextField("Email or phone number", text: $contactInfo) + .padding() + .background(Color(.systemGray6)) + .cornerRadius(12) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color(.systemGray4), lineWidth: 1) + ) + } + + Spacer() + } + .padding(.horizontal, 20) + } + + // Submit Button + Button(action: { + submitFeedback() + }) { + Text("Submit Feedback") + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 56) + .background( + RoundedRectangle(cornerRadius: 25) + .fill(Color.themePrimary) + ) + .padding(.horizontal, 24) + .padding(.bottom, 24) + } + } + .navigationBarHidden(true) + } + + private func submitFeedback() { + // TODO: Implement feedback submission logic + print("Feedback submitted:") + print("Type: \(feedbackType.rawValue)") + print("Message: \(feedbackText)") + if !contactInfo.isEmpty { + print("Contact: \(contactInfo)") + } + + // Dismiss back to feedback type selection + dismiss() + } +} + +// Preview +struct FeedbackView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + FeedbackView() + .environmentObject(Router.shared) + } + } +} + +struct FeedbackDetailView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + FeedbackDetailView(feedbackType: .excellent) + } + } +} \ No newline at end of file diff --git a/wake/View/Login/Login.swift b/wake/View/Login/Login.swift index 0a736b3..f2e7e42 100644 --- a/wake/View/Login/Login.swift +++ b/wake/View/Login/Login.swift @@ -71,19 +71,28 @@ struct LoginView: View { // MARK: - Views private func signInButton() -> some View { - AppleSignInButton { request in + print("🟢 [Debug] Sign in button tapped") + return AppleSignInButton { request in + print("🔵 [Debug] Creating sign in request") let nonce = String.randomURLSafeString(length: 32) self.currentNonce = nonce request.nonce = self.sha256(nonce) + request.requestedScopes = [.fullName, .email] + print("🔵 [Debug] Sign in request configured with nonce") } onCompletion: { result in + print("🔵 [Debug] Sign in completion handler triggered") switch result { case .success(let authResults): print("✅ [Apple Sign In] 登录授权成功") if let appleIDCredential = authResults.credential as? ASAuthorizationAppleIDCredential { + print("🔵 [Debug] Processing Apple ID credential") self.processAppleIDCredential(appleIDCredential) + } else { + print("❌ [Debug] Failed to cast credential to ASAuthorizationAppleIDCredential") } case .failure(let error): print("❌ [Apple Sign In] 登录失败: \(error.localizedDescription)") + print("❌ [Debug] Error details: \(error as NSError)") self.handleSignInError(error) } } @@ -248,8 +257,11 @@ struct LoginView: View { private func handleSuccessfulAuthentication() { print("✅ [Auth] 登录成功,准备跳转到用户信息页面...") + print("🔵 [Debug] isLoggedIn before update: \(isLoggedIn)") DispatchQueue.main.async { + print("🔵 [Debug] Setting isLoggedIn to true") self.isLoggedIn = true + print("🔵 [Debug] isLoggedIn after update: \(self.isLoggedIn)") } } diff --git a/wake/View/Owner/UserInfo/UserInfo.swift b/wake/View/Owner/UserInfo/UserInfo.swift index 296e253..a03f7ce 100644 --- a/wake/View/Owner/UserInfo/UserInfo.swift +++ b/wake/View/Owner/UserInfo/UserInfo.swift @@ -2,6 +2,7 @@ import SwiftUI struct UserInfo: View { @Environment(\.dismiss) private var dismiss + @StateObject private var router = Router.shared // Sample user data - replace with your actual data model @State private var userName = "" @@ -141,6 +142,7 @@ struct UserInfo: View { // Continue Button Button(action: { if showUsername { + router.navigate(to: .avatarBox) let parameters: [String: Any] = [ "username": userName, "avatar_file_id": uploadedFileId ?? "" @@ -157,15 +159,12 @@ struct UserInfo: View { // Update local state with the new user info if let userData = response.data { self.userName = userData.username - // You can update other user data here if needed } - // Show success message or navigate back - self.dismiss() + // Navigate using router + router.navigate(to: .avatarBox) case .failure(let error): print("❌ 用户信息更新失败: \(error.localizedDescription)") - // Show error message to user - // You can use an @State variable to show an alert or toast self.errorMessage = "更新失败: \(error.localizedDescription)" self.showError = true } @@ -190,8 +189,6 @@ struct UserInfo: View { } .padding(.horizontal, 32) // 添加上下边距,与上方按钮保持一致 .padding(.bottom, isKeyboardVisible ? 20 : 40) - .disabled(showUsername && userName.trimmingCharacters(in: .whitespaces).isEmpty) - .opacity((showUsername && userName.trimmingCharacters(in: .whitespaces).isEmpty) ? 0.6 : 1.0) .animation(.easeInOut, value: showUsername) .frame(maxWidth: .infinity) diff --git a/wake/View/Upload/uploadView.swift b/wake/View/Upload/uploadView.swift new file mode 100644 index 0000000..e69de29 diff --git a/wake/WakeApp.swift b/wake/WakeApp.swift index baf704e..c7ce01b 100644 --- a/wake/WakeApp.swift +++ b/wake/WakeApp.swift @@ -4,6 +4,7 @@ import SwiftData @main struct WakeApp: App { + @StateObject private var router = Router.shared @StateObject private var authState = AuthState.shared @State private var showSplash = true @@ -31,40 +32,47 @@ struct WakeApp: App { var body: some Scene { WindowGroup { - ZStack { - if showSplash { - // 显示启动页 - SplashView() - .environmentObject(authState) - // .onAppear { - // // 启动页显示时检查token有效性 - // checkTokenValidity() - // } - } else { - // 根据登录状态显示不同视图 - if authState.isAuthenticated { - // 已登录:显示userInfo页面 - UserInfo() + NavigationStack(path: $router.path) { + ZStack { + if showSplash { + // 显示启动页 + SplashView() .environmentObject(authState) + .onAppear { + // 启动页显示时检查token有效性 + checkTokenValidity() + } } else { - // 未登录:显示登录界面 - // ContentView() - // .environmentObject(authState) - UserInfo() - .environmentObject(authState) + // 根据登录状态显示不同视图 + if authState.isAuthenticated { + // 已登录:显示userInfo页面 + UserInfo() + .environmentObject(authState) + } else { + // 未登录:显示登录界面 + // LoginView() + // .environmentObject(authState) + UserInfo() + .environmentObject(authState) + } + } + } + .navigationDestination(for: AppRoute.self) { route in + route.view + } + } + .environmentObject(router) + .environmentObject(authState) + .onAppear { + //2秒后自动隐藏启动页 + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + withAnimation { + showSplash = false } } } - // .onAppear { - // //3秒后自动隐藏启动页 - // DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - // withAnimation { - // showSplash = false - // } - // } - // } + .modelContainer(container) } - .modelContainer(container) } // MARK: - 私有方法 @@ -92,11 +100,11 @@ struct WakeApp: App { authState.isAuthenticated = true } - // 3秒后自动隐藏启动页 - // DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - // withAnimation { - // showSplash = false - // } - // } + // 2秒后自动隐藏启动页 + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + withAnimation { + showSplash = false + } + } } } \ No newline at end of file