feat: 登录页面
This commit is contained in:
parent
d27f665009
commit
5df804d115
@ -7,7 +7,7 @@
|
|||||||
<key>wake.xcscheme_^#shared#^_</key>
|
<key>wake.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>3</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
BIN
wake/.DS_Store
vendored
BIN
wake/.DS_Store
vendored
Binary file not shown.
@ -70,7 +70,9 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 登录按钮
|
// 登录按钮
|
||||||
NavigationLink(destination: LoginView()) {
|
Button(action: {
|
||||||
|
showLogin = true
|
||||||
|
}) {
|
||||||
Text("登录")
|
Text("登录")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
@ -80,6 +82,9 @@ struct ContentView: View {
|
|||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
}
|
}
|
||||||
.padding(.trailing)
|
.padding(.trailing)
|
||||||
|
.fullScreenCover(isPresented: $showLogin) {
|
||||||
|
LoginView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|||||||
@ -22,8 +22,8 @@
|
|||||||
<string>Sign in with Apple is used to authenticate your account</string>
|
<string>Sign in with Apple is used to authenticate your account</string>
|
||||||
<key>UIAppFonts</key>
|
<key>UIAppFonts</key>
|
||||||
<array>
|
<array>
|
||||||
<string>Quicksand x.ttf</string>
|
<string>Inter.ttf</string>
|
||||||
<string>SankeiCutePopanime.ttf</string>
|
<string>Quicksand X.ttf</string>
|
||||||
<string>Quicksand-Regular.ttf</string>
|
<string>Quicksand-Regular.ttf</string>
|
||||||
<string>Quicksand-Bold.ttf</string>
|
<string>Quicksand-Bold.ttf</string>
|
||||||
<string>Quicksand-SemiBold.ttf</string>
|
<string>Quicksand-SemiBold.ttf</string>
|
||||||
|
|||||||
BIN
wake/Resources/.DS_Store
vendored
Normal file
BIN
wake/Resources/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
wake/Resources/Fonts/Inter.ttf
Normal file
BIN
wake/Resources/Fonts/Inter.ttf
Normal file
Binary file not shown.
Binary file not shown.
@ -32,6 +32,8 @@ struct Theme {
|
|||||||
static let textSecondary = Color(hex: "6B7280") // 次级文本色
|
static let textSecondary = Color(hex: "6B7280") // 次级文本色
|
||||||
static let textTertiary = Color(hex: "9CA3AF") // 三级文本色
|
static let textTertiary = Color(hex: "9CA3AF") // 三级文本色
|
||||||
static let textInverse = Color.white // 反色文本
|
static let textInverse = Color.white // 反色文本
|
||||||
|
static let textMessage = Color(hex: "7B7B7B") // 注释颜色
|
||||||
|
static let textMessageMain = Color(hex: "000000") // 注释主要颜色
|
||||||
|
|
||||||
// MARK: - 状态色
|
// MARK: - 状态色
|
||||||
static let success = Color(hex: "10B981") // 成功色
|
static let success = Color(hex: "10B981") // 成功色
|
||||||
@ -115,6 +117,8 @@ extension Color {
|
|||||||
static var themeSurface: Color { Theme.Colors.surface }
|
static var themeSurface: Color { Theme.Colors.surface }
|
||||||
static var themeTextPrimary: Color { Theme.Colors.textPrimary }
|
static var themeTextPrimary: Color { Theme.Colors.textPrimary }
|
||||||
static var themeTextSecondary: Color { Theme.Colors.textSecondary }
|
static var themeTextSecondary: Color { Theme.Colors.textSecondary }
|
||||||
|
static var themeTextMessage: Color { Theme.Colors.textMessage }
|
||||||
|
static var themeTextMessageMain: Color { Theme.Colors.textMessageMain }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 预览
|
// MARK: - 预览
|
||||||
|
|||||||
@ -3,12 +3,10 @@ import SwiftUI
|
|||||||
// MARK: - 字体库枚举
|
// MARK: - 字体库枚举
|
||||||
/// 定义应用中可用的字体库
|
/// 定义应用中可用的字体库
|
||||||
enum FontFamily: String, CaseIterable {
|
enum FontFamily: String, CaseIterable {
|
||||||
case sankeiCute = "SankeiCutePopanime" // 可爱风格字体
|
case quicksand = "Quicksand x"
|
||||||
case quicksand = "Quicksand x" // 主题字体
|
|
||||||
case quicksandBold = "Quicksand-Bold"
|
case quicksandBold = "Quicksand-Bold"
|
||||||
case quicksandRegular = "Quicksand-Regular"
|
case quicksandRegular = "Quicksand-Regular"
|
||||||
// 后续添加新字体库时在这里添加新 case
|
case inter = "Inter"
|
||||||
// 例如: case anotherFont = "AnotherFontName"
|
|
||||||
|
|
||||||
/// 获取字体名称
|
/// 获取字体名称
|
||||||
var name: String {
|
var name: String {
|
||||||
@ -19,6 +17,7 @@ enum FontFamily: String, CaseIterable {
|
|||||||
// MARK: - 文本样式枚举
|
// MARK: - 文本样式枚举
|
||||||
/// 定义应用中使用的文本样式类型
|
/// 定义应用中使用的文本样式类型
|
||||||
enum TypographyStyle {
|
enum TypographyStyle {
|
||||||
|
case largeTitle // 大标题
|
||||||
case headline // 大标题
|
case headline // 大标题
|
||||||
case title // 标题
|
case title // 标题
|
||||||
case body // 正文
|
case body // 正文
|
||||||
@ -44,11 +43,12 @@ struct Typography {
|
|||||||
|
|
||||||
/// 文本样式配置表
|
/// 文本样式配置表
|
||||||
private static let styleConfig: [TypographyStyle: TypographyConfig] = [
|
private static let styleConfig: [TypographyStyle: TypographyConfig] = [
|
||||||
|
.largeTitle: TypographyConfig(size: 32, weight: .heavy, textStyle: .largeTitle),
|
||||||
.headline: TypographyConfig(size: 24, weight: .bold, textStyle: .headline),
|
.headline: TypographyConfig(size: 24, weight: .bold, textStyle: .headline),
|
||||||
.title: TypographyConfig(size: 20, weight: .semibold, textStyle: .title2),
|
.title: TypographyConfig(size: 20, weight: .semibold, textStyle: .title2),
|
||||||
.body: TypographyConfig(size: 16, weight: .regular, textStyle: .body),
|
.body: TypographyConfig(size: 16, weight: .regular, textStyle: .body),
|
||||||
.subtitle: TypographyConfig(size: 14, weight: .medium, textStyle: .subheadline),
|
.subtitle: TypographyConfig(size: 14, weight: .medium, textStyle: .subheadline),
|
||||||
.caption: TypographyConfig(size: 12, weight: .light, textStyle: .caption1),
|
.caption: TypographyConfig(size: 12, weight: .regular, textStyle: .caption1),
|
||||||
.footnote: TypographyConfig(size: 11, weight: .regular, textStyle: .footnote),
|
.footnote: TypographyConfig(size: 11, weight: .regular, textStyle: .footnote),
|
||||||
.small: TypographyConfig(size: 10, weight: .regular, textStyle: .headline)
|
.small: TypographyConfig(size: 10, weight: .regular, textStyle: .headline)
|
||||||
]
|
]
|
||||||
|
|||||||
95
wake/View/Components/AppleSignInButton.swift
Normal file
95
wake/View/Components/AppleSignInButton.swift
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import AuthenticationServices
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
|
/// 自定义的 Apple 登录按钮组件
|
||||||
|
struct AppleSignInButton: View {
|
||||||
|
// MARK: - 属性
|
||||||
|
|
||||||
|
/// 授权请求回调
|
||||||
|
let onRequest: (ASAuthorizationAppleIDRequest) -> Void
|
||||||
|
|
||||||
|
/// 授权完成回调
|
||||||
|
let onCompletion: (Result<ASAuthorization, Error>) -> Void
|
||||||
|
|
||||||
|
/// 按钮文字
|
||||||
|
let buttonText: String
|
||||||
|
|
||||||
|
// MARK: - 初始化方法
|
||||||
|
|
||||||
|
init(buttonText: String = "Continue with Apple",
|
||||||
|
onRequest: @escaping (ASAuthorizationAppleIDRequest) -> Void,
|
||||||
|
onCompletion: @escaping (Result<ASAuthorization, Error>) -> Void) {
|
||||||
|
self.buttonText = buttonText
|
||||||
|
self.onRequest = onRequest
|
||||||
|
self.onCompletion = onCompletion
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 视图主体
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: handleSignIn) {
|
||||||
|
HStack(alignment: .center, spacing: 8) {
|
||||||
|
Image(systemName: "applelogo")
|
||||||
|
.font(.system(size: 20, weight: .regular))
|
||||||
|
Text(buttonText)
|
||||||
|
.font(.system(size: 18, weight: .regular))
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(height: 60)
|
||||||
|
.background(Color.white)
|
||||||
|
.foregroundColor(.black)
|
||||||
|
.cornerRadius(30)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 30)
|
||||||
|
.stroke(Color.black, lineWidth: 1) // 使用黑色边框
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 私有方法
|
||||||
|
|
||||||
|
private func handleSignIn() {
|
||||||
|
let provider = ASAuthorizationAppleIDProvider()
|
||||||
|
let request = provider.createRequest()
|
||||||
|
request.requestedScopes = [.fullName, .email]
|
||||||
|
|
||||||
|
// 创建 nonce 用于安全验证
|
||||||
|
let nonce = String.randomURLSafeString(length: 32)
|
||||||
|
request.nonce = sha256(nonce)
|
||||||
|
|
||||||
|
// 调用请求回调
|
||||||
|
onRequest(request)
|
||||||
|
|
||||||
|
// 创建并显示授权控制器
|
||||||
|
let controller = ASAuthorizationController(authorizationRequests: [request])
|
||||||
|
controller.delegate = Coordinator(onCompletion: onCompletion)
|
||||||
|
controller.performRequests()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sha256(_ input: String) -> String {
|
||||||
|
let inputData = Data(input.utf8)
|
||||||
|
let hashedData = SHA256.hash(data: inputData)
|
||||||
|
return hashedData.compactMap { String(format: "%02x", $0) }.joined()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 协调器
|
||||||
|
|
||||||
|
private class Coordinator: NSObject, ASAuthorizationControllerDelegate {
|
||||||
|
let onCompletion: (Result<ASAuthorization, Error>) -> Void
|
||||||
|
|
||||||
|
init(onCompletion: @escaping (Result<ASAuthorization, Error>) -> Void) {
|
||||||
|
self.onCompletion = onCompletion
|
||||||
|
}
|
||||||
|
|
||||||
|
// 授权成功回调
|
||||||
|
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
|
||||||
|
onCompletion(.success(authorization))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 授权失败回调
|
||||||
|
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
|
||||||
|
onCompletion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,27 +16,24 @@ struct LoginView: View {
|
|||||||
// MARK: - Body
|
// MARK: - Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
|
||||||
ZStack {
|
ZStack {
|
||||||
// Background
|
// Background
|
||||||
Color(red: 1.0, green: 0.67, blue: 0.15)
|
Color(red: 1.0, green: 0.67, blue: 0.15)
|
||||||
.edgesIgnoringSafeArea(.all)
|
.ignoresSafeArea()
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("Hi, I'm MeMo!")
|
Text("Hi, I'm MeMo!")
|
||||||
.font(.largeTitle)
|
.font(Typography.font(for: .largeTitle))
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(.black)
|
.foregroundColor(.black)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(.leading, 24)
|
.padding(.horizontal, 24)
|
||||||
.padding(.top, 44)
|
.padding(.top, 44)
|
||||||
|
|
||||||
Text("Welcome~")
|
Text("Welcome~")
|
||||||
.font(.largeTitle)
|
.font(Typography.font(for: .largeTitle))
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(.black)
|
.foregroundColor(.black)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(.leading, 24)
|
.padding(.horizontal, 24)
|
||||||
.padding(.bottom, 20)
|
.padding(.bottom, 20)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@ -46,9 +43,10 @@ struct LoginView: View {
|
|||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
Spacer()
|
Spacer()
|
||||||
signInButton()
|
signInButton()
|
||||||
|
.padding(.horizontal, 24)
|
||||||
termsAndPrivacyView()
|
termsAndPrivacyView()
|
||||||
}
|
}
|
||||||
.padding()
|
.frame(maxWidth: .infinity)
|
||||||
.alert(isPresented: $showError) {
|
.alert(isPresented: $showError) {
|
||||||
Alert(
|
Alert(
|
||||||
title: Text("Error"),
|
title: Text("Error"),
|
||||||
@ -61,6 +59,7 @@ struct LoginView: View {
|
|||||||
loadingView()
|
loadingView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.navigationBarBackButtonHidden(true)
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
.fullScreenCover(isPresented: $isLoggedIn) {
|
.fullScreenCover(isPresented: $isLoggedIn) {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
@ -68,27 +67,26 @@ struct LoginView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Views
|
// MARK: - Views
|
||||||
|
|
||||||
private func signInButton() -> some View {
|
private func signInButton() -> some View {
|
||||||
SignInWithAppleButton(
|
AppleSignInButton { request in
|
||||||
onRequest: { request in
|
|
||||||
let nonce = String.randomURLSafeString(length: 32)
|
let nonce = String.randomURLSafeString(length: 32)
|
||||||
self.currentNonce = nonce
|
self.currentNonce = nonce
|
||||||
request.requestedScopes = [.fullName, .email]
|
|
||||||
request.nonce = self.sha256(nonce)
|
request.nonce = self.sha256(nonce)
|
||||||
},
|
} onCompletion: { result in
|
||||||
onCompletion: handleAppleSignIn
|
switch result {
|
||||||
)
|
case .success(let authResults):
|
||||||
.signInWithAppleButtonStyle(.white)
|
print("✅ [Apple Sign In] 登录授权成功")
|
||||||
.frame(height: 50)
|
if let appleIDCredential = authResults.credential as? ASAuthorizationAppleIDCredential {
|
||||||
.cornerRadius(25)
|
self.processAppleIDCredential(appleIDCredential)
|
||||||
.overlay(
|
}
|
||||||
RoundedRectangle(cornerRadius: 25)
|
case .failure(let error):
|
||||||
.stroke(Color.black, lineWidth: 1)
|
print("❌ [Apple Sign In] 登录失败: \(error.localizedDescription)")
|
||||||
)
|
self.handleSignInError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func termsAndPrivacyView() -> some View {
|
private func termsAndPrivacyView() -> some View {
|
||||||
@ -96,13 +94,13 @@ struct LoginView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Text("By continuing, you agree to our")
|
Text("By continuing, you agree to our")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.themeTextMessage)
|
||||||
|
|
||||||
Button("Terms of") {
|
Button("Terms of") {
|
||||||
openURL("https://yourwebsite.com/terms")
|
openURL("https://yourwebsite.com/terms")
|
||||||
}
|
}
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundColor(.blue)
|
.foregroundColor(.themeTextMessageMain)
|
||||||
}
|
}
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.horizontal, 24)
|
.padding(.horizontal, 24)
|
||||||
@ -112,24 +110,24 @@ struct LoginView: View {
|
|||||||
openURL("https://yourwebsite.com/terms")
|
openURL("https://yourwebsite.com/terms")
|
||||||
}
|
}
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundColor(.blue)
|
.foregroundColor(.themeTextMessageMain)
|
||||||
|
|
||||||
Text("and")
|
Text("and")
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.themeTextMessage)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
|
||||||
Button("Privacy Policy") {
|
Button("Privacy Policy") {
|
||||||
openURL("https://yourwebsite.com/privacy")
|
openURL("https://yourwebsite.com/privacy")
|
||||||
}
|
}
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundColor(.blue)
|
.foregroundColor(.themeTextMessageMain)
|
||||||
}
|
}
|
||||||
.padding(.top, 4)
|
.padding(.top, 4)
|
||||||
}
|
}
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.horizontal, 24)
|
.padding(.horizontal, 24)
|
||||||
.padding(.bottom, 24)
|
.padding(.vertical, 12)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadingView() -> some View {
|
private func loadingView() -> some View {
|
||||||
|
|||||||
@ -44,9 +44,7 @@ struct WakeApp: App {
|
|||||||
// 根据登录状态显示不同视图
|
// 根据登录状态显示不同视图
|
||||||
if authState.isAuthenticated {
|
if authState.isAuthenticated {
|
||||||
// 已登录:显示userInfo页面
|
// 已登录:显示userInfo页面
|
||||||
// UserInfo()
|
UserInfo()
|
||||||
// .environmentObject(authState)
|
|
||||||
MediaUploadDemo()
|
|
||||||
.environmentObject(authState)
|
.environmentObject(authState)
|
||||||
} else {
|
} else {
|
||||||
// 未登录:显示登录界面
|
// 未登录:显示登录界面
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user