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

@@ -0,0 +1,50 @@
<script lang='ts' setup>
import { countries } from "@/auth";
const model = defineModel({ type: String, required: true });
const { t } = useI18n();
function handleChangeCountry(code: string) {
model.value = code;
}
</script>
<template>
<ion-list lines="full">
<ion-list-header>
<ion-label>{{ t('auth.login.selectCountryCode') }}</ion-label>
</ion-list-header>
<ion-radio-group :value="model" @ion-change="handleChangeCountry($event.detail.value)">
<ion-item v-for="country in countries" :key="country.code">
<ion-radio :value="country.code" class="py-2">
<div class="flex justify-between w-full items-center">
<div class="flex-center space-x-2">
<div class="icon">
<component :is="country.icon" class="text-lg" />
</div>
<div class="text-sm font-semibold">
{{ country.code }}
</div>
</div>
<div class="end">
{{ country.dialCode }}
</div>
</div>
</ion-radio>
</ion-item>
</ion-radio-group>
</ion-list>
</template>
<style lang='css' scoped>
@reference "tailwindcss";
ion-item {
--padding-start: 0;
--padding-end: 0;
--padding-top: 6px;
--padding-bottom: 6px;
border-radius: 0.25rem;
}
</style>

View File

@@ -0,0 +1,173 @@
<script lang='ts' setup>
import type { GenericObject } from "vee-validate";
import type { EmailVerifyClient } from "@/api/types";
import { toastController } from "@ionic/vue";
import { toTypedSchema } from "@vee-validate/yup";
import { Field, Form } from "vee-validate";
import * as yup from "yup";
import { authClient } from "@/auth";
const emit = defineEmits<{
(e: "submit", value: EmailVerifyClient): void;
}>();
const { t } = useI18n();
const countdown = ref(0);
const isSending = ref(false);
const canResend = computed(() => countdown.value === 0 && !isSending.value);
const email = ref("");
const emailError = ref("");
let timer: number | null = null;
const schema = computed(() => toTypedSchema(yup.object({
email: yup
.string()
.required(t("auth.login.validation.emailRequired"))
.email(t("auth.login.validation.emailInvalid")),
otp: yup
.string()
.required(t("auth.login.validation.otpRequired"))
.matches(/^\d{6}$/, t("auth.login.validation.otpInvalid")),
})));
function startCountdown() {
countdown.value = 60;
timer = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
if (timer) {
clearInterval(timer);
timer = null;
}
}
}, 1000);
}
async function sendOtp() {
const emailValue = email.value.trim();
if (!emailValue) {
emailError.value = t("auth.login.validation.emailRequired");
return;
}
// 使用yup进行验证
try {
await yup.string().email().validate(emailValue);
}
catch {
emailError.value = t("auth.login.validation.emailInvalid");
return;
}
if (!canResend.value) {
return;
}
try {
emailError.value = "";
isSending.value = true;
await authClient.emailOtp.sendVerificationOtp({
email: emailValue,
type: "sign-in",
});
const toast = await toastController.create({
message: t("auth.login.sendCodeSuccess"),
duration: 2000,
position: "top",
color: "success",
});
await toast.present();
startCountdown();
}
catch (error: any) {
const toast = await toastController.create({
message: error?.message || t("auth.common.failedSendCode"),
duration: 2000,
position: "top",
color: "danger",
});
await toast.present();
}
finally {
isSending.value = false;
}
}
function handleSubmit(values: GenericObject) {
emit("submit", values as EmailVerifyClient);
}
onUnmounted(() => {
if (timer) {
clearInterval(timer);
timer = null;
}
});
</script>
<template>
<Form :validation-schema="schema" class="mt-5" @submit="handleSubmit">
<Field v-slot="{ field, errors }" name="email" type="email">
<div class="mb-4">
<ui-input
v-bind="field"
v-model="email"
:placeholder="t('auth.login.enterEmail')"
type="email"
>
<ion-button
slot="end"
fill="clear"
size="small"
:disabled="!canResend"
@click="sendOtp"
>
<span v-if="countdown > 0">
{{ countdown }}s
</span>
<span v-else-if="isSending">
{{ t('auth.login.sending') }}
</span>
<span v-else>
{{ t('auth.login.getCode') }}
</span>
</ion-button>
</ui-input>
<div v-if="errors[0] || emailError" class="text-xs text-red-500 mt-1">
{{ errors[0] || emailError }}
</div>
</div>
</Field>
<Field v-slot="{ field, errorMessage }" name="otp" type="text">
<div class="mb-4">
<ui-input
v-bind="field"
:placeholder="t('auth.login.enterOtp')"
:maxlength="6"
type="text"
/>
<div v-if="errorMessage" class="text-xs text-red-500 mt-1">
{{ errorMessage }}
</div>
</div>
</Field>
<ion-button
expand="block"
class="ion-margin-top"
shape="round"
type="submit"
>
{{ t('auth.login.loginButton') }}
</ion-button>
</Form>
</template>
<style lang='css' scoped>
@reference "tailwindcss";
</style>

