feat: feedback

This commit is contained in:
jinyaqiu 2025-08-24 15:36:50 +08:00
parent 0aa1271c93
commit 4e97f8ebb8
7 changed files with 478 additions and 42 deletions

41
wake/Utils/Router.swift Normal file
View File

@ -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()
}
}

View File

@ -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())
}
}

273
wake/View/Feedback.swift Normal file
View File

@ -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)
}
}
}

View File

@ -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)")
}
}

View File

@ -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)

View File

View File

@ -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
}
}
}
}