Stories

Detail Return Return

Vue 3 + TypeScript + Element Plus + Vite 4.3:實現一個優雅的登錄註冊功能 - Stories Detail

先附上源碼地址:
覺得不錯的話順手一個star

效果展示

最新vite搭建項目

npm create vite@latest mingsl-login -- --template vue-ts

配置tsconfig

tsconfig.node.json

{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}
​

tsconfig.json

{
  "compilerOptions": {
    "target": "ESNext", // 將代碼編譯為最新版本的 JS
    "useDefineForClassFields": true, // 是 TypeScript 3.7.0 中新增的一個編譯選項
    "module": "ESNext", // 使用 ES Module 格式打包編譯後的文件
    "moduleResolution": "Node", // 使用 Node 的模塊解析策略
    "strict": true, // 啓用所用嚴格的類型檢查
    "jsx": "preserve", // 保留原始的 JSX 代碼,不進行編譯
    "resolveJsonModule": true, // 允許引入 JSON 文件
    "isolatedModules": true, // 該屬性要求所有文件都是 ES Module 模塊。
    "esModuleInterop": true, // 允許使用 import 引入使用 export = 導出的內容
    "lib": ["ESNext", "DOM"], // 引入 ES 最新特性和 DOM 接口的類型定義
    "skipLibCheck": true, // 跳過對 .d.ts 文件的類型檢查
    "noEmit": true // 不輸出文件,即編譯後不會生成任何js文件
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  "references": [{ "path": "./tsconfig.node.json" }] 
}
​

配置文件系統路徑別名

安裝 @types/node

npm install @types/node

修改vite.config.ts

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
​
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve:{
    alias:{
      '@':resolve(__dirname,'src')
    }
  }
})
​

修改tsconfig.json

{
  "compilerOptions": {
     //...
    "baseUrl": ".", //查詢的基礎路徑
    "paths": { "@/*": ["src/*"]} //路徑映射,配合別名使用
  }
     //...
}

安裝 element plus

npm install element-plus --save

配置Volar 插件支持

// tsconfig.json
{
  "compilerOptions": {
    // ...
    "types": ["element-plus/global"]
  }
}

自動導入

首先你需要安裝unplugin-vue-componentsunplugin-auto-import這兩款插件`

npm install -D unplugin-vue-components unplugin-auto-import

然後把下列代碼插入到你的 Vite配置文件中`

// vite.config.ts
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
​
export default defineConfig({
  // ...
  plugins: [
    // ...
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
})

全局引入css樣式

//main.ts
import 'element-plus/dist/index.css'

安裝 icon 圖庫

安裝

npm install @element-plus/icons-vue

全局註冊

// main.ts
​
// 如果您正在使用CDN引入,請刪除下面一行。
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
​
const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}
​
app.mount('#app')

定義 LoginReq 接口

// @/interface/user.ts
/**
 * 登錄表單提交請求數據類型
 */
export interface LoginReq{
    username:string;
    password:string;
}
​

自定義loginForm 組件

<!-- @/compaments/loginForm.vue -->
<template>
    <ElForm class="login-form" ref="loginRef" :model="loginParam" :rules="loginRules">
        <h1 class="login-title">登錄</h1>
        <ElFormItem prop="username">
            <ElInput placeholder="請輸入賬號" :prefix-icon="User" v-model="loginParam.username" size="large"></ElInput>
        </ElFormItem>
        <ElFormItem prop="password">
            <ElInput placeholder="請輸入密碼" show-password :prefix-icon="Lock" v-model="loginParam.password" size="large"></ElInput>
        </ElFormItem>
        <ElFormItem>
            <ElButton type="primary" class="login-btn" size="large" @click="submit(loginRef)">登錄</ElButton>
        </ElFormItem>
    </ElForm>