View File

@@ -0,0 +1,210 @@
<script lang='ts' setup>
import type { GenericObject } from "vee-validate";
import type { PhoneNumberVerifyClient } from "@/api/types";
import type { PhoneCountry } from "@/auth/type";
import { toastController } from "@ionic/vue";
import { toTypedSchema } from "@vee-validate/yup";
import { chevronDown } from "ionicons/icons";
import { Field, Form } from "vee-validate";
import * as yup from "yup";
import { authClient, countries } from "@/auth";
import Country from "./country.vue";
const emit = defineEmits<{
(e: "submit", value: PhoneNumberVerifyClient): void;
}>();
const { t } = useI18n();
const countdown = ref(0);
const isSending = ref(false);
const canResend = computed(() => countdown.value === 0 && !isSending.value);
const phoneNumber = ref("");
const phoneNumberError = ref("");
const modalInst = useTemplateRef<ModalInstance>("modalInst");
const countryCode = ref<string>(countries[0].code);
const currentCountry = computed<PhoneCountry>(() => {
return countries.find(c => c.code === countryCode.value) || countries[0];
});
let timer: number | null = null;
function dismiss() {
modalInst.value?.$el.dismiss();
}
function handleChangeCountry() {
dismiss();
phoneNumber.value = "";
phoneNumberError.value = "";
}
function validatePhoneNumber(phone: string): boolean {
return currentCountry.value.pattern.test(phone);
}
const schema = computed(() => toTypedSchema(yup.object({
phoneNumber: yup
.string()
.required(t("auth.login.validation.phoneNumberRequired"))
.test(
"phone-format",
t("auth.login.validation.phoneNumberInvalid"),
value => !value || validatePhoneNumber(value),
),
code: yup
.string()
.required(t("auth.login.validation.codeRequired"))
.matches(/^\d{6}$/, t("auth.login.validation.codeInvalid")),
})));
function startCountdown() {
countdown.value = 60;
timer = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
if (timer) {
clearInterval(timer);
timer = null;
}
}
}, 1000);
}
async function sendOtp() {
const phone = phoneNumber.value.trim();
if (!phone) {
phoneNumberError.value = t("auth.login.validation.phoneNumberRequired");
return;
}
if (!validatePhoneNumber(phone)) {
phoneNumberError.value = t("auth.login.validation.phoneNumberInvalid");
return;
}
if (!canResend.value) {
return;
}
try {
phoneNumberError.value = "";
isSending.value = true;
await authClient.phoneNumber.sendOtp({
phoneNumber: `${currentCountry.value.dialCode}${phone}`,
});
const toast = await toastController.create({
message: t("auth.login.sendCodeSuccess"),
duration: 2000,
position: "top",
color: "success",
});
await toast.present();
startCountdown();
}
catch (error: any) {
const toast = await toastController.create({
message: error?.message || t("auth.common.failedSendCode"),
duration: 2000,
position: "top",
color: "danger",
});
await toast.present();
}
finally {
isSending.value = false;
}
}
function handleSubmit(values: GenericObject) {
emit("submit", {
phoneNumber: `${currentCountry.value.dialCode}${values.phoneNumber}`,
code: values.code,
});
}
onUnmounted(() => {
if (timer) {
clearInterval(timer);
timer = null;
}
});
</script>
<template>
<Form :validation-schema="schema" class="mt-5" @submit="handleSubmit">
<Field v-slot="{ field, errors }" name="phoneNumber" type="tel">
<div class="mb-4">
<ui-input
v-bind="field"
v-model="phoneNumber"
:placeholder="currentCountry.placeholder"
:maxlength="currentCountry.maxLength"
type="tel"
>
<ion-button
id="open-country"
slot="start"
fill="clear"
size="small"
>
{{ currentCountry.dialCode }}
<ion-icon slot="end" :icon="chevronDown" class="text-sm" />
</ion-button>
<ion-button
slot="end"
fill="clear"
size="small"
:disabled="!canResend"
@click="sendOtp"
>
<span v-if="countdown > 0">
{{ countdown }}s
</span>
<span v-else-if="isSending">
{{ t('auth.login.sending') }}
</span>
<span v-else>
{{ t('auth.login.getCode') }}
</span>
</ion-button>
</ui-input>
<div v-if="errors[0] || phoneNumberError" class="text-xs text-red-500 mt-1">
{{ errors[0] || phoneNumberError }}
</div>
</div>
</Field>
<Field v-slot="{ field, errorMessage }" name="code" type="text">
<div class="mb-4">
<ui-input
v-bind="field"
:placeholder="t('auth.login.enterCode')"
:maxlength="6"
/>
<div v-if="errorMessage" class="text-xs text-red-500 mt-1">
{{ errorMessage }}
</div>
</div>
</Field>
<ion-button
expand="block"
class="ion-margin-top"
shape="round"
type="submit"
>
{{ t('auth.login.loginButton') }}
</ion-button>
</Form>
<ion-modal ref="modalInst" trigger="open-country" :initial-breakpoint="0.95" :breakpoints="[0, 0.95]">
<ion-content class="ion-padding">
<Country v-model="countryCode" @update:model-value="handleChangeCountry" />
</ion-content>
</ion-modal>
</template>
<style lang='css' scoped>
@reference "tailwindcss";
</style>

