feat(login): 实现登录注册组件及流程

- 添加 Login.vue 组件,包含用户名、密码输入及登录、注册、取消功能
- 添加 LoginProcess.vue 组件,管理登录注册流程切换
- 添加 Register.vue 组件占位
- 配置 axios 请求拦截器及环境变量类型定义
- 更新 App.vue 登录流程,替换原有 passkey 登录逻辑
- 添加 slide-up 动画样式并移除旧动画定义
- 更新依赖 tdesign-mobile-vue 为 tdesign-vue-next
- 配置开发环境与生产环境 BASE_URL
- 调整布局样式,优化登录界面显示效果
This commit is contained in:
Grand-cocoa 2025-11-10 14:45:43 +08:00
parent 5abf344572
commit 0c7aaa848d
16 changed files with 144 additions and 4775 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
BASE_URL=https://animo.alina-dace.info/api

1
.env.development Normal file
View File

@ -0,0 +1 @@
BASE_URL=http://localhost:8080/

10
auto-imports.d.ts vendored
View File

@ -1,10 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
}

25
components.d.ts vendored
View File

@ -1,25 +0,0 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
Card: typeof import('./src/components/Card.vue')['default']
IconCommunity: typeof import('./src/components/icons/IconCommunity.vue')['default']
IconDocumentation: typeof import('./src/components/icons/IconDocumentation.vue')['default']
IconEcosystem: typeof import('./src/components/icons/IconEcosystem.vue')['default']
IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default']
IconTooling: typeof import('./src/components/icons/IconTooling.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
TAvatar: typeof import('tdesign-mobile-vue')['Avatar']
TButton: typeof import('tdesign-mobile-vue')['Button']
}
}

4682
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,12 +15,13 @@
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"axios": "^1.13.2",
"gsap": "^3.13.0", "gsap": "^3.13.0",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"sass-loader": "^16.0.6", "sass-loader": "^16.0.6",
"scss": "^0.2.4", "scss": "^0.2.4",
"tdesign-icons-vue-next": "^0.4.1", "tdesign-icons-vue-next": "^0.4.1",
"tdesign-mobile-vue": "^1.11.0", "tdesign-vue-next": "^1.17.2",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-router": "^4.6.3" "vue-router": "^4.6.3"
}, },

View File

@ -7,7 +7,7 @@
<div class="login-info"> <div class="login-info">
<transition name="slide-up" mode="out-in"> <transition name="slide-up" mode="out-in">
<div v-if="!login"> <div v-if="!login">
<t-button :disabled="!passkey" @click="handleLogin">登录</t-button> <LoginProcess @login="handleLogin"/>
</div> </div>
<div v-else-if="!weCome"> <div v-else-if="!weCome">
<transition name="slide-up" mode="out-in"> <transition name="slide-up" mode="out-in">
@ -34,6 +34,7 @@
import { RouterView } from 'vue-router' import { RouterView } from 'vue-router'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import avatar from '@/assets/logo.svg' import avatar from '@/assets/logo.svg'
import LoginProcess from '@/components/login/LoginProcess.vue'
const login = ref(false) const login = ref(false)
const weCome = ref(false) const weCome = ref(false)
@ -48,33 +49,34 @@ if (!navigator.credentials) {
} }
function handleLogin(){ function handleLogin(){
navigator.credentials.create({ // navigator.credentials.create({
publicKey: { // publicKey: {
challenge: new Uint8Array(32), // challenge: new Uint8Array(32),
timeout: 60000, // timeout: 60000,
rp: { // rp: {
id: window.location.host, // id: window.location.host,
name: "Animo" // name: "Animo"
}, // },
user: { // user: {
id: new Uint8Array(32), // id: new Uint8Array(32),
name: "Amico", // name: "Amico",
displayName: "Amico", // displayName: "Amico",
}, // },
pubKeyCredParams: [{ // pubKeyCredParams: [{
alg: -7, type: "public-key" // alg: -7, type: "public-key"
},{ // },{
alg: -257, type: "public-key" // alg: -257, type: "public-key"
}], // }],
excludeCredentials: [], // excludeCredentials: [],
authenticatorSelection: { // authenticatorSelection: {
authenticatorAttachment: "platform", // authenticatorAttachment: "platform",
requireResidentKey: true, // requireResidentKey: true,
} // }
}, // },
}).then(resp => { // }).then(resp => {
// login.value = true
// })
login.value = true login.value = true
})
} }
// //
@ -112,7 +114,9 @@ header {
padding: 2vh; padding: 2vh;
transition: all 0.5s ease-out; transition: all 0.5s ease-out;
.logo { .logo {
display: block; flex: 1;
display: flex;
align-items: flex-end;
margin: 0 calc(50vw - 62.5px); margin: 0 calc(50vw - 62.5px);
transition: all 0.3s ease-out; transition: all 0.3s ease-out;
position: relative; position: relative;
@ -121,6 +125,7 @@ header {
} }
} }
.login-info { .login-info {
flex: 1;
margin: 1rem; margin: 1rem;
line-height: 2rem; line-height: 2rem;
height: 2rem; height: 2rem;
@ -149,6 +154,7 @@ header {
justify-content: start; justify-content: start;
height: 10vh; height: 10vh;
.logo { .logo {
flex: none;
height: 6vh; height: 6vh;
width: 6vh; width: 6vh;
margin: 0 0; margin: 0 0;
@ -210,29 +216,4 @@ nav a:first-of-type {
margin-top: 1rem; margin-top: 1rem;
} }
} }
.slide-up-enter-active,
.slide-up-leave-active {
transition: all 0.25s ease-out;
}
.slide-up-enter-from {
opacity: 0;
transform: translateX(30px);
}
.slide-up-leave-to {
opacity: 0;
transform: translateX(-30px);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style> </style>

View File

@ -19,3 +19,18 @@ a,
background-color: hsla(160, 100%, 37%, 0.2); background-color: hsla(160, 100%, 37%, 0.2);
} }
} }
.slide-up-enter-active,
.slide-up-leave-active {
transition: all 0.25s ease-out;
}
.slide-up-enter-from {
opacity: 0;
transform: translateX(30px);
}
.slide-up-leave-to {
opacity: 0;
transform: translateX(-30px);
}