</template>
​
<script setup lang="ts">
import { User ,Lock} from '@element-plus/icons-vue';
import { LoginReq } from '@/interface/user'; 
import {reactive,ref} from 'vue'
import { ElMessage, FormInstance,FormRules } from 'element-plus';
const loginParam:LoginReq=reactive({
    username:"",
    password:""
})
​
const loginRef=ref<FormInstance>()
const loginRules:FormRules=reactive({
    username:[{required:true,message:"賬號不能為空",trigger:'blur'}],
    password:[{required:true,message:"密碼不能為空",trigger:'blur'}]
})
const submit=(formEl: FormInstance | undefined)=>{
    if(!formEl){
        return false
    }
    formEl.validate(async(validate:boolean)=>{
        if(validate){
            console.log("開始做登錄的邏輯");            
        }else{
            return false;
        }
    })
}
</script>
​
<style scoped>
.login-form{
    grid-column: 1;
    grid-row: 1;
    opacity: 1;
    transition: 1s ease-in-out;
    transition-delay: 0.5s;
    /* 上下 | 左右 */
    padding: 1% 25%;
    z-index: 1;
}
.login-form.sign-up-model{
    opacity: 0;
    transition: 1s ease-in-out;
    z-index: 0;
}
.login-title{
    text-align: center;
    color:#444;
}
.login-btn{
    width: 100%;
    font-size: 18px;
}
</style>
​

定義 RegisterReq 接口

// @/interface/user.ts
/**
 * 註冊表單提交請求數據類型
 */
export interface RegisterReq{
    username:string;
    password:string;
    email:string
}
​

自定義registerForm 組件

<!-- @/compaments/registerForm.vue -->
<template>
    <ElForm class="register-form" ref="registerRef" :model="registerParam" :rules="registerRules">
        <h1 class="register-title">註冊</h1>
        <ElFormItem prop="username">
            <ElInput placeholder="請輸入賬號" :prefix-icon="User" v-model="registerParam.username" size="large"></ElInput>
        </ElFormItem>
        <ElFormItem prop="password">
            <ElInput placeholder="請輸入密碼" show-password :prefix-icon="Lock" v-model="registerParam.password" size="large"></ElInput>
        </ElFormItem>
        <ElFormItem prop="email">
            <ElInput placeholder="請輸入郵箱" :prefix-icon="Message" v-model="registerParam.email" size="large"></ElInput>
        </ElFormItem>
        <ElFormItem>
            <ElButton type="primary" class="register-btn" size="large" @click="submit(registerRef)">註冊</ElButton>
        </ElFormItem>
    </ElForm>
</template>
​
<script setup lang="ts">
import { User, Lock, Message } from '@element-plus/icons-vue';
import { RegisterReq } from '@/interface/user';
import {reactive,ref} from 'vue'
import { ElMessage, FormInstance,FormRules } from 'element-plus';
const registerParam:RegisterReq=reactive({
    username:"",
    password:"",
    email:""
})
const registerRef=ref<FormInstance>()
const registerRules:FormRules=reactive({
    username:[{required:true,message:"賬號不能為空",trigger:'blur'}],
    password:[{required:true,message:"密碼不能為空",trigger:'blur'}
    ,{required:true,message:"密碼是6~20位",min:6,max:20 ,trigger:'blur'}],
    email:[{required:true,message:"郵箱不能為空",trigger:'blur'}]
})
const submit=(formEl: FormInstance | undefined)=>{
    if(!formEl){
        return false
    }
    formEl.validate(async (validate:boolean)=>{
        if(validate){
            console.log("開始做註冊的邏輯");
        }else{
            return false;
        }
    })
    }
</script>
​
<style scoped>
.register-form {
    grid-row: 1;
    grid-column: 1;
    opacity: 0;
    transition: 1s ease-in-out;
    /* 上下 | 左右 */
    padding: 1% 25%;
    z-index: 0;
}
​
.register-form.sign-up-model {
    opacity: 1;
    transition: 1s ease-in-out;
    transition-delay: 0.5s;
    z-index: 1;
}
​
.register-title{
    text-align: center;
    color: #444;
}
.register-btn{
    width: 100%;
    font-size: 18px;
}
</style>
​

去掉頁面內邊距佔滿屏幕

