refactor: 重构登陆模块
This commit is contained in:
50
src/views/auth/login/components/country.vue
Normal file
50
src/views/auth/login/components/country.vue
Normal 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>
|
||||
173
src/views/auth/login/components/email.vue
Normal file
173
src/views/auth/login/components/email.vue
Normal 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>
|
||||
210
src/views/auth/login/components/phone-number.vue
Normal file
210
src/views/auth/login/components/phone-number.vue
Normal 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>
|
||||
67
src/views/auth/login/index.vue
Normal file
67
src/views/auth/login/index.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user