diff --git a/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate b/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate index 0f10014..6c4a3be 100644 Binary files a/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate and b/wake.xcodeproj/project.xcworkspace/xcuserdata/elliwood.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/wake/Models/AuthState.swift b/wake/Models/AuthState.swift new file mode 100644 index 0000000..c57917d --- /dev/null +++ b/wake/Models/AuthState.swift @@ -0,0 +1,49 @@ +import SwiftUI +import Combine + +/// 管理用户认证状态的类 +public class AuthState: ObservableObject { + @Published public var isAuthenticated: Bool = false { + didSet { + print("🔔 认证状态变更: \(isAuthenticated ? "已登录" : "已登出")") + } + } + @Published public var isLoading = false + @Published public var errorMessage: String? + @Published public var user: User? + + // 单例模式 + public static let shared = AuthState() + + private init() {} + + /// 登录成功时调用 + public func login(user: User? = nil) { + if let user = user { + self.user = user + } + isAuthenticated = true + errorMessage = nil + } + + /// 登出时调用 + public func logout() { + print("👋 用户登出") + user = nil + isAuthenticated = false + + // 清除用户数据 + TokenManager.shared.clearTokens() + UserDefaults.standard.removeObject(forKey: "lastLoginUser") + } + + /// 更新加载状态 + public func setLoading(_ loading: Bool) { + isLoading = loading + } + + /// 设置错误信息 + public func setError(_ message: String) { + errorMessage = message + } +} diff --git a/wake/Utils/APIConfig.swift b/wake/Utils/APIConfig.swift index 46f51f9..d640459 100644 --- a/wake/Utils/APIConfig.swift +++ b/wake/Utils/APIConfig.swift @@ -7,12 +7,10 @@ public enum APIConfig { <<<<<<< HEAD public static let baseURL = "https://api-dev.memorywake.com:31274/api/v1" - /// 认证 token - 从 Keychain 中获取 + /// 获取认证token public static var authToken: String { let token = KeychainHelper.getAccessToken() ?? "" - if !token.isEmpty { - print("🔑 [APIConfig] 当前访问令牌: \(token.prefix(10))...") // 只打印前10个字符,避免敏感信息完全暴露 - } else { + if token.isEmpty { print("⚠️ [APIConfig] 未找到访问令牌") } return token @@ -42,11 +40,17 @@ public enum APIConfig { >>>>>>> 1814789 (feat: 登录接口联调) /// 认证请求头 public static var authHeaders: [String: String] { - return [ - "Authorization": "Bearer \(authToken)", + let token = authToken + var headers = [ "Content-Type": "application/json", "Accept": "application/json" ] + + if !token.isEmpty { + headers["Authorization"] = "Bearer \(token)" + } + + return headers } <<<<<<< HEAD <<<<<<< HEAD diff --git a/wake/Utils/NetWork.swift b/wake/Utils/NetWork.swift deleted file mode 100644 index 0cdffc9..0000000 --- a/wake/Utils/NetWork.swift +++ /dev/null @@ -1,34 +0,0 @@ -import SwiftUI - -class Network: ObservableObject { - @Published var users: [User] = [] - - func getUsers() { - guard let url = URL(string: "http://192.168.31.156:31646/api/iam/login/password-login") else { fatalError("Missing URL") } - - let urlRequest = URLRequest(url: url) - - let dataTask = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in - if let error = error { - print("Request error: ", error) - return - } - - guard let response = response as? HTTPURLResponse else { return } - - if response.statusCode == 200 { - guard let data = data else { return } - DispatchQueue.main.async { - do { - let decodedUsers = try JSONDecoder().decode([User].self, from: data) - self.users = decodedUsers - } catch let error { - print("Error decoding: ", error) - } - } - } - } - - dataTask.resume() - } -} diff --git a/wake/Utils/NetworkService.swift b/wake/Utils/NetworkService.swift new file mode 100644 index 0000000..ffe53b0 --- /dev/null +++ b/wake/Utils/NetworkService.swift @@ -0,0 +1,207 @@ +import Foundation + +enum NetworkError: Error { + case invalidURL + case noData + case decodingError(Error) + case serverError(String) + case unauthorized + case other(Error) + case networkError(Error) + case unknownError(Error) + + var localizedDescription: String { + switch self { + case .invalidURL: + return "无效的URL" + case .noData: + return "没有收到数据" + case .decodingError(let error): + return "数据解析错误: \(error.localizedDescription)" + case .serverError(let message): + return "服务器错误: \(message)" + case .unauthorized: + return "未授权,请重新登录" + case .other(let error): + return error.localizedDescription + case .networkError(let error): + return "网络请求错误: \(error.localizedDescription)" + case .unknownError(let error): + return "未知错误: \(error.localizedDescription)" + } + } +} + +class NetworkService { + static let shared = NetworkService() + + // 默认请求头 + private let defaultHeaders: [String: String] = [ + "Content-Type": "application/json", + "Accept": "application/json" + ] + + private init() {} + + // MARK: - 基础请求方法 + private func request( + _ method: String, + path: String, + parameters: [String: Any]? = nil, + headers: [String: String]? = nil, + completion: @escaping (Result) -> Void + ) { + // 构建URL + guard let url = URL(string: APIConfig.baseURL + path) else { + completion(.failure(.invalidURL)) + return + } + + // 创建请求 + var request = URLRequest(url: url) + request.httpMethod = method + + // 设置请求头 - 合并默认头、认证头和自定义头 + defaultHeaders.forEach { key, value in + request.setValue(value, forHTTPHeaderField: key) + } + + // 添加认证头 + APIConfig.authHeaders.forEach { key, value in + request.setValue(value, forHTTPHeaderField: key) + } + + // 添加自定义头(如果提供) + headers?.forEach { key, value in + request.setValue(value, forHTTPHeaderField: key) + } + + // 设置请求体(如果是POST/PUT请求) + if let parameters = parameters, (method == "POST" || method == "PUT") { + do { + request.httpBody = try JSONSerialization.data(withJSONObject: parameters) + } catch { + completion(.failure(.other(error))) + return + } + } + + // 打印请求信息(调试用) + print("🌐 [Network] \(method) \(url.absoluteString)") + if let headers = request.allHTTPHeaderFields { + print("📤 Headers: \(headers)") + } + if let body = request.httpBody, let bodyString = String(data: body, encoding: .utf8) { + print("📦 Body: \(bodyString)") + } + + // 创建任务 + let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in + // 处理响应 + self?.handleResponse(data: data, response: response, error: error, completion: completion) + } + + // 开始请求 + task.resume() + } + + private func handleResponse( + data: Data?, + response: URLResponse?, + error: Error?, + completion: @escaping (Result) -> Void + ) { + // 打印响应信息(调试用) + if let httpResponse = response as? HTTPURLResponse { + print("📥 [Network] Status: \(httpResponse.statusCode) \(HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode))") + if let headers = httpResponse.allHeaderFields as? [String: Any] { + print("📥 Headers: \(headers)") + } + + // 检查状态码 + if !(200...299).contains(httpResponse.statusCode) { + print("❌ [Network] 请求失败,状态码: \(httpResponse.statusCode)") + if let data = data, let errorResponse = String(data: data, encoding: .utf8) { + print("❌ [Network] 错误响应: \(errorResponse)") + } + } + } + + // 处理网络错误 + if let error = error { + print("❌ [Network] 网络请求错误: \(error.localizedDescription)") + completion(.failure(.networkError(error))) + return + } + + // 检查数据是否存在 + guard let data = data else { + print("❌ [Network] 没有收到数据") + completion(.failure(.noData)) + return + } + + // 打印响应数据(调试用) + if let responseString = String(data: data, encoding: .utf8) { + print("📥 [Network] 响应数据: \(responseString)") + } + + do { + // 解析JSON数据 + let decoder = JSONDecoder() + let result = try decoder.decode(T.self, from: data) + completion(.success(result)) + } catch let decodingError as DecodingError { + print("❌ [Network] JSON解析失败: \(decodingError.localizedDescription)") + if let dataString = String(data: data, encoding: .utf8) { + print("📋 [Network] 原始响应: \(dataString)") + } + completion(.failure(.decodingError(decodingError))) + } catch { + print("❌ [Network] 未知错误: \(error.localizedDescription)") + completion(.failure(.unknownError(error))) + } + } + + // MARK: - 公共方法 + + /// GET 请求 + func get( + path: String, + parameters: [String: Any]? = nil, + headers: [String: String]? = nil, + completion: @escaping (Result) -> Void + ) { + request("GET", path: path, parameters: parameters, headers: headers, completion: completion) + } + + /// POST 请求 + func post( + path: String, + parameters: [String: Any]? = nil, + headers: [String: String]? = nil, + completion: @escaping (Result) -> Void + ) { + request("POST", path: path, parameters: parameters, headers: headers, completion: completion) + } + + /// DELETE 请求 + func delete( + path: String, + parameters: [String: Any]? = nil, + headers: [String: String]? = nil, + completion: @escaping (Result) -> Void + ) { + request("DELETE", path: path, parameters: parameters, headers: headers, completion: completion) + } + + /// PUT 请求 + func put( + path: String, + parameters: [String: Any]? = nil, + headers: [String: String]? = nil, + completion: @escaping (Result) -> Void + ) { + request("PUT", path: path, parameters: parameters, headers: headers, completion: completion) + } +} diff --git a/wake/Utils/TokenManager.swift b/wake/Utils/TokenManager.swift new file mode 100644 index 0000000..3b11939 --- /dev/null +++ b/wake/Utils/TokenManager.swift @@ -0,0 +1,424 @@ +import Foundation + +/// Token管理器 +/// 负责管理应用的认证令牌,包括验证、刷新和过期处理 +class TokenManager { + /// 单例实例 + static let shared = TokenManager() + + /// token有效期阈值(秒),在token即将过期前进行刷新 + /// 例如:设置为300表示在token过期前5分钟开始刷新 + private let tokenValidityThreshold: TimeInterval = 300 + + /// 私有化初始化方法,确保单例模式 + private init() {} + + // MARK: - Token 状态检查 + + /// 检查是否存在有效的访问令牌 + var hasToken: Bool { + return KeychainHelper.getAccessToken()?.isEmpty == false + } + + // MARK: - Token 验证 + + /// 验证并刷新token(如果需要) + /// - 检查token是否存在 + /// - 检查token是否有效 + /// - 在token即将过期时自动刷新 + /// - Parameter completion: 完成回调,返回验证/刷新结果 + /// - isValid: token是否有效 + /// - error: 错误信息(如果有) + func validateAndRefreshTokenIfNeeded(completion: @escaping (Bool, Error?) -> Void) { + // 1. 检查token是否存在 + guard let token = KeychainHelper.getAccessToken(), !token.isEmpty else { + // token不存在,返回未授权错误 + let error = NSError( + domain: "TokenManager", + code: 401, + userInfo: [NSLocalizedDescriptionKey: "未找到访问令牌"] + ) + completion(false, error) + return + } + + // 2. 检查token是否有效 + if isTokenValid(token) { + // token有效,直接返回成功 + completion(true, nil) + return + } + + // 3. token无效或即将过期,尝试刷新 + refreshToken { [weak self] success, error in + if success { + // 刷新成功,返回成功 + completion(true, nil) + } else { + // 刷新失败,返回错误信息 + let finalError = error ?? NSError( + domain: "TokenManager", + code: 401, + userInfo: [NSLocalizedDescriptionKey: "Token刷新失败"] + ) + completion(false, finalError) + } + } + } + + /// 检查token是否有效 + /// - Parameter token: 要检查的token字符串 + /// - Returns: 如果token有效返回true,否则返回false + /// + /// 该方法会检查token的有效性,包括检查token是否为空、是否过期以及通过网络请求验证token。 + /// + /// - Note: 该方法会打印一些调试信息,包括token验证开始、token过期时间等。 + public func isTokenValid(_ token: String) -> Bool { + print("🔍 TokenManager: 开始验证token...") + + // 1. 基础验证:检查token是否为空 + guard !token.isEmpty else { + print("❌ TokenManager: Token为空") + return false + } + + // 2. 检查token是否过期(如果可能) + if let expiryDate = getTokenExpiryDate(token) { + print("⏰ TokenManager: Token过期时间: \(expiryDate)") + if Date() > expiryDate { + print("❌ TokenManager: Token已过期") + return false + } + } + + // 3. 创建信号量用于同步网络请求 + let semaphore = DispatchSemaphore(value: 0) + var isValid = false + var requestCompleted = false + + print("🌐 TokenManager: 发送验证请求到服务器...") + + // 4. 发送验证请求 + let task = URLSession.shared.dataTask(with: createValidationRequest(token: token)) { data, response, error in + defer { + requestCompleted = true + semaphore.signal() + } + + // 检查网络错误 + if let error = error { + print("❌ TokenManager: 验证请求错误: \(error.localizedDescription)") + return + } + + // 检查响应状态码 + guard let httpResponse = response as? HTTPURLResponse else { + print("❌ TokenManager: 无效的服务器响应") + return + } + + print("📡 TokenManager: 服务器响应状态码: \(httpResponse.statusCode)") + + // 检查状态码 + guard (200...299).contains(httpResponse.statusCode) else { + print("❌ TokenManager: 服务器返回错误状态码: \(httpResponse.statusCode)") + return + } + + // 检查是否有数据 + if let data = data, !data.isEmpty { + do { + // 尝试解析响应数据 + let response = try JSONDecoder().decode(IdentityCheckResponse.self, from: data) + isValid = response.isValid + print("✅ TokenManager: Token验证\(isValid ? "成功" : "失败")") + } catch { + print("❌ TokenManager: 解析响应数据失败: \(error.localizedDescription)") + // 如果解析失败但状态码是200,我们假设token是有效的 + isValid = true + print("ℹ️ TokenManager: 状态码200,假设token有效") + } + } else { + // 如果没有返回数据但状态码是200,我们假设token是有效的 + print("ℹ️ TokenManager: 没有返回数据,但状态码为200,假设token有效") + isValid = true + } + } + + task.resume() + + // 5. 设置超时时间(10秒) + let timeoutResult = semaphore.wait(timeout: .now() + 15) + + // 检查是否超时 + if !requestCompleted && timeoutResult == .timedOut { + print("⚠️ TokenManager: 验证请求超时") + task.cancel() + return false + } + + return isValid + } + + /// 创建验证请求 + private func createValidationRequest(token: String) -> URLRequest { + let url = URL(string: APIConfig.baseURL + "/iam/identity-check")! + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + return request + } + + /// 从token中提取过期时间(示例实现) + private func getTokenExpiryDate(_ token: String) -> Date? { + // 这里需要根据实际的JWT或其他token格式来解析过期时间 + // 以下是JWT token的示例解析 + let parts = token.components(separatedBy: ".") + guard parts.count > 1, let payloadData = base64UrlDecode(parts[1]) else { + return nil + } + + do { + if let payload = try JSONSerialization.jsonObject(with: payloadData) as? [String: Any], + let exp = payload["exp"] as? TimeInterval { + return Date(timeIntervalSince1970: exp) + } + } catch { + print("❌ TokenManager: 解析token过期时间失败: \(error.localizedDescription)") + } + + return nil + } + + private func base64UrlDecode(_ base64Url: String) -> Data? { + var base64 = base64Url + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + + // 添加必要的填充 + let length = Double(base64.lengthOfBytes(using: .utf8)) + let requiredLength = 4 * ceil(length / 4.0) + let paddingLength = requiredLength - length + if paddingLength > 0 { + let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0) + base64 += padding + } + + return Data(base64Encoded: base64) + } + + /// 刷新token + /// - Parameter completion: 刷新完成回调 + /// - success: 是否刷新成功 + /// - error: 错误信息(如果有) + private func refreshToken(completion: @escaping (Bool, Error?) -> Void) { + // 获取刷新令牌 + guard let refreshToken = KeychainHelper.getRefreshToken(), !refreshToken.isEmpty else { + // 没有可用的刷新令牌 + let error = NSError( + domain: "TokenManager", + code: 401, + userInfo: [NSLocalizedDescriptionKey: "未找到刷新令牌"] + ) + completion(false, error) + return + } + + // 准备刷新请求参数 + let parameters: [String: Any] = [ + "refresh_token": refreshToken, + "grant_type": "refresh_token" + ] + + // 发送刷新请求 + NetworkService.shared.postWithToken(path: "/v1/iam/access-token-refresh", parameters: parameters) { + (result: Result) in + + switch result { + case .success(let tokenResponse): + // 1. 保存新的访问令牌 + KeychainHelper.saveAccessToken(tokenResponse.accessToken) + + // 2. 如果返回了新的刷新令牌,也保存起来 + if let newRefreshToken = tokenResponse.refreshToken { + KeychainHelper.saveRefreshToken(newRefreshToken) + } + + print("✅ Token刷新成功") + completion(true, nil) + + case .failure(let error): + print("❌ Token刷新失败: \(error.localizedDescription)") + + // 刷新失败,清除本地token,需要用户重新登录 + KeychainHelper.clearTokens() + + completion(false, error) + } + } + } + + /// 清除所有存储的 token + func clearTokens() { + print("🗑️ TokenManager: 清除所有 token") + KeychainHelper.clearTokens() + // 清除其他与 token 相关的存储 + UserDefaults.standard.removeObject(forKey: "tokenExpiryDate") + UserDefaults.standard.synchronize() + } +} + +// MARK: - Token响应模型 +/// 用于解析token刷新接口的响应数据 +private struct TokenResponse: Codable { + /// 访问令牌 + let accessToken: String + + /// 刷新令牌(可选) + let refreshToken: String? + + /// 过期时间(秒) + let expiresIn: TimeInterval? + + /// 令牌类型(如:Bearer) + let tokenType: String? + + // 使用CodingKeys自定义键名映射 + enum CodingKeys: String, CodingKey { + case accessToken = "access_token" + case refreshToken = "refresh_token" + case expiresIn = "expires_in" + case tokenType = "token_type" + } +} + +// MARK: - 身份验证响应模型 +/// 用于解析身份验证接口的响应数据 +private struct IdentityCheckResponse: Codable { + /// 是否有效 + let isValid: Bool + + /// 用户ID(可选) + let userId: String? + + /// 过期时间(可选) + let expiresAt: Date? + + enum CodingKeys: String, CodingKey { + case isValid = "is_valid" + case userId = "user_id" + case expiresAt = "expires_at" + } +} + +// MARK: - NetworkService扩展 +/// 为NetworkService添加带token验证的请求方法 +extension NetworkService { + // MARK: - 核心请求方法 + + /// 带token验证的网络请求 + /// - Parameters: + /// - method: HTTP方法(GET/POST/PUT/DELETE等) + /// - path: 接口路径 + /// - parameters: 请求参数 + /// - headers: 自定义请求头 + /// - completion: 完成回调 + func requestWithToken( + _ method: String, + path: String, + parameters: [String: Any]? = nil, + headers: [String: String]? = nil, + completion: @escaping (Result) -> Void + ) { + // 1. 验证并刷新token + TokenManager.shared.validateAndRefreshTokenIfNeeded { [weak self] isValid, error in + guard let self = self else { return } + + if isValid { + // 2. token有效,继续发送原始请求 + switch method.uppercased() { + case "GET": + self.get(path: path, parameters: parameters, headers: headers, completion: completion) + case "POST": + self.post(path: path, parameters: parameters, headers: headers, completion: completion) + case "PUT": + self.put(path: path, parameters: parameters, headers: headers, completion: completion) + case "DELETE": + self.delete(path: path, parameters: parameters, headers: headers, completion: completion) + default: + let error = NSError( + domain: "NetworkService", + code: 400, + userInfo: [NSLocalizedDescriptionKey: "不支持的HTTP方法: \(method)"] + ) + completion(.failure(.other(error))) + } + } else { + // 3. token无效,返回未授权错误 + let error = error ?? NSError( + domain: "NetworkService", + code: 401, + userInfo: [NSLocalizedDescriptionKey: "未授权访问"] + ) + + DispatchQueue.main.async { + completion(.failure(.unauthorized)) + } + + // 4. 发送登出通知,让应用处理未授权情况 + NotificationCenter.default.post(name: .userDidLogout, object: nil) + } + } + } + + // MARK: - 便捷方法 + + /// 带token验证的GET请求 + func getWithToken( + path: String, + parameters: [String: Any]? = nil, + headers: [String: String]? = nil, + completion: @escaping (Result) -> Void + ) { + requestWithToken("GET", path: path, parameters: parameters, headers: headers, completion: completion) + } + + /// 带token验证的POST请求 + func postWithToken( + path: String, + parameters: [String: Any]? = nil, + headers: [String: String]? = nil, + completion: @escaping (Result) -> Void + ) { + requestWithToken("POST", path: path, parameters: parameters, headers: headers, completion: completion) + } + + /// 带token验证的PUT请求 + func putWithToken( + path: String, + parameters: [String: Any]? = nil, + headers: [String: String]? = nil, + completion: @escaping (Result) -> Void + ) { + requestWithToken("PUT", path: path, parameters: parameters, headers: headers, completion: completion) + } + + /// 带token验证的DELETE请求 + func deleteWithToken( + path: String, + parameters: [String: Any]? = nil, + headers: [String: String]? = nil, + completion: @escaping (Result) -> Void + ) { + requestWithToken("DELETE", path: path, parameters: parameters, headers: headers, completion: completion) + } +} + +// MARK: - 通知名称 +/// 定义应用中使用的通知名称 +extension Notification.Name { + /// 用户登出通知 + /// 当token失效或用户主动登出时发送 + static let userDidLogout = Notification.Name("UserDidLogoutNotification") +} diff --git a/wake/Utils/User.swift b/wake/Utils/User.swift index 2587619..797be1a 100644 --- a/wake/Utils/User.swift +++ b/wake/Utils/User.swift @@ -1,31 +1,61 @@ import Foundation -struct User: Identifiable, Decodable { - var id: Int - var name: String - var username: String - var email: String - var address: Address - var phone: String - var website: String - var company: Company - - struct Address: Decodable { - var street: String - var suite: String - var city: String - var zipcode: String - var geo: Geo - - struct Geo: Decodable { - var lat: String - var lng: String +public struct User: Identifiable, Decodable { + public var id: Int + public var name: String + public var username: String + public var email: String + public var address: Address + public var phone: String + public var website: String + public var company: Company + + public init(id: Int, name: String, username: String, email: String, address: Address, phone: String, website: String, company: Company) { + self.id = id + self.name = name + self.username = username + self.email = email + self.address = address + self.phone = phone + self.website = website + self.company = company + } + + public struct Address: Decodable { + public var street: String + public var suite: String + public var city: String + public var zipcode: String + public var geo: Geo + + public init(street: String, suite: String, city: String, zipcode: String, geo: Geo) { + self.street = street + self.suite = suite + self.city = city + self.zipcode = zipcode + self.geo = geo + } + + public struct Geo: Decodable { + public var lat: String + public var lng: String + + public init(lat: String, lng: String) { + self.lat = lat + self.lng = lng + } } } - - struct Company: Decodable { - var name: String - var catchPhrase: String - var bs: String + + public struct Company: Decodable { + public var name: String + public var catchPhrase: String + public var bs: String + + public init(name: String, catchPhrase: String, bs: String) { + self.name = name + self.catchPhrase = catchPhrase + self.bs = bs + } } } diff --git a/wake/View/Owner/UserInfo/UserInfo.swift b/wake/View/Owner/UserInfo/UserInfo.swift index 7a49929..38c42b5 100644 --- a/wake/View/Owner/UserInfo/UserInfo.swift +++ b/wake/View/Owner/UserInfo/UserInfo.swift @@ -10,6 +10,7 @@ struct UserInfo: View { @State private var darkModeEnabled = false @State private var showLogoutAlert = false @State private var avatarImage: UIImage? // Add this line + @State private var name: String = "" var body: some View { VStack(spacing: 0) { @@ -39,53 +40,49 @@ struct UserInfo: View { .font(Typography.font(for: .title)) .frame(maxWidth: .infinity, alignment: .center) - // Avatar - ZStack { - // Show either the SVG or the uploaded image - if let avatarImage = avatarImage { - Image(uiImage: avatarImage) - .resizable() - .scaledToFill() - .frame(width: 200, height: 200) - .clipShape(Circle()) - } else { - SVGImage(svgName: "Avatar") - .frame(width: 200, height: 200) - } + // Avatar section + VStack { + Text("your name") + .font(.headline) + .padding(.bottom, 10) - // Make sure the AvatarUploader is on top and covers the entire area - AvatarUploader(selectedImage: $avatarImage, size: 200) - .contentShape(Rectangle()) // This makes the entire area tappable + // Avatar image or placeholder + Circle() + .fill(Color.gray.opacity(0.3)) + .frame(width: 100, height: 100) + .overlay( + Image(systemName: "person.fill") + .resizable() + .scaledToFit() + .foregroundColor(.white) + .padding(30) + ) } - .frame(width: 200, height: 200) - .padding(.vertical, 20) + .padding(.top, 30) - // Buttons - Button(action: { - // Action for first button - }) { - Text("Upload from Gallery") - .frame(maxWidth: .infinity) - .padding() - .foregroundColor(.black) - .background( - RoundedRectangle(cornerRadius: 25) - .fill(Color(red: 1.0, green: 0.973, blue: 0.871)) - ) - } + // Name input field + TextField("Enter your name", text: $name) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .padding(.horizontal, 40) + .padding(.top, 20) + Spacer() + + // Next/Open button Button(action: { - // Action for second button + // Action for open button }) { - Text("Take a Photo") + Text("Open") .frame(maxWidth: .infinity) .padding() .foregroundColor(.black) .background( RoundedRectangle(cornerRadius: 25) - .fill(Color(red: 1.0, green: 0.973, blue: 0.871)) + .fill(Color(red: 1.0, green: 0.714, blue: 0.271)) ) } + .padding(.bottom, 30) + .padding(.horizontal, 20) } .padding() .background(Color(.white)) diff --git a/wake/View/Welcome/SplashView.swift b/wake/View/Welcome/SplashView.swift new file mode 100644 index 0000000..c81fbb6 --- /dev/null +++ b/wake/View/Welcome/SplashView.swift @@ -0,0 +1,92 @@ +import SwiftUI + +struct SplashView: View { + @State private var isAnimating = false + @State private var showLogin = false + @EnvironmentObject private var authState: AuthState + + var body: some View { + NavigationView { + ZStack { + // 背景渐变 + LinearGradient( + gradient: Gradient(colors: [ + Theme.Colors.primary, // Primary color with some transparency + Theme.Colors.primaryDark, // Darker shade of the primary color + ]), + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .edgesIgnoringSafeArea(.all) + VStack(spacing: 50) { + Spacer() + // 欢迎文字动画 + Text("Welcome") + .font(.system(size: 40, weight: .bold, design: .rounded)) + .foregroundColor(.primary) + .scaleEffect(isAnimating ? 1.1 : 0.9) + .opacity(isAnimating ? 1 : 0.3) + .animation( + .easeInOut(duration: 1.5) + .repeatForever(autoreverses: true), + value: isAnimating + ) + + // 动画图标 + Image(systemName: "moon.stars.fill") + .font(.system(size: 120)) + .foregroundColor(.accentColor) + .rotationEffect(.degrees(isAnimating ? 360 : 0)) + .scaleEffect(isAnimating ? 1.2 : 0.8) + .animation( + .easeInOut(duration: 2) + .repeatForever(autoreverses: true), + value: isAnimating + ) + + Spacer() + + // 圆形按钮 + Button(action: { + withAnimation { + showLogin = true + } + }) { + Image(systemName: "arrow.right") + .font(.title) + .foregroundColor(.white) + .frame(width: 140, height: 140) + .background( + Circle() + .fill(Color.accentColor.opacity(0.7)) // 80% opacity + .shadow(radius: 10) + ) + } + .padding(.bottom, 40) + .background( + NavigationLink(destination: LoginView().environmentObject(authState), isActive: $showLogin) { + EmptyView() + } + .hidden() + ) + Spacer() + } + .padding() + } + .onAppear { + isAnimating = true + } + } + .navigationViewStyle(StackNavigationViewStyle()) + } +} + +// 预览 +#if DEBUG +struct SplashView_Previews: PreviewProvider { + static var previews: some View { + SplashView() + .environmentObject(AuthState.shared) + } +} +#endif \ No newline at end of file diff --git a/wake/WakeApp.swift b/wake/WakeApp.swift index 49addd2..ec8fe09 100644 --- a/wake/WakeApp.swift +++ b/wake/WakeApp.swift @@ -1,20 +1,12 @@ - import SwiftUI import UIKit import SwiftData @main struct WakeApp: App { - // init() { - // // 打印所有可用的字体 - // print("\n=== 所有可用的字体 ===") - // for family in UIFont.familyNames.sorted() { - // print("\n\(family):") - // for name in UIFont.fontNames(forFamilyName: family).sorted() { - // print(" - \(name)") - // } - // } - // } + @StateObject private var authState = AuthState.shared + @State private var showSplash = true + // 使用更简单的方式创建模型容器 let container: ModelContainer @@ -32,26 +24,77 @@ struct WakeApp: App { // 3. 重新创建容器 container = try! ModelContainer(for: Login.self) } + + // 配置网络层 + configureNetwork() } - var body: some Scene { WindowGroup { - ContentView() -// SettingsView() - // 导航栏按钮 - // TabView{ - // ContentView() - // .tabItem{ - // Label("wake", systemImage: "book") - // } - // SettingView() - // .tabItem{ - // Label("setting", systemImage: "gear") - // } - // } + ZStack { + if showSplash { + // 显示启动页 + SplashView() + .environmentObject(authState) + .onAppear { + // 启动页显示时检查token有效性 + checkTokenValidity() + } + } else { + // 根据登录状态显示不同视图 + if authState.isAuthenticated { + // 已登录:显示主界面 + ContentView() + .environmentObject(authState) + } else { + // 未登录:显示登录界面 + ContentView() + .environmentObject(authState) + } + } + } + .onAppear { + // 3秒后自动隐藏启动页 + // DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + // withAnimation { + // showSplash = false + // } + // } + } } - // 注入模型容器到环境中 .modelContainer(container) } -} + + // MARK: - 私有方法 + + /// 配置网络层 + private func configureNetwork() { + // 配置网络请求超时时间等 + let configuration = URLSessionConfiguration.default + configuration.timeoutIntervalForRequest = 30 + configuration.timeoutIntervalForResource = 60 + + // 可以在这里添加其他网络配置 + } + + /// 检查token有效性 + private func checkTokenValidity() { + guard TokenManager.shared.hasToken, + let token = KeychainHelper.getAccessToken() else { + showSplash = false + return + } + + // 检查token是否有效 + if TokenManager.shared.isTokenValid(token) { + authState.isAuthenticated = true + } + + // 3秒后自动隐藏启动页 + // DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + // withAnimation { + // showSplash = false + // } + // } + } +} \ No newline at end of file