<!-- index.html  -->
  <style>
    body{
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
  </style>

配置 app.vue 組件實現效果

<template>
    <div :class="{ container: true, 'sign-up-model': vari }">
        <div class="inner-left-container">
            <div class="login-content">
                <h1 style="color: white;">明思梨教育</h1>
                <ElButton type="primary" @click="onClick" size="large">去註冊</ElButton>
            </div>
            <img src="@/assets/login-bg.svg" class="image">
        </div>
        <div class="inner-right-container">
            <div class="register-content">
                <h1 style="color: white;">明思梨教育</h1>
                <ElButton type="primary" @click="onClick" size="large">去登錄</ElButton>
            </div>
            <img src="@/assets/register-bg.svg" class="image">
        </div>
        <div class="inner-sign-up-container">
            <login :class="{ 'sign-up-model': vari }"></login>
            <register :class="{ 'sign-up-model': vari }"></register>
        </div>
    </div>
</template>
​
<script setup lang="ts">
import { ref } from 'vue'
import login from '@/components/login/loginForm.vue'
import register from '@/components/login/registerForm.vue'
const onClick = () => {
    vari.value = !vari.value
}
let vari = ref<boolean>(false)
</script>
<style scoped>
.container {
    width: 100vw;
    height: 100vh;
    background-color: white;
    overflow: hidden;
    position: relative;
    display: flex;
    flex-direction: row;
}
.container::before {
    content: "";
    width: 2000px;
    height: 2000px;
    background-color: rgb(160, 209, 35);
    position: absolute;
    border-radius: 50%;
    transform: translateY(-50%);
    right: 48%;
    top: -10%;
    transition: 1.8s ease-in-out;
    z-index: 2;
}
.inner-left-container {
    width: 0;
    flex: 1;
    z-index: 2;
    display: flex;
    flex-direction: column;
    justify-content: space-around;
    align-items: flex-end;
    /* 上邊|右邊|下邊|左邊 */
    padding: 3rem 10% 2rem 10%;
    pointer-events: all;
​
}
.inner-right-container {
    width: 0;
    flex: 1;
    z-index: 2;
    display: flex;
    flex-direction: column;
    justify-content: space-around;
    align-items: flex-start;
    /* 上邊|右邊|下邊|左邊 */
    padding: 3rem 10% 2rem 10%;
    pointer-events: none;
}
.container .inner-right-container .register-content,
.container .inner-right-container .image {
    transform: translateX(1000px);
    transition: 1s ease-in-out;
    transition-delay: 0.5s;
}
.container .inner-left-container .login-content,
.container .inner-left-container .image {
    transform: translateX(0px);
    transition: 1s ease-in-out;
    transition-delay: 0.5s;
​
}
.image {
    width: 100%;
}
.inner-sign-up-container {
    width: 50%;
    height: 50%;
    position: absolute;
    right: 0;
    top: 20%;
    transition: 1s ease-in-out;
    transition-delay: 0.5s;
    display: grid;
    grid-template-columns: 1fr;
}
​
/* 動畫 */
.container.sign-up-model::before {
    transform: translate(100%, -50%);
    transition: 1.8s ease-in-out;
    right: 52%;
}
​
.container.sign-up-model .inner-right-container .register-content,
.container.sign-up-model .inner-right-container .image {
    transform: translateX(0px);
    transition: 1s ease-in-out;
    transition-delay: 0.5s;
}
​
.container.sign-up-model .inner-left-container .login-content,
.container.sign-up-model .inner-left-container .image {
    transform: translateX(-1000px);
    transition: 1s ease-in-out;
    transition-delay: 0.5s;
}
​
.container.sign-up-model .inner-sign-up-container {
    width: 50%;
    height: 50%;
    top: 20%;
    right: 50%;
    transition: 1s ease-in-out;
    transition-delay: 0.5s;
}
​
.container.sign-up-model .inner-right-container {
    pointer-events: all;
}
​
.container.sign-up-model .inner-left-container {
    pointer-events: none;
}
​
</style>
​

Add a new Comments

Some HTML is okay.