refactor: 重构登陆模块
This commit is contained in:
@@ -1 +1 @@
|
|||||||
VITE_API_URL=http://192.168.1.22:9527
|
VITE_API_URL=http://192.168.1.27:9527
|
||||||
12
components.d.ts
vendored
12
components.d.ts
vendored
@@ -20,11 +20,6 @@ declare module 'vue' {
|
|||||||
IonBackButton: typeof import('@ionic/vue')['IonBackButton']
|
IonBackButton: typeof import('@ionic/vue')['IonBackButton']
|
||||||
IonButton: typeof import('@ionic/vue')['IonButton']
|
IonButton: typeof import('@ionic/vue')['IonButton']
|
||||||
IonButtons: typeof import('@ionic/vue')['IonButtons']
|
IonButtons: typeof import('@ionic/vue')['IonButtons']
|
||||||
IonCard: typeof import('@ionic/vue')['IonCard']
|
|
||||||
IonCardContent: typeof import('@ionic/vue')['IonCardContent']
|
|
||||||
IonCardHeader: typeof import('@ionic/vue')['IonCardHeader']
|
|
||||||
IonCardSubtitle: typeof import('@ionic/vue')['IonCardSubtitle']
|
|
||||||
IonCardTitle: typeof import('@ionic/vue')['IonCardTitle']
|
|
||||||
IonCol: typeof import('@ionic/vue')['IonCol']
|
IonCol: typeof import('@ionic/vue')['IonCol']
|
||||||
IonContent: typeof import('@ionic/vue')['IonContent']
|
IonContent: typeof import('@ionic/vue')['IonContent']
|
||||||
IonDatetime: typeof import('@ionic/vue')['IonDatetime']
|
IonDatetime: typeof import('@ionic/vue')['IonDatetime']
|
||||||
@@ -37,6 +32,7 @@ declare module 'vue' {
|
|||||||
IonInfiniteScrollContent: typeof import('@ionic/vue')['IonInfiniteScrollContent']
|
IonInfiniteScrollContent: typeof import('@ionic/vue')['IonInfiniteScrollContent']
|
||||||
IonInput: typeof import('@ionic/vue')['IonInput']
|
IonInput: typeof import('@ionic/vue')['IonInput']
|
||||||
IonInputOtp: typeof import('@ionic/vue')['IonInputOtp']
|
IonInputOtp: typeof import('@ionic/vue')['IonInputOtp']
|
||||||
|
IonInputPasswordToggle: typeof import('@ionic/vue')['IonInputPasswordToggle']
|
||||||
IonItem: typeof import('@ionic/vue')['IonItem']
|
IonItem: typeof import('@ionic/vue')['IonItem']
|
||||||
IonLabel: typeof import('@ionic/vue')['IonLabel']
|
IonLabel: typeof import('@ionic/vue')['IonLabel']
|
||||||
IonList: typeof import('@ionic/vue')['IonList']
|
IonList: typeof import('@ionic/vue')['IonList']
|
||||||
@@ -87,11 +83,6 @@ declare global {
|
|||||||
const IonBackButton: typeof import('@ionic/vue')['IonBackButton']
|
const IonBackButton: typeof import('@ionic/vue')['IonBackButton']
|
||||||
const IonButton: typeof import('@ionic/vue')['IonButton']
|
const IonButton: typeof import('@ionic/vue')['IonButton']
|
||||||
const IonButtons: typeof import('@ionic/vue')['IonButtons']
|
const IonButtons: typeof import('@ionic/vue')['IonButtons']
|
||||||
const IonCard: typeof import('@ionic/vue')['IonCard']
|
|
||||||
const IonCardContent: typeof import('@ionic/vue')['IonCardContent']
|
|
||||||
const IonCardHeader: typeof import('@ionic/vue')['IonCardHeader']
|
|
||||||
const IonCardSubtitle: typeof import('@ionic/vue')['IonCardSubtitle']
|
|
||||||
const IonCardTitle: typeof import('@ionic/vue')['IonCardTitle']
|
|
||||||
const IonCol: typeof import('@ionic/vue')['IonCol']
|
const IonCol: typeof import('@ionic/vue')['IonCol']
|
||||||
const IonContent: typeof import('@ionic/vue')['IonContent']
|
const IonContent: typeof import('@ionic/vue')['IonContent']
|
||||||
const IonDatetime: typeof import('@ionic/vue')['IonDatetime']
|
const IonDatetime: typeof import('@ionic/vue')['IonDatetime']
|
||||||
@@ -104,6 +95,7 @@ declare global {
|
|||||||
const IonInfiniteScrollContent: typeof import('@ionic/vue')['IonInfiniteScrollContent']
|
const IonInfiniteScrollContent: typeof import('@ionic/vue')['IonInfiniteScrollContent']
|
||||||
const IonInput: typeof import('@ionic/vue')['IonInput']
|
const IonInput: typeof import('@ionic/vue')['IonInput']
|
||||||
const IonInputOtp: typeof import('@ionic/vue')['IonInputOtp']
|
const IonInputOtp: typeof import('@ionic/vue')['IonInputOtp']
|
||||||
|
const IonInputPasswordToggle: typeof import('@ionic/vue')['IonInputPasswordToggle']
|
||||||
const IonItem: typeof import('@ionic/vue')['IonItem']
|
const IonItem: typeof import('@ionic/vue')['IonItem']
|
||||||
const IonLabel: typeof import('@ionic/vue')['IonLabel']
|
const IonLabel: typeof import('@ionic/vue')['IonLabel']
|
||||||
const IonList: typeof import('@ionic/vue')['IonList']
|
const IonList: typeof import('@ionic/vue')['IonList']
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"@elysiajs/eden": "^1.4.5",
|
"@elysiajs/eden": "^1.4.5",
|
||||||
"@ionic/vue": "^8.7.11",
|
"@ionic/vue": "^8.7.11",
|
||||||
"@ionic/vue-router": "^8.7.11",
|
"@ionic/vue-router": "^8.7.11",
|
||||||
"@riwa/api-types": "http://192.168.1.27:9527/api/riwa-api-types-0.0.37.tgz",
|
"@riwa/api-types": "http://192.168.1.27:9527/api/riwa-api-types-0.0.39.tgz",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@vee-validate/yup": "^4.15.1",
|
"@vee-validate/yup": "^4.15.1",
|
||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^6.6.1",
|
"@antfu/eslint-config": "^6.6.1",
|
||||||
"@capacitor/cli": "8.0.0",
|
"@capacitor/cli": "8.0.0",
|
||||||
|
"@iconify-json/circle-flags": "^1.2.10",
|
||||||
"@iconify-json/cryptocurrency-color": "^1.2.4",
|
"@iconify-json/cryptocurrency-color": "^1.2.4",
|
||||||
"@iconify-json/ic": "^1.2.4",
|
"@iconify-json/ic": "^1.2.4",
|
||||||
"@iconify-json/material-icon-theme": "^1.2.44",
|
"@iconify-json/material-icon-theme": "^1.2.44",
|
||||||
|
|||||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@@ -45,8 +45,8 @@ importers:
|
|||||||
specifier: ^8.7.11
|
specifier: ^8.7.11
|
||||||
version: 8.7.11(@stencil/core@4.39.0)(vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
|
version: 8.7.11(@stencil/core@4.39.0)(vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
|
||||||
'@riwa/api-types':
|
'@riwa/api-types':
|
||||||
specifier: http://192.168.1.27:9527/api/riwa-api-types-0.0.37.tgz
|
specifier: http://192.168.1.27:9527/api/riwa-api-types-0.0.39.tgz
|
||||||
version: http://192.168.1.27:9527/api/riwa-api-types-0.0.37.tgz(@elysiajs/eden@1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))
|
version: http://192.168.1.27:9527/api/riwa-api-types-0.0.39.tgz(@elysiajs/eden@1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))
|
||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.1.18
|
specifier: ^4.1.18
|
||||||
version: 4.1.18(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
|
version: 4.1.18(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
|
||||||
@@ -102,6 +102,9 @@ importers:
|
|||||||
'@capacitor/cli':
|
'@capacitor/cli':
|
||||||
specifier: 8.0.0
|
specifier: 8.0.0
|
||||||
version: 8.0.0
|
version: 8.0.0
|
||||||
|
'@iconify-json/circle-flags':
|
||||||
|
specifier: ^1.2.10
|
||||||
|
version: 1.2.10
|
||||||
'@iconify-json/cryptocurrency-color':
|
'@iconify-json/cryptocurrency-color':
|
||||||
specifier: ^1.2.4
|
specifier: ^1.2.4
|
||||||
version: 1.2.4
|
version: 1.2.4
|
||||||
@@ -1145,6 +1148,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||||
engines: {node: '>=18.18'}
|
engines: {node: '>=18.18'}
|
||||||
|
|
||||||
|
'@iconify-json/circle-flags@1.2.10':
|
||||||
|
resolution: {integrity: sha512-sZRxs689a281RtZvuAiKtV7pQHv8Tk0HkinSM7QvLgdLEK8xgGRCcbDvL09Rq+/KtemmsMzGhS9/qt+r3ca+Ug==}
|
||||||
|
|
||||||
'@iconify-json/cryptocurrency-color@1.2.4':
|
'@iconify-json/cryptocurrency-color@1.2.4':
|
||||||
resolution: {integrity: sha512-8vjIfTAAMg0zo3/CdVWV7YjViY1L/q4TFfjROmqRPCRPhM6iVecW4TzMFS8hxm48S2Ge69SNM1yC8FmHT+jfHw==}
|
resolution: {integrity: sha512-8vjIfTAAMg0zo3/CdVWV7YjViY1L/q4TFfjROmqRPCRPhM6iVecW4TzMFS8hxm48S2Ge69SNM1yC8FmHT+jfHw==}
|
||||||
|
|
||||||
@@ -1315,9 +1321,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
|
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
|
||||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||||
|
|
||||||
'@riwa/api-types@http://192.168.1.27:9527/api/riwa-api-types-0.0.37.tgz':
|
'@riwa/api-types@http://192.168.1.27:9527/api/riwa-api-types-0.0.39.tgz':
|
||||||
resolution: {tarball: http://192.168.1.27:9527/api/riwa-api-types-0.0.37.tgz}
|
resolution: {tarball: http://192.168.1.27:9527/api/riwa-api-types-0.0.39.tgz}
|
||||||
version: 0.0.37
|
version: 0.0.39
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@elysiajs/eden': ^1.4.5
|
'@elysiajs/eden': ^1.4.5
|
||||||
|
|
||||||
@@ -6086,6 +6092,10 @@ snapshots:
|
|||||||
|
|
||||||
'@humanwhocodes/retry@0.4.3': {}
|
'@humanwhocodes/retry@0.4.3': {}
|
||||||
|
|
||||||
|
'@iconify-json/circle-flags@1.2.10':
|
||||||
|
dependencies:
|
||||||
|
'@iconify/types': 2.0.0
|
||||||
|
|
||||||
'@iconify-json/cryptocurrency-color@1.2.4':
|
'@iconify-json/cryptocurrency-color@1.2.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@iconify/types': 2.0.0
|
'@iconify/types': 2.0.0
|
||||||
@@ -6390,7 +6400,7 @@ snapshots:
|
|||||||
|
|
||||||
'@pkgr/core@0.2.9': {}
|
'@pkgr/core@0.2.9': {}
|
||||||
|
|
||||||
'@riwa/api-types@http://192.168.1.27:9527/api/riwa-api-types-0.0.37.tgz(@elysiajs/eden@1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))':
|
'@riwa/api-types@http://192.168.1.27:9527/api/riwa-api-types-0.0.39.tgz(@elysiajs/eden@1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@elysiajs/eden': 1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3))
|
'@elysiajs/eden': 1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3))
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Treaty } from "@elysiajs/eden";
|
import type { Treaty } from "@elysiajs/eden";
|
||||||
import type { client } from ".";
|
import type { client } from ".";
|
||||||
import type { AssetCodeEnum, PaymentChannelEnum, WithdrawMethodEnum } from "./enum";
|
import type { AssetCodeEnum, PaymentChannelEnum, WithdrawMethodEnum } from "./enum";
|
||||||
|
import type { authClient } from "@/auth";
|
||||||
|
|
||||||
export type DepositFiatBody = Parameters<typeof client.api.deposit.fiat.post>[0] & {
|
export type DepositFiatBody = Parameters<typeof client.api.deposit.fiat.post>[0] & {
|
||||||
paymentChannel: PaymentChannelEnum;
|
paymentChannel: PaymentChannelEnum;
|
||||||
@@ -51,3 +52,9 @@ export type RwaData = Treaty.Data<typeof client.api.rwa.subscription.available_e
|
|||||||
export type MySubscribeRwaData = Treaty.Data<typeof client.api.rwa.subscription.my_subscriptions.get>;
|
export type MySubscribeRwaData = Treaty.Data<typeof client.api.rwa.subscription.my_subscriptions.get>;
|
||||||
|
|
||||||
export type MySubscribeRwaBody = TreatyQuery<typeof client.api.rwa.subscription.my_subscriptions.get>;
|
export type MySubscribeRwaBody = TreatyQuery<typeof client.api.rwa.subscription.my_subscriptions.get>;
|
||||||
|
|
||||||
|
export type PhoneNumberVerifyClient = TreatyBody<typeof authClient.phoneNumber.verify>;
|
||||||
|
|
||||||
|
export type UsernameClient = TreatyBody<typeof authClient.signIn.username>;
|
||||||
|
|
||||||
|
export type EmailVerifyClient = TreatyBody<typeof authClient.emailOtp.verifyEmail>;
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -1,34 +1,53 @@
|
|||||||
import { modalController } from "@ionic/vue";
|
import type { PhoneCountry } from "./type";
|
||||||
import { emailOTPClient } from "better-auth/client/plugins";
|
import { emailOTPClient, phoneNumberClient, usernameClient } from "better-auth/client/plugins";
|
||||||
import { createAuthClient } from "better-auth/vue";
|
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({
|
export const authClient = createAuthClient({
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
},
|
},
|
||||||
plugins: [emailOTPClient()],
|
plugins: [emailOTPClient(), phoneNumberClient(), usernameClient()],
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function modelControllerSignup(presentingElement?: HTMLElement) {
|
export const countries: PhoneCountry[] = [
|
||||||
const SignupContent = await import("./components/signup/index.vue");
|
{
|
||||||
|
code: "CN",
|
||||||
const modal = await modalController.create({
|
dialCode: "+86",
|
||||||
component: SignupContent.default,
|
name: "中国大陆",
|
||||||
presentingElement,
|
pattern: /^1[3-9]\d{9}$/,
|
||||||
canDismiss: async (data, role) => role !== "gesture",
|
maxLength: 11,
|
||||||
});
|
placeholder: "13800138000",
|
||||||
|
icon: CircleFlagsZh,
|
||||||
return modal;
|
},
|
||||||
}
|
{
|
||||||
|
code: "HK",
|
||||||
export async function modelControllerLogin(presentingElement?: HTMLElement) {
|
dialCode: "+852",
|
||||||
const LoginContent = await import("./components/login/index.vue");
|
name: "中国香港",
|
||||||
|
pattern: /^[5-9]\d{7}$/,
|
||||||
const modal = await modalController.create({
|
maxLength: 8,
|
||||||
component: LoginContent.default,
|
placeholder: "51234567",
|
||||||
presentingElement,
|
icon: CircleFlagsCnHk,
|
||||||
canDismiss: async (data, role) => role !== "gesture",
|
},
|
||||||
});
|
{
|
||||||
|
code: "TW",
|
||||||
return modal;
|
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,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { FunctionalComponent, SVGAttributes } from "vue";
|
||||||
|
|
||||||
export interface AuthUserSignup {
|
export interface AuthUserSignup {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -5,3 +7,13 @@ export interface AuthUserSignup {
|
|||||||
confirmPassword: string;
|
confirmPassword: string;
|
||||||
verificationCode: string;
|
verificationCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PhoneCountry {
|
||||||
|
code: string;
|
||||||
|
dialCode: string;
|
||||||
|
name: string;
|
||||||
|
pattern: RegExp;
|
||||||
|
maxLength: number;
|
||||||
|
placeholder: string;
|
||||||
|
icon: FunctionalComponent<SVGAttributes>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ defineExpose({} as ComponentInstance<typeof IonInput>);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<component :is="h(IonInput, { ...$attrs, ref: changeRef })" class="ui-input" />
|
<component :is="h(IonInput, { ...$attrs, ref: changeRef }, { default: () => { return $slots.default ? $slots.default() : null } })" class="ui-input" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -286,7 +286,33 @@
|
|||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "تسجيل الدخول",
|
"title": "تسجيل الدخول",
|
||||||
"loginButton": "تسجيل الدخول"
|
"loginButton": "تسجيل الدخول",
|
||||||
|
"username": "اسم المستخدم",
|
||||||
|
"phone": "الهاتف",
|
||||||
|
"selectCountryCode": "اختر الدولة/المنطقة",
|
||||||
|
"enterPhoneNumber": "أدخل رقم الهاتف",
|
||||||
|
"enterUsername": "أدخل اسم المستخدم",
|
||||||
|
"enterPassword": "أدخل كلمة المرور",
|
||||||
|
"enterEmail": "أدخل البريد الإلكتروني",
|
||||||
|
"enterCode": "أدخل رمز التحقق",
|
||||||
|
"enterOtp": "أدخل رمز التحقق",
|
||||||
|
"getCode": "احصل على الرمز",
|
||||||
|
"resendCode": "إعادة إرسال",
|
||||||
|
"sending": "جاري الإرسال...",
|
||||||
|
"sendCodeSuccess": "تم إرسال رمز التحقق",
|
||||||
|
"forgotPassword": "هل نسيت كلمة المرور؟",
|
||||||
|
"validation": {
|
||||||
|
"usernameRequired": "الرجاء إدخال اسم المستخدم",
|
||||||
|
"passwordRequired": "الرجاء إدخال كلمة المرور",
|
||||||
|
"phoneNumberRequired": "الرجاء إدخال رقم الهاتف",
|
||||||
|
"phoneNumberInvalid": "الرجاء إدخال رقم هاتف صحيح",
|
||||||
|
"codeRequired": "الرجاء إدخال رمز التحقق",
|
||||||
|
"codeInvalid": "الرجاء إدخال رمز تحقق من 6 أرقام",
|
||||||
|
"emailRequired": "الرجاء إدخال البريد الإلكتروني",
|
||||||
|
"emailInvalid": "الرجاء إدخال عنوان بريد إلكتروني صحيح",
|
||||||
|
"otpRequired": "الرجاء إدخال رمز التحقق",
|
||||||
|
"otpInvalid": "الرجاء إدخال رمز تحقق من 6 أرقام"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"signup": {
|
"signup": {
|
||||||
"title": "ما هو بريدك الإلكتروني؟",
|
"title": "ما هو بريدك الإلكتروني؟",
|
||||||
|
|||||||
@@ -286,7 +286,33 @@
|
|||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Log in",
|
"title": "Log in",
|
||||||
"loginButton": "Log in"
|
"loginButton": "Log in",
|
||||||
|
"username": "Username",
|
||||||
|
"phone": "Phone",
|
||||||
|
"selectCountryCode": "Select Country/Region",
|
||||||
|
"enterPhoneNumber": "Enter phone number",
|
||||||
|
"enterUsername": "Enter username",
|
||||||
|
"enterPassword": "Enter password",
|
||||||
|
"enterEmail": "Enter email",
|
||||||
|
"enterCode": "Enter verification code",
|
||||||
|
"enterOtp": "Enter verification code",
|
||||||
|
"getCode": "Get Code",
|
||||||
|
"resendCode": "Resend",
|
||||||
|
"sending": "Sending...",
|
||||||
|
"sendCodeSuccess": "Verification code sent",
|
||||||
|
"forgotPassword": "Forgot password?",
|
||||||
|
"validation": {
|
||||||
|
"usernameRequired": "Please enter username",
|
||||||
|
"passwordRequired": "Please enter password",
|
||||||
|
"phoneNumberRequired": "Please enter phone number",
|
||||||
|
"phoneNumberInvalid": "Please enter a valid phone number",
|
||||||
|
"codeRequired": "Please enter verification code",
|
||||||
|
"codeInvalid": "Please enter a 6-digit verification code",
|
||||||
|
"emailRequired": "Please enter email",
|
||||||
|
"emailInvalid": "Please enter a valid email address",
|
||||||
|
"otpRequired": "Please enter verification code",
|
||||||
|
"otpInvalid": "Please enter a 6-digit verification code"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"signup": {
|
"signup": {
|
||||||
"title": "What's your email?",
|
"title": "What's your email?",
|
||||||
|
|||||||
@@ -286,7 +286,33 @@
|
|||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "登录",
|
"title": "登录",
|
||||||
"loginButton": "登录"
|
"loginButton": "登录",
|
||||||
|
"username": "用户名",
|
||||||
|
"phone": "手机号",
|
||||||
|
"selectCountryCode": "选择国家/地区",
|
||||||
|
"enterPhoneNumber": "请输入手机号",
|
||||||
|
"enterUsername": "请输入用户名",
|
||||||
|
"enterPassword": "请输入密码",
|
||||||
|
"enterEmail": "请输入邮箱",
|
||||||
|
"enterCode": "请输入验证码",
|
||||||
|
"enterOtp": "请输入验证码",
|
||||||
|
"getCode": "获取验证码",
|
||||||
|
"resendCode": "重新发送",
|
||||||
|
"sending": "发送中...",
|
||||||
|
"sendCodeSuccess": "验证码已发送",
|
||||||
|
"forgotPassword": "忘记密码?",
|
||||||
|
"validation": {
|
||||||
|
"usernameRequired": "请输入用户名",
|
||||||
|
"passwordRequired": "请输入密码",
|
||||||
|
"phoneNumberRequired": "请输入手机号",
|
||||||
|
"phoneNumberInvalid": "请输入有效的手机号",
|
||||||
|
"codeRequired": "请输入验证码",
|
||||||
|
"codeInvalid": "请输入6位数字验证码",
|
||||||
|
"emailRequired": "请输入邮箱",
|
||||||
|
"emailInvalid": "请输入有效的邮箱地址",
|
||||||
|
"otpRequired": "请输入验证码",
|
||||||
|
"otpInvalid": "请输入6位数字验证码"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"signup": {
|
"signup": {
|
||||||
"title": "您的邮箱是什么?",
|
"title": "您的邮箱是什么?",
|
||||||
|
|||||||
@@ -286,7 +286,33 @@
|
|||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "登錄",
|
"title": "登錄",
|
||||||
"loginButton": "登錄"
|
"loginButton": "登錄",
|
||||||
|
"username": "用戶名",
|
||||||
|
"phone": "手機號碼",
|
||||||
|
"selectCountryCode": "選擇國家/地區",
|
||||||
|
"enterPhoneNumber": "請輸入手機號碼",
|
||||||
|
"enterUsername": "請輸入用戶名",
|
||||||
|
"enterPassword": "請輸入密碼",
|
||||||
|
"enterEmail": "請輸入郵箱",
|
||||||
|
"enterCode": "請輸入驗證碼",
|
||||||
|
"enterOtp": "請輸入驗證碼",
|
||||||
|
"getCode": "獲取驗證碼",
|
||||||
|
"resendCode": "重新發送",
|
||||||
|
"sending": "發送中...",
|
||||||
|
"sendCodeSuccess": "驗證碼已發送",
|
||||||
|
"forgotPassword": "忘記密碼?",
|
||||||
|
"validation": {
|
||||||
|
"usernameRequired": "請輸入用戶名",
|
||||||
|
"passwordRequired": "請輸入密碼",
|
||||||
|
"phoneNumberRequired": "請輸入手機號碼",
|
||||||
|
"phoneNumberInvalid": "請輸入有效的手機號碼",
|
||||||
|
"codeRequired": "請輸入驗證碼",
|
||||||
|
"codeInvalid": "請輸入6位數字驗證碼",
|
||||||
|
"emailRequired": "請輸入郵箱",
|
||||||
|
"emailInvalid": "請輸入有效的郵箱地址",
|
||||||
|
"otpRequired": "請輸入驗證碼",
|
||||||
|
"otpInvalid": "請輸入6位數字驗證碼"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"signup": {
|
"signup": {
|
||||||
"title": "您的郵箱是什麼?",
|
"title": "您的郵箱是什麼?",
|
||||||
|
|||||||
14
src/router/auth.ts
Normal file
14
src/router/auth.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { RouteRecordRaw } from "vue-router";
|
||||||
|
|
||||||
|
const routes: Array<RouteRecordRaw> = [
|
||||||
|
{
|
||||||
|
path: "/auth/login",
|
||||||
|
component: () => import("@/views/auth/login/index.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/auth/signup",
|
||||||
|
component: () => import("@/auth/components/signup/index.vue"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { RouteRecordRaw } from "vue-router";
|
import type { RouteRecordRaw } from "vue-router";
|
||||||
import { createRouter, createWebHistory } from "@ionic/vue-router";
|
import { createRouter, createWebHistory } from "@ionic/vue-router";
|
||||||
|
import authRoutes from "./auth";
|
||||||
import { createRouterGuard } from "./guard";
|
import { createRouterGuard } from "./guard";
|
||||||
|
|
||||||
const routes: Array<RouteRecordRaw> = [
|
const routes: Array<RouteRecordRaw> = [
|
||||||
@@ -11,6 +12,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
path: "/:pathMatch(.*)*",
|
path: "/:pathMatch(.*)*",
|
||||||
redirect: "/layout/riwa",
|
redirect: "/layout/riwa",
|
||||||
},
|
},
|
||||||
|
...authRoutes,
|
||||||
{
|
{
|
||||||
path: "/layout",
|
path: "/layout",
|
||||||
component: () => import("@/components/layout/default.vue"),
|
component: () => import("@/components/layout/default.vue"),
|
||||||
|
|||||||
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 { t } = useI18n();
|
||||||
const model = defineModel({ type: String, default: "" });
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,18 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { authClient, modelControllerLogin, modelControllerSignup } from "@/auth";
|
import { authClient } from "@/auth";
|
||||||
|
|
||||||
const page = useTemplateRef<PageInstance>("page");
|
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const tradingViewContainer = useTemplateRef<HTMLElement>("tradingViewContainer");
|
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() {
|
async function handleLogout() {
|
||||||
await authClient.signOut();
|
await authClient.signOut();
|
||||||
}
|
}
|
||||||
@@ -32,17 +23,17 @@ useTradingView(tradingViewContainer, {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<IonPage ref="page">
|
<IonPage>
|
||||||
<IonHeader>
|
<IonHeader>
|
||||||
<IonToolbar class="ui-tabbar">
|
<IonToolbar class="ui-tabbar">
|
||||||
<IonTitle>Home</IonTitle>
|
<IonTitle>Home</IonTitle>
|
||||||
</IonToolbar>
|
</IonToolbar>
|
||||||
</IonHeader>
|
</IonHeader>
|
||||||
<IonContent :fullscreen="true">
|
<IonContent :fullscreen="true">
|
||||||
<IonButton @click="openSignin">
|
<IonButton @click="$router.push('/auth/login')">
|
||||||
Log in
|
Log in
|
||||||
</IonButton>
|
</IonButton>
|
||||||
<IonButton @click="openSignup">
|
<IonButton @click="$router.push('/auth/signup')">
|
||||||
Sign up
|
Sign up
|
||||||
</IonButton>
|
</IonButton>
|
||||||
<IonButton @click="handleLogout">
|
<IonButton @click="handleLogout">
|
||||||
|
|||||||
@@ -46,13 +46,3 @@ function handleLanguageChange(event: CustomEvent) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang='css' scoped></style>
|
<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