View File

@ -0,0 +1,32 @@
<template>
<div>
<t-input v-model="username" placeholder="请输入用户名"></t-input>
<t-input v-model="password" placeholder="请输入密码"></t-input>
<t-link @click="handleRegister">没有账号去注册</t-link>
<t-button @click="handleLogin">登录</t-button>
<t-button @click="handleUsePasskey">取消</t-button>
</div>
</template>
<script setup lang="ts">
import { reactive, toRefs } from 'vue'
const { username, password } = toRefs(
reactive({
username: '',
password: '',
}),
)
const emits = defineEmits(['complete', 'register'])
function handleLogin() {
emits('complete')
}
function handleUsePasskey() {
emits('complete')
}
function handleRegister() {
emits('register')
}
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,27 @@
<template>
<div>
<transition name="slide-up">
<Login v-if="step === 'login'" @complete="handleLogin" @register="handleRegister"></Login>
<Register v-else-if="step === 'register'" @complete="handleLogin"></Register>
</transition>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Login from '@/components/login/Login.vue'
import Register from '@/components/login/Register.vue'
const emits = defineEmits(['login'])
const step = ref('login')
function handleLogin() {
emits('login')
}
function handleRegister() {
step.value = 'register'
}
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,5 @@
<template></template>
<script setup lang="ts"></script>
<style scoped lang="scss"></style>

View File

@ -1,5 +1,5 @@
import './assets/main.css' import './assets/main.css'
import 'tdesign-mobile-vue/es/style/index.css'; import 'tdesign-vue-next/es/style/index.css'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'

15
src/request/axios.ts Normal file
View File

@ -0,0 +1,15 @@
import axios, { type InternalAxiosRequestConfig } from 'axios'
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8';
const request = axios.create({
timeout: 10000,
baseURL: import.meta.env.BASE_URL
})
request.interceptors.request.use((config: InternalAxiosRequestConfig) => {
return Promise.reject()
})
export default request

8
src/types/env.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
// 环境变量
interface ImportMetaEnv {
BASE_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
// readonly glob: any;
}

View File

@ -9,7 +9,7 @@
} }
], ],
"include": [ "include": [
"node_modules/tdesign-mobile-vue/global.d.ts", "node_modules/tdesign-vue-next/global.d.ts",
"src/types/**/*.d.ts" "src/types/**/*.d.ts"
] ]
} }

View File

@ -14,12 +14,12 @@ export default defineConfig({
vueDevTools(), vueDevTools(),
AutoImport({ AutoImport({
resolvers: [TDesignResolver({ resolvers: [TDesignResolver({
library: 'mobile-vue' library: 'vue-next'
})], })],
}), }),
Components({ Components({
resolvers: [TDesignResolver({ resolvers: [TDesignResolver({
library: 'mobile-vue' library: 'vue-next'
})], })],
}), }),
], ],