refactor: 重构登陆模块

This commit is contained in:
2025-12-20 21:38:49 +07:00
parent 3d9785fdf2
commit 57bbebe961
28 changed files with 715 additions and 494 deletions

View File

@@ -1,89 +0,0 @@
<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 { t } = useI18n();
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 || t("auth.common.failedSendCode"),
duration: 1500,
position: "bottom",
});
await toast.present();
}
}
</script>
<template>
<h1 class="title">
<strong>{{ t('auth.login.title') }}</strong>
</h1>
<ui-input-label
ref="inputInstance"
v-model="model"
:label="t('auth.common.email')"
:placeholder="t('auth.common.enterEmail')"
type="email"
:error-text="t('auth.common.validEmailError')"
@ion-input="validate($event.target.value as string)"
@ion-blur="markTouched"
/>
<ion-button expand="block" class="ion-margin-top" shape="round" @click="submitSendVerification">
{{ t('auth.common.next') }}
</ion-button>
<ui-divider :text="t('auth.common.orContinueWith')" />
<ion-button color="medium" expand="block" class="ion-margin-top" shape="round">
<IonIcon slot="start" aria-hidden="true" :icon="logoGoogle" />
{{ t('auth.common.google') }}
</ion-button>
</template>
<style scoped></style>

View File

@@ -1,45 +0,0 @@
<script lang='ts' setup>
import { toastController } from "@ionic/vue";
const props = defineProps<{
email: string;
}>();
const emit = defineEmits<{
(e: "success", value: string): void;
}>();
const { t } = useI18n();
const model = defineModel({ type: String, required: true });
async function submitSignup() {
if (model.value.length !== 6) {
const toast = await toastController.create({
message: t("auth.common.validVerificationCodeError"),
duration: 1500,
position: "bottom",
});
await toast.present();
return;
}
emit("success", model.value);
}
</script>
<template>
<h1><strong>{{ t('auth.verification.title') }}</strong></h1>
<p>{{ t('auth.verification.description', { email: props.email }) }}</p>
<div>
<ion-input-otp v-model="model" :length="6" />
<ion-button expand="block" class="ion-margin-top" shape="round" @click="submitSignup">
{{ t('auth.login.loginButton') }}
</ion-button>
</div>
</template>
<style scoped></style>

View File

@@ -1,55 +0,0 @@
<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 { t } = useI18n();
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">
{{ t('auth.common.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,93 +0,0 @@
<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 { t } = useI18n();
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 || t("auth.common.failedSendCode"),
duration: 1500,
position: "bottom",
});
await toast.present();
}
}
</script>
<template>
<h1><strong>{{ t('auth.signup.title') }}</strong></h1>
<p>{{ t('auth.signup.description') }}</p>
<div>
<ui-input
ref="inputInstance"
v-model="model"
type="email"
:placeholder="t('auth.signup.emailPlaceholder')"
:error-text="t('auth.common.validEmailError')"
@ion-input="validate($event.target.value as string)"
@ion-blur="markTouched"
/>
<ion-button expand="block" class="ion-margin-top" shape="round" @click="submitSendVerification">
{{ t('auth.signup.signupButton') }}
</ion-button>
<ui-divider :text="t('auth.common.orContinueWith')" />
<ion-button color="medium" expand="block" class="ion-margin-top" shape="round">
<IonIcon slot="start" aria-hidden="true" :icon="phonePortraitOutline" />
{{ t('auth.common.phoneNumber') }}
</ion-button>
<ion-button color="medium" expand="block" class="ion-margin-top" shape="round">
<IonIcon slot="start" aria-hidden="true" :icon="logoGoogle" />
{{ t('auth.common.google') }}
</ion-button>
</div>
</template>
<style scoped></style>

View File

@@ -1,49 +0,0 @@
<script lang='ts' setup>
import type { PropType } from "vue";
import type { AuthUserSignup } from "@/auth/type";
import { toastController } from "@ionic/vue";
const emit = defineEmits<{
(e: "success", value: AuthUserSignup): void;
}>();
const { t } = useI18n();
const model = defineModel({ type: Object as PropType<AuthUserSignup>, required: true });
async function submitSignup() {
if (model.value.verificationCode.length !== 6) {
const toast = await toastController.create({
message: t("auth.common.validVerificationCodeError"),
duration: 1500,
position: "bottom",
});
await toast.present();
return;
}
emit("success", model.value);
}
</script>
<template>
<h1><strong>{{ t('auth.verification.title') }}</strong></h1>
<p>{{ t('auth.verification.description', { email: model.email }) }}</p>
<div>
<ion-input-otp v-model="model.verificationCode" :length="6" />
<!--
<ui-input v-model="model.name" placeholder="Name" />
<ui-input v-model="model.password" placeholder="Password" />
<ui-input v-model="model.confirmPassword" placeholder="Confirm Password" /> -->
<ion-button expand="block" class="ion-margin-top" shape="round" @click="submitSignup">
{{ t('auth.common.submit') }}
</ion-button>
</div>
</template>
<style scoped></style>

