feat: 添加用户认证功能,优化登录和注册流程,集成表单验证和加载状态
This commit is contained in:
3
auto-imports.d.ts
vendored
3
auto-imports.d.ts
vendored
@@ -27,6 +27,7 @@ declare global {
|
|||||||
const createReusableTemplate: typeof import('@vueuse/core').createReusableTemplate
|
const createReusableTemplate: typeof import('@vueuse/core').createReusableTemplate
|
||||||
const createSharedComposable: typeof import('@vueuse/core').createSharedComposable
|
const createSharedComposable: typeof import('@vueuse/core').createSharedComposable
|
||||||
const createTemplatePromise: typeof import('@vueuse/core').createTemplatePromise
|
const createTemplatePromise: typeof import('@vueuse/core').createTemplatePromise
|
||||||
|
const createUUID: typeof import('./src/utils/helper').createUUID
|
||||||
const createUnrefFn: typeof import('@vueuse/core').createUnrefFn
|
const createUnrefFn: typeof import('@vueuse/core').createUnrefFn
|
||||||
const customRef: typeof import('vue').customRef
|
const customRef: typeof import('vue').customRef
|
||||||
const debouncedRef: typeof import('@vueuse/core').debouncedRef
|
const debouncedRef: typeof import('@vueuse/core').debouncedRef
|
||||||
@@ -216,6 +217,7 @@ declare global {
|
|||||||
const useMouseInElement: typeof import('@vueuse/core').useMouseInElement
|
const useMouseInElement: typeof import('@vueuse/core').useMouseInElement
|
||||||
const useMousePressed: typeof import('@vueuse/core').useMousePressed
|
const useMousePressed: typeof import('@vueuse/core').useMousePressed
|
||||||
const useMutationObserver: typeof import('@vueuse/core').useMutationObserver
|
const useMutationObserver: typeof import('@vueuse/core').useMutationObserver
|
||||||
|
const useNavigateToRedirect: typeof import('./src/composables/useNavigateToRedirect').useNavigateToRedirect
|
||||||
const useNavigatorLanguage: typeof import('@vueuse/core').useNavigatorLanguage
|
const useNavigatorLanguage: typeof import('@vueuse/core').useNavigatorLanguage
|
||||||
const useNetwork: typeof import('@vueuse/core').useNetwork
|
const useNetwork: typeof import('@vueuse/core').useNetwork
|
||||||
const useNow: typeof import('@vueuse/core').useNow
|
const useNow: typeof import('@vueuse/core').useNow
|
||||||
@@ -533,6 +535,7 @@ declare module 'vue' {
|
|||||||
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
|
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
|
||||||
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
|
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
|
||||||
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
|
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
|
||||||
|
readonly useNavigateToRedirect: UnwrapRef<typeof import('./src/composables/useNavigateToRedirect')['useNavigateToRedirect']>
|
||||||
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
|
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
|
||||||
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
|
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
|
||||||
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
|
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
|
||||||
|
|||||||
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -25,6 +25,7 @@ declare module 'vue' {
|
|||||||
IonLabel: typeof import('@ionic/vue')['IonLabel']
|
IonLabel: typeof import('@ionic/vue')['IonLabel']
|
||||||
IonPage: typeof import('@ionic/vue')['IonPage']
|
IonPage: typeof import('@ionic/vue')['IonPage']
|
||||||
IonRouterOutlet: typeof import('@ionic/vue')['IonRouterOutlet']
|
IonRouterOutlet: typeof import('@ionic/vue')['IonRouterOutlet']
|
||||||
|
IonSpinner: typeof import('@ionic/vue')['IonSpinner']
|
||||||
IonTabBar: typeof import('@ionic/vue')['IonTabBar']
|
IonTabBar: typeof import('@ionic/vue')['IonTabBar']
|
||||||
IonTabButton: typeof import('@ionic/vue')['IonTabButton']
|
IonTabButton: typeof import('@ionic/vue')['IonTabButton']
|
||||||
IonTabs: typeof import('@ionic/vue')['IonTabs']
|
IonTabs: typeof import('@ionic/vue')['IonTabs']
|
||||||
@@ -51,6 +52,7 @@ declare global {
|
|||||||
const IonLabel: typeof import('@ionic/vue')['IonLabel']
|
const IonLabel: typeof import('@ionic/vue')['IonLabel']
|
||||||
const IonPage: typeof import('@ionic/vue')['IonPage']
|
const IonPage: typeof import('@ionic/vue')['IonPage']
|
||||||
const IonRouterOutlet: typeof import('@ionic/vue')['IonRouterOutlet']
|
const IonRouterOutlet: typeof import('@ionic/vue')['IonRouterOutlet']
|
||||||
|
const IonSpinner: typeof import('@ionic/vue')['IonSpinner']
|
||||||
const IonTabBar: typeof import('@ionic/vue')['IonTabBar']
|
const IonTabBar: typeof import('@ionic/vue')['IonTabBar']
|
||||||
const IonTabButton: typeof import('@ionic/vue')['IonTabButton']
|
const IonTabButton: typeof import('@ionic/vue')['IonTabButton']
|
||||||
const IonTabs: typeof import('@ionic/vue')['IonTabs']
|
const IonTabs: typeof import('@ionic/vue')['IonTabs']
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"@capacitor/status-bar": "catalog:",
|
"@capacitor/status-bar": "catalog:",
|
||||||
"@capp/eden": "catalog:",
|
"@capp/eden": "catalog:",
|
||||||
"@elysiajs/eden": "catalog:",
|
"@elysiajs/eden": "catalog:",
|
||||||
|
"@faker-js/faker": "catalog:",
|
||||||
"@ionic/vue": "catalog:",
|
"@ionic/vue": "catalog:",
|
||||||
"@ionic/vue-router": "catalog:",
|
"@ionic/vue-router": "catalog:",
|
||||||
"@tailwindcss/vite": "catalog:",
|
"@tailwindcss/vite": "catalog:",
|
||||||
|
|||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -60,6 +60,9 @@ catalogs:
|
|||||||
'@elysiajs/eden':
|
'@elysiajs/eden':
|
||||||
specifier: ^1.4.6
|
specifier: ^1.4.6
|
||||||
version: 1.4.6
|
version: 1.4.6
|
||||||
|
'@faker-js/faker':
|
||||||
|
specifier: ^10.2.0
|
||||||
|
version: 10.2.0
|
||||||
'@iconify-json/bx':
|
'@iconify-json/bx':
|
||||||
specifier: ^1.2.2
|
specifier: ^1.2.2
|
||||||
version: 1.2.2
|
version: 1.2.2
|
||||||
@@ -299,6 +302,9 @@ importers:
|
|||||||
'@elysiajs/eden':
|
'@elysiajs/eden':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))
|
version: 1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))
|
||||||
|
'@faker-js/faker':
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 10.2.0
|
||||||
'@ionic/vue':
|
'@ionic/vue':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 8.7.17(@stencil/core@4.41.1)(vue-router@4.6.4(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3))
|
version: 8.7.17(@stencil/core@4.41.1)(vue-router@4.6.4(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3))
|
||||||
@@ -1632,6 +1638,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
|
'@faker-js/faker@10.2.0':
|
||||||
|
resolution: {integrity: sha512-rTXwAsIxpCqzUnZvrxVh3L0QA0NzToqWBLAhV+zDV3MIIwiQhAZHMdPCIaj5n/yADu/tyk12wIPgL6YHGXJP+g==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'}
|
||||||
|
|
||||||
'@humanfs/core@0.19.1':
|
'@humanfs/core@0.19.1':
|
||||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||||
engines: {node: '>=18.18.0'}
|
engines: {node: '>=18.18.0'}
|
||||||
@@ -7190,6 +7200,8 @@ snapshots:
|
|||||||
'@eslint/core': 0.17.0
|
'@eslint/core': 0.17.0
|
||||||
levn: 0.4.1
|
levn: 0.4.1
|
||||||
|
|
||||||
|
'@faker-js/faker@10.2.0': {}
|
||||||
|
|
||||||
'@humanfs/core@0.19.1': {}
|
'@humanfs/core@0.19.1': {}
|
||||||
|
|
||||||
'@humanfs/node@0.16.7':
|
'@humanfs/node@0.16.7':
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ catalog:
|
|||||||
'@capp/eden': http://192.168.1.2:9538/api/capp-eden-0.0.2.tgz
|
'@capp/eden': http://192.168.1.2:9538/api/capp-eden-0.0.2.tgz
|
||||||
'@cloudflare/workers-types': ^4.20260113.0
|
'@cloudflare/workers-types': ^4.20260113.0
|
||||||
'@elysiajs/eden': ^1.4.6
|
'@elysiajs/eden': ^1.4.6
|
||||||
|
'@faker-js/faker': ^10.2.0
|
||||||
'@iconify-json/bx': ^1.2.2
|
'@iconify-json/bx': ^1.2.2
|
||||||
'@iconify-json/circle-flags': ^1.2.10
|
'@iconify-json/circle-flags': ^1.2.10
|
||||||
'@iconify-json/cryptocurrency-color': ^1.2.4
|
'@iconify-json/cryptocurrency-color': ^1.2.4
|
||||||
|
|||||||
15
src/App.vue
15
src/App.vue
@@ -1,4 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { App as CapacitorApp } from "@capacitor/app";
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const { isAuthenticated } = storeToRefs(userStore);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!isAuthenticated.value)
|
||||||
|
return;
|
||||||
|
userStore.updateProfile();
|
||||||
|
CapacitorApp.addListener("appStateChange", ({ isActive }) => {
|
||||||
|
if (isActive) {
|
||||||
|
userStore.updateProfile();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
12
src/composables/useNavigateToRedirect.ts
Normal file
12
src/composables/useNavigateToRedirect.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import type { LocationQueryValue } from "vue-router";
|
||||||
|
import { router } from "@/router";
|
||||||
|
|
||||||
|
export function useNavigateToRedirect(redirect: LocationQueryValue): void;
|
||||||
|
|
||||||
|
export function useNavigateToRedirect(redirect: LocationQueryValue[], index: number): void;
|
||||||
|
|
||||||
|
export function useNavigateToRedirect(redirect: LocationQueryValue | LocationQueryValue[], index?: number) {
|
||||||
|
const _redirect = Array.isArray(redirect) ? redirect[index || 0] as string : redirect as string;
|
||||||
|
const path = decodeURIComponent(_redirect || "/");
|
||||||
|
router.replace(path);
|
||||||
|
}
|
||||||
41
src/main.ts
41
src/main.ts
@@ -4,22 +4,22 @@ import VConsole from "vconsole";
|
|||||||
import { useRegisterSW } from "virtual:pwa-register/vue";
|
import { useRegisterSW } from "virtual:pwa-register/vue";
|
||||||
import { createApp } from "vue";
|
import { createApp } from "vue";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
|
import { authClient } from "./auth";
|
||||||
import { i18n } from "./locales";
|
import { i18n } from "./locales";
|
||||||
import { router } from "./router";
|
|
||||||
|
|
||||||
|
import { router } from "./router";
|
||||||
/* Core CSS required for Ionic components to work properly */
|
/* Core CSS required for Ionic components to work properly */
|
||||||
import "@ionic/vue/css/core.css";
|
import "@ionic/vue/css/core.css";
|
||||||
/* Basic CSS for apps built with Ionic */
|
/* Basic CSS for apps built with Ionic */
|
||||||
import "@ionic/vue/css/normalize.css";
|
import "@ionic/vue/css/normalize.css";
|
||||||
import "@ionic/vue/css/structure.css";
|
|
||||||
|
|
||||||
|
import "@ionic/vue/css/structure.css";
|
||||||
import "@ionic/vue/css/typography.css";
|
import "@ionic/vue/css/typography.css";
|
||||||
/* Optional CSS utils that can be commented out */
|
/* Optional CSS utils that can be commented out */
|
||||||
import "@ionic/vue/css/padding.css";
|
import "@ionic/vue/css/padding.css";
|
||||||
import "@ionic/vue/css/float-elements.css";
|
import "@ionic/vue/css/float-elements.css";
|
||||||
import "@ionic/vue/css/text-alignment.css";
|
import "@ionic/vue/css/text-alignment.css";
|
||||||
import "@ionic/vue/css/text-transformation.css";
|
import "@ionic/vue/css/text-transformation.css";
|
||||||
import "@ionic/vue/css/flex-utils.css";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ionic Dark Mode
|
* Ionic Dark Mode
|
||||||
@@ -28,6 +28,7 @@ import "@ionic/vue/css/flex-utils.css";
|
|||||||
* https://ionicframework.com/docs/theming/dark-mode
|
* https://ionicframework.com/docs/theming/dark-mode
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import "@ionic/vue/css/flex-utils.css";
|
||||||
import "@ionic/vue/css/display.css";
|
import "@ionic/vue/css/display.css";
|
||||||
// import "@ionic/vue/css/palettes/dark.system.css";
|
// import "@ionic/vue/css/palettes/dark.system.css";
|
||||||
// import "@ionic/vue/css/palettes/dark.always.css";
|
// import "@ionic/vue/css/palettes/dark.always.css";
|
||||||
@@ -64,21 +65,25 @@ if (import.meta.env.DEV) {
|
|||||||
console.log("VConsole is enabled in development mode.");
|
console.log("VConsole is enabled in development mode.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const pinia = createPinia();
|
authClient.getSession().then((session) => {
|
||||||
|
const pinia = createPinia();
|
||||||
|
const userStore = useUserStore(pinia);
|
||||||
|
userStore.setToken(session.data?.session.token || "");
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
.use(IonicVue, {
|
.use(IonicVue, {
|
||||||
backButtonText: "返回",
|
backButtonText: "返回",
|
||||||
mode: "ios",
|
mode: "ios",
|
||||||
statusTap: true,
|
statusTap: true,
|
||||||
swipeBackEnabled: true,
|
swipeBackEnabled: true,
|
||||||
// rippleEffect: true,
|
// rippleEffect: true,
|
||||||
// animated: false,
|
// animated: false,
|
||||||
})
|
})
|
||||||
.use(pinia)
|
.use(pinia)
|
||||||
.use(router)
|
.use(router)
|
||||||
.use(i18n);
|
.use(i18n);
|
||||||
|
|
||||||
router.isReady().then(() => {
|
router.isReady().then(() => {
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
import type { Router } from "vue-router";
|
import type { RouteLocation, Router } from "vue-router";
|
||||||
|
import authRoutes from "./auth";
|
||||||
|
|
||||||
|
function isAuthRoute(route: RouteLocation) {
|
||||||
|
return authRoutes.some(r => r.path === route.path);
|
||||||
|
}
|
||||||
|
|
||||||
export function createRouterGuard(router: Router) {
|
export function createRouterGuard(router: Router) {
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
// if (to.meta.requiresAuth && !userStore.isAuthenticated) {
|
if (to.meta.requiresAuth && !userStore.isAuthenticated) {
|
||||||
// if (from.path === "/auth/login") {
|
if (isAuthRoute(from)) {
|
||||||
// return next("/");
|
return next("/");
|
||||||
// }
|
}
|
||||||
// const redirect = encodeURIComponent(to.fullPath);
|
const redirect = encodeURIComponent(to.fullPath);
|
||||||
// return next({ path: "/auth/login", query: { redirect } });
|
return next({ path: "/auth/login", query: { redirect } });
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
if (isAuthRoute(to) && userStore.isAuthenticated) {
|
||||||
|
return next("/");
|
||||||
|
}
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,65 @@
|
|||||||
<script lang='ts' setup>
|
<script lang='ts' setup>
|
||||||
|
import { toastController } from "@ionic/vue";
|
||||||
|
import { safeClient } from "@/api";
|
||||||
|
import { authClient } from "@/auth";
|
||||||
|
import { LoginSchema } from "./schema";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const phoneNumber = ref("");
|
const form = ref({
|
||||||
const password = ref("");
|
phoneNumber: "",
|
||||||
|
password: "",
|
||||||
|
});
|
||||||
const agreed = ref(false);
|
const agreed = ref(false);
|
||||||
const showPassword = ref(false);
|
const showPassword = ref(false);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
function handleLogin() {
|
async function showToast(message: string, color: "success" | "danger" | "warning" = "danger") {
|
||||||
if (!phoneNumber.value || !password.value) {
|
const toast = await toastController.create({
|
||||||
// TODO: 显示提示信息
|
message,
|
||||||
return;
|
duration: 2000,
|
||||||
}
|
position: "top",
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
await toast.present();
|
||||||
|
}
|
||||||
|
async function handleLogin() {
|
||||||
if (!agreed.value) {
|
if (!agreed.value) {
|
||||||
// TODO: 提示需要同意条款
|
await showToast("请先阅读并同意服务条款和隐私政策", "warning");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// TODO: 实现登录逻辑
|
const result = LoginSchema.safeParse({ ...form.value });
|
||||||
console.log("登录", { phoneNumber: phoneNumber.value, password: password.value });
|
if (!result.success) {
|
||||||
|
const first = result.error.issues[0];
|
||||||
|
await showToast(first.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const { data } = await safeClient(authClient.signIn.username({
|
||||||
|
username: form.value.phoneNumber,
|
||||||
|
password: form.value.password,
|
||||||
|
}));
|
||||||
|
if (!data.value?.token) {
|
||||||
|
toastController.create({
|
||||||
|
message: "登录失败,请检查手机号或密码",
|
||||||
|
duration: 2000,
|
||||||
|
color: "danger",
|
||||||
|
}).then(toast => toast.present());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
userStore.setToken(data.value.token);
|
||||||
|
await userStore.updateProfile();
|
||||||
|
await showToast("登录成功!", "success");
|
||||||
|
router.push(route.query.redirect as string || "/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSignup() {
|
function handleSignup() {
|
||||||
@@ -71,7 +113,7 @@ function goToTerms(type: "service" | "privacy") {
|
|||||||
<!-- 手机号输入 -->
|
<!-- 手机号输入 -->
|
||||||
<ion-item lines="none" class="input-item">
|
<ion-item lines="none" class="input-item">
|
||||||
<ion-input
|
<ion-input
|
||||||
v-model="phoneNumber"
|
v-model="form.phoneNumber"
|
||||||
type="tel"
|
type="tel"
|
||||||
placeholder="请输入手机号"
|
placeholder="请输入手机号"
|
||||||
class="custom-input"
|
class="custom-input"
|
||||||
@@ -82,7 +124,7 @@ function goToTerms(type: "service" | "privacy") {
|
|||||||
<!-- 密码输入 -->
|
<!-- 密码输入 -->
|
||||||
<ion-item lines="none" class="input-item">
|
<ion-item lines="none" class="input-item">
|
||||||
<ion-input
|
<ion-input
|
||||||
v-model="password"
|
v-model="form.password"
|
||||||
:type="showPassword ? 'text' : 'password'"
|
:type="showPassword ? 'text' : 'password'"
|
||||||
placeholder="请输入密码"
|
placeholder="请输入密码"
|
||||||
class="custom-input"
|
class="custom-input"
|
||||||
@@ -110,9 +152,11 @@ function goToTerms(type: "service" | "privacy") {
|
|||||||
<ion-button
|
<ion-button
|
||||||
expand="block"
|
expand="block"
|
||||||
class="login-button mt-2"
|
class="login-button mt-2"
|
||||||
|
:disabled="isLoading"
|
||||||
@click="handleLogin"
|
@click="handleLogin"
|
||||||
>
|
>
|
||||||
登录
|
<ion-spinner v-if="isLoading" name="crescent" class="mr-2" />
|
||||||
|
{{ isLoading ? '登录中...' : '登录' }}
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
|
||||||
<!-- 注册按钮 -->
|
<!-- 注册按钮 -->
|
||||||
|
|||||||
50
src/views/auth/schema.ts
Normal file
50
src/views/auth/schema.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import zod from "zod";
|
||||||
|
|
||||||
|
export const SignupSchema = zod.object({
|
||||||
|
phoneNumber: zod
|
||||||
|
.string()
|
||||||
|
.min(1, "请输入手机号")
|
||||||
|
.regex(/^1[3-9]\d{9}$/, "请输入正确的手机号码"),
|
||||||
|
|
||||||
|
password: zod
|
||||||
|
.string()
|
||||||
|
.min(6, "密码至少6位")
|
||||||
|
.max(20, "密码最多20位")
|
||||||
|
.regex(/^(?=.*[a-z])(?=.*\d).+$/i, "密码必须包含字母和数字"),
|
||||||
|
|
||||||
|
confirmPassword: zod
|
||||||
|
.string()
|
||||||
|
.min(1, "请确认密码"),
|
||||||
|
|
||||||
|
realName: zod
|
||||||
|
.string()
|
||||||
|
.min(2, "请输入真实姓名")
|
||||||
|
.max(10, "姓名长度不能超过10个字符"),
|
||||||
|
|
||||||
|
idCard: zod
|
||||||
|
.string()
|
||||||
|
.min(1, "请输入身份证号码")
|
||||||
|
.regex(
|
||||||
|
/(^\d{15}$)|(^\d{18}$)|(^\d{17}([\dX])$)/i,
|
||||||
|
"请输入正确的身份证号码",
|
||||||
|
),
|
||||||
|
|
||||||
|
inviteCode: zod
|
||||||
|
.string()
|
||||||
|
.optional(),
|
||||||
|
}).refine(data => data.password === data.confirmPassword, {
|
||||||
|
message: "两次输入的密码不一致",
|
||||||
|
path: ["confirmPassword"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const LoginSchema = zod.object({
|
||||||
|
phoneNumber: zod
|
||||||
|
.string()
|
||||||
|
.min(1, "请输入手机号")
|
||||||
|
.regex(/^1[3-9]\d{9}$/, "请输入正确的手机号码"),
|
||||||
|
|
||||||
|
password: zod
|
||||||
|
.string()
|
||||||
|
.min(6, "密码至少6位")
|
||||||
|
.max(20, "密码最多20位"),
|
||||||
|
});
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
<script lang='ts' setup>
|
<script lang='ts' setup>
|
||||||
|
import { faker } from "@faker-js/faker";
|
||||||
|
import { loadingController, toastController } from "@ionic/vue";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
import { safeClient } from "@/api";
|
||||||
|
import { authClient } from "@/auth";
|
||||||
|
import { SignupSchema } from "./schema";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
@@ -15,45 +21,62 @@ const formData = ref({
|
|||||||
const agreed = ref(false);
|
const agreed = ref(false);
|
||||||
const showPassword = ref(false);
|
const showPassword = ref(false);
|
||||||
const showConfirmPassword = ref(false);
|
const showConfirmPassword = ref(false);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
function handleSignup() {
|
async function showToast(message: string, color: "success" | "danger" | "warning" = "danger") {
|
||||||
// 验证手机号
|
const toast = await toastController.create({
|
||||||
if (!formData.value.phoneNumber) {
|
message,
|
||||||
// TODO: 显示提示信息
|
duration: 2000,
|
||||||
return;
|
position: "top",
|
||||||
}
|
color,
|
||||||
|
});
|
||||||
// 验证密码
|
await toast.present();
|
||||||
if (!formData.value.password) {
|
}
|
||||||
// TODO: 提示输入密码
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证确认密码
|
|
||||||
if (formData.value.password !== formData.value.confirmPassword) {
|
|
||||||
// TODO: 提示密码不一致
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证姓名
|
|
||||||
if (!formData.value.realName) {
|
|
||||||
// TODO: 提示输入姓名
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证身份证
|
|
||||||
if (!formData.value.idCard) {
|
|
||||||
// TODO: 提示输入身份证
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
async function handleSignup() {
|
||||||
|
// 检查是否同意协议
|
||||||
if (!agreed.value) {
|
if (!agreed.value) {
|
||||||
// TODO: 提示需要同意条款
|
await showToast("请先阅读并同意服务条款和隐私政策", "warning");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 实现注册逻辑
|
const result = SignupSchema.safeParse(formData.value);
|
||||||
console.log("注册", formData.value);
|
if (!result.success) {
|
||||||
|
const first = result.error.issues[0];
|
||||||
|
await showToast(first.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const email = faker.internet.email();
|
||||||
|
const { data } = await safeClient(authClient.signUp.email({
|
||||||
|
email,
|
||||||
|
name: formData.value.realName,
|
||||||
|
username: formData.value.phoneNumber,
|
||||||
|
password: formData.value.password,
|
||||||
|
idCard: formData.value.idCard,
|
||||||
|
inviteCode: formData.value.inviteCode || undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!data.value?.token) {
|
||||||
|
toastController.create({
|
||||||
|
message: "注册成功,但未收到令牌,请联系管理员",
|
||||||
|
duration: 2000,
|
||||||
|
color: "danger",
|
||||||
|
}).then(toast => toast.present());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await showToast("注册成功!", "success");
|
||||||
|
const userStore = useUserStore();
|
||||||
|
userStore.setToken(data.value.token);
|
||||||
|
await userStore.updateProfile();
|
||||||
|
useNavigateToRedirect(route.query.redirect as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLogin() {
|
function handleLogin() {
|
||||||
@@ -193,9 +216,11 @@ function goToTerms(type: "service" | "privacy") {
|
|||||||
<ion-button
|
<ion-button
|
||||||
expand="block"
|
expand="block"
|
||||||
class="signup-button mt-2"
|
class="signup-button mt-2"
|
||||||
|
:disabled="isLoading"
|
||||||
@click="handleSignup"
|
@click="handleSignup"
|
||||||
>
|
>
|
||||||
注册
|
<ion-spinner v-if="isLoading" name="crescent" class="mr-2" />
|
||||||
|
{{ isLoading ? '注册中...' : '注册' }}
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
|
||||||
<!-- 登录按钮 -->
|
<!-- 登录按钮 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user