feat: feedback
This commit is contained in:
parent
0aa1271c93
commit
4e97f8ebb8
41
wake/Utils/Router.swift
Normal file
41
wake/Utils/Router.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
105
wake/View/Blind/AvatarBox.swift
Normal file
105
wake/View/Blind/AvatarBox.swift
Normal 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
273
wake/View/Feedback.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
0
wake/View/Upload/uploadView.swift
Normal file
0
wake/View/Upload/uploadView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user