364 lines
12 KiB
Swift
364 lines
12 KiB
Swift
//
|
||
// ResetPasswordView.swift
|
||
// punchnet
|
||
//
|
||
// Created by 安礼成 on 2026/3/9.
|
||
//
|
||
import SwiftUI
|
||
import Observation
|
||
|
||
// MARK: - 注册根视图
|
||
struct RegisterRootView: View {
|
||
@State private var registerModel = RegisterModel()
|
||
|
||
var body: some View {
|
||
ZStack {
|
||
// 背景毛玻璃
|
||
VisualEffectView(material: .underWindowBackground, blendingMode: .behindWindow)
|
||
.ignoresSafeArea()
|
||
|
||
Group {
|
||
switch registerModel.stage {
|
||
case .requestVerifyCode(let username):
|
||
RegisterRequestVerifyCodeView(username: username ?? "")
|
||
case .submitVerifyCode(let username):
|
||
RegisterSubmitVerifyCodeView(username: username)
|
||
case .setPassword(let username):
|
||
RegisterSetPasswordView(username: username)
|
||
}
|
||
}
|
||
.transition(.asymmetric(
|
||
insertion: .move(edge: registerModel.transitionEdge).combined(with: .opacity),
|
||
removal: .move(edge: registerModel.transitionEdge == .trailing ? .leading : .trailing).combined(with: .opacity)
|
||
))
|
||
}
|
||
.environment(registerModel)
|
||
.frame(width: 400, height: 450)
|
||
}
|
||
}
|
||
|
||
// MARK: - 封装的输入框组件
|
||
struct PunchTextField: View {
|
||
let icon: String
|
||
let placeholder: String
|
||
@Binding var text: String
|
||
var isSecure: Bool = false
|
||
var isDisabled: Bool = false
|
||
|
||
var body: some View {
|
||
HStack(spacing: 12) {
|
||
Image(systemName: icon)
|
||
.foregroundColor(.secondary)
|
||
.frame(width: 20)
|
||
|
||
if isSecure {
|
||
SecureField(placeholder, text: $text)
|
||
.textFieldStyle(.plain)
|
||
} else {
|
||
TextField(placeholder, text: $text)
|
||
.textFieldStyle(.plain)
|
||
.disabled(isDisabled)
|
||
}
|
||
}
|
||
.padding(10)
|
||
.background(Color.primary.opacity(isDisabled ? 0.02 : 0.05))
|
||
.cornerRadius(8)
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: 8)
|
||
.stroke(Color.primary.opacity(0.1), lineWidth: 1)
|
||
)
|
||
}
|
||
}
|
||
|
||
// MARK: - 第一步:获取验证码
|
||
struct RegisterRequestVerifyCodeView: View {
|
||
@Environment(RegisterModel.self) var registerModel
|
||
@State var username: String = ""
|
||
@State private var isProcessing = false
|
||
|
||
// 错误提示
|
||
@State private var showAlert: Bool = false
|
||
@State private var errorMessage: String = ""
|
||
|
||
var body: some View {
|
||
VStack(spacing: 24) {
|
||
headerSection(title: "创建个人网络", subtitle: "输入手机号或邮箱开始注册")
|
||
|
||
VStack(spacing: 16) {
|
||
PunchTextField(icon: "person.crop.circle", placeholder: "手机号 / 邮箱", text: $username)
|
||
}
|
||
.frame(width: 280)
|
||
|
||
Button(action: {
|
||
self.requestVerifyCode()
|
||
}) {
|
||
Text("获取验证码")
|
||
.fontWeight(.medium)
|
||
.frame(maxWidth: .infinity)
|
||
}
|
||
.buttonStyle(.borderedProminent)
|
||
.controlSize(.large)
|
||
.frame(width: 280)
|
||
.disabled(!SDLUtil.isValidIdentifyContact(username) || isProcessing)
|
||
|
||
Spacer()
|
||
}
|
||
.padding(40)
|
||
.alert(isPresented: $showAlert) {
|
||
Alert(title: Text("提示"), message: Text(self.errorMessage))
|
||
}
|
||
}
|
||
|
||
private func requestVerifyCode() {
|
||
self.isProcessing = true
|
||
Task { @MainActor in
|
||
if username.isEmpty {
|
||
self.showAlert = true
|
||
self.errorMessage = "手机号/邮箱为空"
|
||
self.isProcessing = false
|
||
return
|
||
}
|
||
|
||
switch SDLUtil.identifyContact(username) {
|
||
case .email, .phone:
|
||
do {
|
||
_ = try await self.registerModel.requestVerifyCode(username: username)
|
||
withAnimation(.spring(duration: 0.6, bounce: 0.2)) {
|
||
self.registerModel.stage = .submitVerifyCode(username: username)
|
||
self.registerModel.transitionEdge = .trailing
|
||
}
|
||
} catch {
|
||
self.showAlert = true
|
||
self.errorMessage = error.localizedDescription
|
||
}
|
||
case .invalid:
|
||
self.showAlert = true
|
||
self.errorMessage = "手机号/邮箱格式错误"
|
||
}
|
||
self.isProcessing = false
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// MARK: - 第二步:验证
|
||
struct RegisterSubmitVerifyCodeView: View {
|
||
@Environment(RegisterModel.self) var registerModel
|
||
let username: String
|
||
@State private var code: String = ""
|
||
@State private var isProcessing = false
|
||
|
||
// 错误提示
|
||
@State private var showAlert: Bool = false
|
||
@State private var errorMessage: String = ""
|
||
|
||
// 重新发送是否可以使用
|
||
@State private var isEnabled: Bool = false
|
||
@State private var remainingSeconds = 60
|
||
@State private var timer: Timer? = nil
|
||
|
||
// 判断验证码是否正确
|
||
var validInputCode: Bool {
|
||
return !self.code.isEmpty && self.code.count == 4 && self.code.allSatisfy {$0.isNumber}
|
||
}
|
||
|
||
var body: some View {
|
||
VStack(spacing: 24) {
|
||
headerSection(title: "身份验证", subtitle: "验证码已发送至 \(username)")
|
||
|
||
VStack(alignment: .trailing, spacing: 16) {
|
||
PunchTextField(icon: "envelope.badge", placeholder: "输入 4 位验证码", text: $code)
|
||
|
||
Button {
|
||
self.resendVerifyCodeAction()
|
||
} label: {
|
||
if isEnabled {
|
||
Text("没有收到?重新获取")
|
||
} else {
|
||
Text("重新获取 (\(remainingSeconds)s)")
|
||
}
|
||
}
|
||
.buttonStyle(.link)
|
||
.font(.caption)
|
||
.disabled(!isEnabled) // 倒计时期间禁用按钮
|
||
}
|
||
.frame(width: 280)
|
||
|
||
VStack(spacing: 12) {
|
||
Button(action: {
|
||
self.submitVerifyCode()
|
||
}) {
|
||
Text("验证并设置密码")
|
||
.fontWeight(.medium)
|
||
.frame(maxWidth: .infinity)
|
||
}
|
||
.buttonStyle(.borderedProminent)
|
||
.controlSize(.large)
|
||
.disabled(!self.validInputCode)
|
||
|
||
Button("返回上一步") {
|
||
withAnimation(.spring(duration: 0.6, bounce: 0.2)) {
|
||
self.registerModel.stage = .requestVerifyCode(username: self.username)
|
||
self.registerModel.transitionEdge = .leading
|
||
}
|
||
}
|
||
.buttonStyle(.plain)
|
||
.foregroundColor(.secondary)
|
||
}
|
||
.frame(width: 280)
|
||
|
||
Spacer()
|
||
}
|
||
.padding(40)
|
||
.alert(isPresented: $showAlert) {
|
||
Alert(title: Text("提示"), message: Text(errorMessage))
|
||
}
|
||
.task {
|
||
await self.startCountdown()
|
||
}
|
||
}
|
||
|
||
// 重新发送验证码
|
||
private func resendVerifyCodeAction() {
|
||
Task {
|
||
do {
|
||
let result = try await self.registerModel.requestVerifyCode(username: username)
|
||
print("send verify code result: \(result)")
|
||
} catch let err {
|
||
print("resend verify get error: \(err)")
|
||
}
|
||
// 重新计时
|
||
await self.startCountdown()
|
||
}
|
||
}
|
||
|
||
// 重新倒计时
|
||
private func startCountdown() async {
|
||
self.isEnabled = false
|
||
self.remainingSeconds = 60
|
||
for sec in (1...self.remainingSeconds).reversed() {
|
||
self.remainingSeconds = sec
|
||
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
||
}
|
||
self.isEnabled = true
|
||
}
|
||
|
||
// 提交验证码
|
||
private func submitVerifyCode() {
|
||
self.isProcessing = true
|
||
Task { @MainActor in
|
||
do {
|
||
_ = try await self.registerModel.submitVerifyCode(username: username, verifyCode: self.code)
|
||
withAnimation(.spring(duration: 0.6, bounce: 0.2)) {
|
||
self.registerModel.stage = .setPassword(username: username)
|
||
self.registerModel.transitionEdge = .trailing
|
||
}
|
||
} catch {
|
||
self.showAlert = true
|
||
self.errorMessage = error.localizedDescription
|
||
}
|
||
self.isProcessing = false
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 第三步:设置密码
|
||
struct RegisterSetPasswordView: View {
|
||
@Environment(RegisterModel.self) var registerModel
|
||
let username: String
|
||
@State private var password = ""
|
||
@State private var confirm = ""
|
||
@State private var isProcessing = false
|
||
|
||
// 错误提示
|
||
@State private var errorMessage: String?
|
||
|
||
// 提示错误信息
|
||
var passwordError: String? {
|
||
if password.isEmpty || confirm.isEmpty {
|
||
return nil
|
||
}
|
||
|
||
if password != confirm {
|
||
return "两次输入的密码不一致"
|
||
}
|
||
if password.count < 8 {
|
||
return "密码至少需要 8 位"
|
||
}
|
||
return nil
|
||
}
|
||
|
||
var body: some View {
|
||
VStack(spacing: 24) {
|
||
headerSection(title: "设置安全密码", subtitle: "最后一步,请确保密码足够强大")
|
||
|
||
VStack(spacing: 12) {
|
||
PunchTextField(icon: "lock.shield", placeholder: "新密码", text: $password, isSecure: true)
|
||
PunchTextField(icon: "lock.shield", placeholder: "确认密码", text: $confirm, isSecure: true)
|
||
if let error = passwordError {
|
||
Text(error)
|
||
.font(.caption)
|
||
.foregroundColor(.red)
|
||
.frame(width: 280, alignment: .leading)
|
||
}
|
||
}
|
||
.frame(width: 280)
|
||
|
||
Button(action: {
|
||
self.handleRegister()
|
||
}) {
|
||
if isProcessing {
|
||
ProgressView()
|
||
.controlSize(.small)
|
||
} else {
|
||
Text("完成注册")
|
||
.fontWeight(.medium)
|
||
.frame(maxWidth: .infinity)
|
||
}
|
||
}
|
||
.buttonStyle(.borderedProminent)
|
||
.controlSize(.large)
|
||
.frame(width: 280)
|
||
.disabled(passwordError != nil)
|
||
|
||
Spacer()
|
||
}
|
||
.padding(40)
|
||
}
|
||
|
||
private func handleRegister() {
|
||
self.isProcessing = true
|
||
Task { @MainActor in
|
||
do {
|
||
let result = try await self.registerModel.register(username: username, password: self.password)
|
||
print("send verify code result: \(result)")
|
||
} catch {
|
||
self.errorMessage = error.localizedDescription
|
||
}
|
||
self.isProcessing = false
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// MARK: - 辅助视图
|
||
extension View {
|
||
|
||
func headerSection(title: String, subtitle: String) -> some View {
|
||
VStack(spacing: 8) {
|
||
Image(systemName: "shield.lefthalf.filled")
|
||
.font(.system(size: 42))
|
||
.foregroundStyle(.blue.gradient)
|
||
|
||
Text(title)
|
||
.font(.title2.bold())
|
||
|
||
Text(subtitle)
|
||
.font(.subheadline)
|
||
.foregroundColor(.secondary)
|
||
.multilineTextAlignment(.center)
|
||
}
|
||
}
|
||
|
||
}
|