View File

@@ -1,27 +0,0 @@
<script lang='ts' setup>
import { modalController } from "@ionic/vue";
import VerificationCode from "./verification-code.vue";
const { t } = useI18n();
async function closeModal() {
await modalController.dismiss();
}
</script>
<template>
<IonPage>
<IonHeader class="ion-no-border">
<IonToolbar>
<IonButtons slot="start">
<IonButton @click="closeModal">
{{ t('auth.common.close') }}
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent :fullscreen="true" class="ion-padding">
<VerificationCode />
</IonContent>
</IonPage>
</template>

View File

@@ -1,63 +0,0 @@
<script lang='ts' setup>
import type { AuthUserSignup } from "@/auth/type";
import { modalController, toastController } from "@ionic/vue";
import MaterialIconThemeGoogle from "~icons/material-icon-theme/google";
import { authClient } from "@/auth";
import Step1 from "./email/step1.vue";
import Step2 from "./email/step2.vue";
const { t } = useI18n();
const form = ref<AuthUserSignup>({
name: "",
email: "",
password: "",
confirmPassword: "",
verificationCode: "",
});
const step = ref(1);
function reset() {
step.value = 1;
form.value.name = "";
form.value.email = "";
form.value.password = "";
form.value.confirmPassword = "";
form.value.verificationCode = "";
}
async function submitSignup() {
const { data, error } = await authClient.signIn.emailOtp({
email: form.value.email,
otp: form.value.verificationCode,
});
if (data?.user) {
reset();
step.value = 1;
await modalController.dismiss(data.user);
const toast = await toastController.create({
message: t("auth.signup.emailVerified"),
duration: 1500,
position: "bottom",
});
await toast.present();
}
else {
const toast = await toastController.create({
message: error?.message || t("auth.common.failedVerifyCode"),
duration: 1500,
position: "bottom",
});
await toast.present();
}
}
</script>
<template>
<Step1 v-if="step === 1" v-model="form.email" @success="step = 2" />
<Step2 v-else-if="step === 2" v-model="form" @success="submitSignup" />
</template>
<style lang='css' scoped></style>

View File

@@ -1,34 +1,53 @@
import { modalController } from "@ionic/vue";
import { emailOTPClient } from "better-auth/client/plugins";
import type { PhoneCountry } from "./type";
import { emailOTPClient, phoneNumberClient, usernameClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/vue";
import CircleFlagsCnHk from "~icons/circle-flags/cn-hk";
import CircleFlagsEnUs from "~icons/circle-flags/en-us";
import CircleFlagsTw from "~icons/circle-flags/tw";
import CircleFlagsZh from "~icons/circle-flags/zh";
export const authClient = createAuthClient({
fetchOptions: {
credentials: "include",
},
plugins: [emailOTPClient()],
plugins: [emailOTPClient(), phoneNumberClient(), usernameClient()],
});
export async function modelControllerSignup(presentingElement?: HTMLElement) {
const SignupContent = await import("./components/signup/index.vue");
const modal = await modalController.create({
component: SignupContent.default,
presentingElement,
canDismiss: async (data, role) => role !== "gesture",
});
return modal;
}
export async function modelControllerLogin(presentingElement?: HTMLElement) {
const LoginContent = await import("./components/login/index.vue");
const modal = await modalController.create({
component: LoginContent.default,
presentingElement,
canDismiss: async (data, role) => role !== "gesture",
});
return modal;
}
export const countries: PhoneCountry[] = [
{
code: "CN",
dialCode: "+86",
name: "中国大陆",
pattern: /^1[3-9]\d{9}$/,
maxLength: 11,
placeholder: "13800138000",
icon: CircleFlagsZh,
},
{
code: "HK",
dialCode: "+852",
name: "中国香港",
pattern: /^[5-9]\d{7}$/,
maxLength: 8,
placeholder: "51234567",
icon: CircleFlagsCnHk,
},
{
code: "TW",
dialCode: "+886",
name: "中国台湾",
pattern: /^9\d{8}$/,
maxLength: 9,
placeholder: "912345678",
icon: CircleFlagsTw,
},
{
code: "US",
dialCode: "+1",
name: "美国",
pattern: /^\d{10}$/,
maxLength: 10,
placeholder: "2025550123",
icon: CircleFlagsEnUs,
},
];

View File

@@ -1,3 +1,5 @@
import type { FunctionalComponent, SVGAttributes } from "vue";
export interface AuthUserSignup {
name: string;
email: string;
@@ -5,3 +7,13 @@ export interface AuthUserSignup {
confirmPassword: string;
verificationCode: string;
}
export interface PhoneCountry {
code: string;
dialCode: string;
name: string;
pattern: RegExp;
maxLength: number;
placeholder: string;
icon: FunctionalComponent<SVGAttributes>;
}