feat: 重构登录和注册流程,添加邮箱验证和验证码输入组件,优化样式

This commit is contained in:
2025-12-12 14:44:30 +07:00
parent 5bd063f982
commit 2d82ef6e35
10 changed files with 189 additions and 58 deletions

View File

@@ -1,51 +0,0 @@
<script lang='ts' setup>
import { modalController } from "@ionic/vue";
import { logoGoogle } from "ionicons/icons";
async function closeModal() {
await modalController.dismiss();
}
</script>
<template>
<IonPage>
<IonHeader class="ion-no-border">
<IonToolbar>
<IonButtons slot="start">
<IonButton @click="closeModal">
Close
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent :fullscreen="true" class="ion-padding">
<h1 class="title">
<strong>Log in</strong>
</h1>
<ui-input-label
label="Email"
type="email"
placeholder="Enter your email"
clear-input
/>
<ion-button expand="block" class="ion-margin-top" shape="round">
Next
</ion-button>
<ui-divider text="Or continue with" />
<ion-button color="medium" expand="block" class="ion-margin-top" shape="round">
<IonIcon slot="start" aria-hidden="true" :icon="logoGoogle" />
Google
</ion-button>
</IonContent>
</IonPage>
</template>
<style scoped>
.title {
margin-bottom: 30px;
}
</style>

View File

@@ -0,0 +1,87 @@
<script lang='ts' setup>
import type { InputCustomEvent } from "@ionic/vue";
import { toastController } from "@ionic/vue";
import { logoGoogle, phonePortraitOutline } from "ionicons/icons";
import { authClient } from "@/auth";
const emit = defineEmits<{
(e: "success", value: string): void;
}>();
const model = defineModel({ type: String, required: true });
const inputInstance = useTemplateRef<InputInstance>("inputInstance");
function markTouched() {
inputInstance.value?.$el.classList.add("ion-touched");
}
function validate(value: string) {
inputInstance.value?.$el.classList.remove("ion-valid");
inputInstance.value?.$el.classList.remove("ion-invalid");
if (value === "") {
return false;
}
const isEmailValid = emailPattern.test(model.value);
isEmailValid ? inputInstance.value?.$el.classList.add("ion-valid") : inputInstance.value?.$el.classList.add("ion-invalid");
return isEmailValid;
}
async function submitSendVerification() {
const isEmailValid = validate(model.value);
if (!isEmailValid) {
inputInstance.value?.$el.classList.remove("ion-invalid");
inputInstance.value?.$el.classList.add("ion-invalid");
inputInstance.value?.$el.classList.add("ion-touched");
return;
}
const { data, error } = await authClient.emailOtp.sendVerificationOtp({
email: model.value, // required
type: "sign-in", // required
});
if (data?.success) {
emit("success", model.value);
}
else {
const toast = await toastController.create({
message: error?.message || "Failed to send verification code.",
duration: 1500,
position: "bottom",
});
await toast.present();
}
}
</script>
<template>
<h1 class="title">
<strong>Log in</strong>
</h1>
<ui-input-label
ref="inputInstance"
v-model="model"
label="Email"
placeholder="Enter your email"
type="email"
error-text="Please enter a valid email address."
@ion-input="validate($event.target.value as string)"
@ion-blur="markTouched"
/>
<ion-button expand="block" class="ion-margin-top" shape="round" @click="submitSendVerification">
Next
</ion-button>
<ui-divider text="Or continue with" />
<ion-button color="medium" expand="block" class="ion-margin-top" shape="round">
<IonIcon slot="start" aria-hidden="true" :icon="logoGoogle" />
Google
</ion-button>
</template>
<style scoped></style>

View File

@@ -0,0 +1,41 @@
<script lang='ts' setup>
import { toastController } from "@ionic/vue";
const props = defineProps<{
email: string;
}>();
const emit = defineEmits<{
(e: "success", value: string): void;
}>();
const model = defineModel({ type: String, required: true });
async function submitSignup() {
if (model.value.length !== 6) {
const toast = await toastController.create({
message: "Please enter a valid 6-digit verification code.",
duration: 1500,
position: "bottom",
});
await toast.present();
return;
}
emit("success", model.value);
}
</script>
<template>
<h1><strong>Verify your email</strong></h1>
<p>We have sent a verification code to {{ email }}. Please enter the code below to verify your email address.</p>
<div>
<ion-input-otp v-model="model" :length="6" />
<ion-button expand="block" class="ion-margin-top" shape="round" @click="submitSignup">
Log in
</ion-button>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,53 @@
<script lang='ts' setup>
import { modalController } from "@ionic/vue";
import { authClient } from "@/auth";
import Step1 from "./email/step1.vue";
import Step2 from "./email/step2.vue";
const form = ref({
email: "",
verificationCode: "",
});
const step = ref(1);
async function closeModal() {
await modalController.dismiss();
}
async function submitSignup() {
const { data, error } = await authClient.signIn.emailOtp({
email: form.value.email,
otp: form.value.verificationCode,
});
if (data?.user) {
await modalController.dismiss(data.user);
}
else {
console.error("Login failed", error);
}
}
</script>
<template>
<IonPage>
<IonHeader class="ion-no-border">
<IonToolbar>
<IonButtons slot="start">
<IonButton @click="closeModal">
Close
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent :fullscreen="true" class="ion-padding">
<Step1 v-if="step === 1" v-model="form.email" @success="step = 2" />
<Step2 v-else-if="step === 2" v-model="form.verificationCode" :email="form.email" @success="submitSignup" />
</IonContent>
</IonPage>
</template>
<style scoped>
.title {
margin-bottom: 30px;
}
</style>

View File

@@ -1,8 +1,8 @@
<script lang='ts' setup>
import type { AuthUserSignup } from "../type";
import { toastController } from "@ionic/vue";
import type { AuthUserSignup } from "@/auth/type";
import { modalController, toastController } from "@ionic/vue";
import MaterialIconThemeGoogle from "~icons/material-icon-theme/google";
import { authClient } from "..";
import { authClient } from "@/auth";
import Step1 from "./email/step1.vue";
import Step2 from "./email/step2.vue";
@@ -29,9 +29,10 @@ async function submitSignup() {
email: form.value.email,
otp: form.value.verificationCode,
});
if (data?.token) {
if (data?.user) {
reset();
step.value = 1;
await modalController.dismiss(data.user);
const toast = await toastController.create({
message: "Email verified successfully!",
duration: 1500,

View File

@@ -10,7 +10,7 @@ export const authClient = createAuthClient({
});
export async function modelControllerSignup(presentingElement?: HTMLElement) {
const SignupContent = await import("./components/signup.vue");
const SignupContent = await import("./components/signup/index.vue");
const modal = await modalController.create({
component: SignupContent.default,
@@ -22,7 +22,7 @@ export async function modelControllerSignup(presentingElement?: HTMLElement) {
}
export async function modelControllerLogin(presentingElement?: HTMLElement) {
const LoginContent = await import("./components/login.vue");
const LoginContent = await import("./components/login/index.vue");
const modal = await modalController.create({
component: LoginContent.default,