View File

@@ -0,0 +1,67 @@
<script lang='ts' setup>
import type { EmailVerifyClient, PhoneNumberVerifyClient } from "@/api/types";
import { authClient } from "@/auth";
import EmailLogin from "./components/email.vue";
import PhoneNumberLogin from "./components/phone-number.vue";
const { t } = useI18n();
const router = useRouter();
async function handleSignInEmail(value: EmailVerifyClient) {
const { data } = await authClient.signIn.emailOtp({
email: value.email,
otp: value.otp,
});
if (data?.token) {
router.back();
}
}
async function handleSignInPhoneNumber(value: PhoneNumberVerifyClient) {
const { data } = await authClient.phoneNumber.verify({
phoneNumber: value.phoneNumber,
code: value.code,
disableSession: false,
updatePhoneNumber: true,
});
if (data?.token) {
router.back();
}
}
</script>
<template>
<IonPage>
<IonHeader class="ion-no-border">
<IonToolbar class="ui-toolbar">
<ion-back-button slot="start" />
<ion-button slot="end" fill="clear">
注册
</ion-button>
</IonToolbar>
</IonHeader>
<IonContent :fullscreen="true" class="ion-padding">
<div class="text-2xl font-semibold mb-5">
{{ t('auth.login.title') }}
</div>
<ui-tabs class="mb-5">
<ui-tab-pane name="username" :title="t('auth.login.username')">
<div class="py-5">
<EmailLogin @submit="handleSignInEmail" />
</div>
</ui-tab-pane>
<ui-tab-pane name="phone" :title="t('auth.login.phone')">
<div class="py-5">
<PhoneNumberLogin @submit="handleSignInPhoneNumber" />
</div>
</ui-tab-pane>
</ui-tabs>
</IonContent>
</IonPage>
</template>
<style scoped>
.title {
margin-bottom: 30px;
}
</style>

View File

@@ -4,7 +4,7 @@ import { client, safeClient } from "@/api";
const { t } = useI18n();
const model = defineModel({ type: String, default: "" });
const { data: categories } = await safeClient(() => client.api.rwa.issuance.categories.get());
const { data: categories } = await safeClient(() => client.api.rwa.category.categories.get());
</script>
<template>

View File

@@ -1,18 +1,9 @@
<script setup lang="ts">
import { authClient, modelControllerLogin, modelControllerSignup } from "@/auth";
import { authClient } from "@/auth";
const page = useTemplateRef<PageInstance>("page");
const { user } = useAuth();
const tradingViewContainer = useTemplateRef<HTMLElement>("tradingViewContainer");
async function openSignin() {
const modal = await modelControllerLogin(page.value?.$el);
await modal.present();
}
async function openSignup() {
const modal = await modelControllerSignup(page.value?.$el);
await modal.present();
}
async function handleLogout() {
await authClient.signOut();
}
@@ -32,17 +23,17 @@ useTradingView(tradingViewContainer, {
</script>
<template>
<IonPage ref="page">
<IonPage>
<IonHeader>
<IonToolbar class="ui-tabbar">
<IonTitle>Home</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent :fullscreen="true">
<IonButton @click="openSignin">
<IonButton @click="$router.push('/auth/login')">
Log in
</IonButton>
<IonButton @click="openSignup">
<IonButton @click="$router.push('/auth/signup')">
Sign up
</IonButton>
<IonButton @click="handleLogout">

View File

@@ -46,13 +46,3 @@ function handleLanguageChange(event: CustomEvent) {
</template>
<style lang='css' scoped></style>
"css" scoped>
@reference "tailwindcss";
.language-item {
@apply py-1;
}
ion-radio {
width: 100%;
}