从零开始搭Vue项目-03 登录

264 阅读3分钟

登录

文件结构 image-20220710235841610.png

创建loginPanel

login-panel

<template>
  <div>
		loginPanel
  </div>
</template>

<script setup lang="ts">

</script>

<style scoped>

</style>

创建login account和login register

login-acount

<template>
  <div>
		loginAccount
  </div>
</template>

<script setup lang="ts">

</script>

<style scoped>

</style>

register同上

配置login.vue文件,引入loginPanel

login

<template>
  <div class="login">
    <loginPanel />
  </div>
</template>

<script setup lang="ts">
import loginPanel from './compontents/login-panel.vue'
</script>

<style scoped lang="less">
.login {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  background-color: rgba(230, 230, 230, 1);
}
</style>

login-panel

loginPanel引入element-Plus panel组件

loginPanel主要内容有选择卡片 以及三个按钮

image-20220711000126130.png

<template>
  <div class="login_panel">
    <h1 class="title"></h1>
    <el-tabs type="border-card" stretch class="tabs">
      <el-tab-pane>
        <template #label>
          <span class="tabs-label">
            <el-icon><Coordinate /></el-icon>
            <span>登录</span>
          </span>
        </template>
        <login-account />
      </el-tab-pane>
      <el-tab-pane>
        <template #label>
          <span class="tabs-label">
            <el-icon><EditPen /></el-icon>
            <span>注册</span>
          </span>
        </template>
        <LoginRegister />
      </el-tab-pane>
    </el-tabs>
    <div class="account-control">
      <el-checkbox>记住密码</el-checkbox>
      <el-link type="primary">忘记密码</el-link>
    </div>

    <el-button type="primary" class="login-btn">
      立即登录
    </el-button>
  </div>
</template>

<script setup lang="ts">
import { Coordinate, EditPen } from '@element-plus/icons-vue'
import LoginAccount from './login-account.vue'
import LoginRegister from './login-register.vue'
</script>

<style scoped>
.login_panel {
  margin-bottom: 150px;
  width: 320px;
}
.title {
  text-align: center;
}

.account-control {
  margin-top: 10px;
  display: flex;
  justify-content: space-between;
}

.login-btn {
  width: 100%;
  margin-top: 10px;
}
</style>

创建登录界面

配置rules

  • 创建 input 验证的规则

account-config'

// 编写好规则
export const rules = {
  name: [
    {
      required: true,
      message: '用户名是必传内容~',
      trigger: 'blur'
    },
    {
      pattern: /^[a-z0-9]{5,10}$/,
      message: '用户名必须是5~10个字母或者数字~',
      trigger: 'blur'
    }
  ],
  password: [
    {
      required: true,
      message: '密码是必传内容~',
      trigger: 'blur'
    },
    {
      pattern: /^[a-z0-9]{3,}$/,
      message: '用户名必须是3位以上的字母或者数字~',
      trigger: 'blur'
    }
  ]
}

引入rules动态绑定在el-form处

引入vue中的ref对象,获取到组件实例,在实例中取到输入框的值是否符合规范

login-acount

<template>
  <div class="login-account">
    <el-form label-width="60px" :rules="rules" :model="account" ref="formRef">
      <el-form-item label="账号" prop="name">
        <el-input v-model="account.name" />
      </el-form-item>
      <el-form-item label="密码" prop="password">
        <el-input v-model="account.password" show-password />
      </el-form-item>
    </el-form>
  </div>
</template>

<script setup lang="ts">
import { defineExpose, reactive, ref } from 'vue'
import { ElForm } from 'element-plus'
import { rules } from '../config/account-config'

// 初始化数据
const account = reactive({
  name: '',
  password: ''
})
</script>

<style scoped></style>

响应panel点击事件

在account组件中传出一个函数,可以被panel获取到,当登录按钮按下的时候,触发事件

login-acount

<template>
  <div class="login-account">
    <el-form label-width="60px" :rules="rules" :model="account" ref="formRef">
      <el-form-item label="账号" prop="name">
        <el-input v-model="account.name" />
      </el-form-item>
      <el-form-item label="密码" prop="password">
        <el-input v-model="account.password" show-password />
      </el-form-item>
    </el-form>
  </div>
</template>

<script setup lang="ts">
import { defineExpose, reactive, ref } from 'vue'
import { ElForm } from 'element-plus'
// 工具类
import localCache from '@/utils/cache'

// 初始化数据
const account = reactive({
  name: localCache.getCache('name') ?? '',
  password: crypt.decryptByDES(localCache.getCache('password')) ?? ''
})

// 获取输入框对象
const formRef = ref<InstanceType<typeof ElForm>>()

const loginAction = () => {
  formRef.value?.validate((valid) => {
    // 判断输入框内数据是否符合规范
    if (valid) {
      console.log("我被调用啦")
      console.log(account)
    }
  })
}
// setup语法糖需要对导出的函数进行声明
defineExpose({
  // 导出函数
  loginAction
})
</script>

<style scoped></style>

定义ref对象 , 并在代码中绑定获取子组件

响应点击事件

login-panel

<template>
  <div class="login_panel">
    <h1 class="title"></h1>
    <el-tabs type="border-card" stretch class="tabs">
      <el-tab-pane>
        <template #label>
          <span class="tabs-label">
            <el-icon><Coordinate /></el-icon>
            <span>登录</span>
          </span>
        </template>
        <login-account ref="accountRef" />
      </el-tab-pane>
      <el-tab-pane>
        <template #label>
          <span class="tabs-label">
            <el-icon><EditPen /></el-icon>
            <span>注册</span>
          </span>
        </template>
        <LoginRegister />
      </el-tab-pane>
    </el-tabs>
    <div class="account-control">
      <el-checkbox v-model="isKeepPassword">记住密码</el-checkbox>
      <el-link type="primary">忘记密码</el-link>
    </div>

    <el-button type="primary" class="login-btn" @click="handleLoginClick">
      立即登录
    </el-button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Coordinate, EditPen } from '@element-plus/icons-vue'
import LoginAccount from './login-account.vue'
import LoginRegister from './login-register.vue'

const accountRef = ref<InstanceType<typeof LoginAccount>>()
// 调用子函数响应事件
const handleLoginClick = () => {
  accountRef.value?.loginAction()
}
</script>

<style scoped>
  略...
</style>

将账号密码存储在本地

定义isKeepPassword类型,获取选项内容

传入到 loginAction 函数中

login-panel

<template>
  <div class="login_panel">
    <h1 class="title"></h1>
    <el-tabs type="border-card" stretch class="tabs">
      <el-tab-pane>
        <template #label>
          <span class="tabs-label">
            <el-icon><Coordinate /></el-icon>
            <span>登录</span>
          </span>
        </template>
        <login-account ref="accountRef" />
      </el-tab-pane>
      <el-tab-pane>
        <template #label>
          <span class="tabs-label">
            <el-icon><EditPen /></el-icon>
            <span>注册</span>
          </span>
        </template>
        <LoginRegister />
      </el-tab-pane>
    </el-tabs>
    <div class="account-control">
      <el-checkbox v-model="isKeepPassword">记住密码</el-checkbox>
      <el-link type="primary">忘记密码</el-link>
    </div>

    <el-button type="primary" class="login-btn" @click="handleLoginClick">
      立即登录
    </el-button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Coordinate, EditPen } from '@element-plus/icons-vue'
import LoginAccount from './login-account.vue'
import LoginRegister from './login-register.vue'

const isKeepPassword = ref(true)
const accountRef = ref<InstanceType<typeof LoginAccount>>()
// 调用子函数响应事件
const handleLoginClick = () => {
  accountRef.value?.loginAction(isKeepPassword.value)
}
</script>

<style scoped>
  略...
</style>

判断是否需要存储账号密码

将密码存入到localstorage中(封装了cache工具)

login-acount

<template>
  <div class="login-account">
    <el-form label-width="60px" :rules="rules" :model="account" ref="formRef">
      <el-form-item label="账号" prop="name">
        <el-input v-model="account.name" />
      </el-form-item>
      <el-form-item label="密码" prop="password">
        <el-input v-model="account.password" show-password />
      </el-form-item>
    </el-form>
  </div>
</template>

<script setup lang="ts">
import { defineExpose, reactive, ref } from 'vue'
import { ElForm } from 'element-plus'
// 工具类
import localCache from '@/utils/cache'

import { rules } from '../config/account-config'

// 初始化数据
const account = reactive({
  name: localCache.getCache('name') ?? '',
  password: localCache.getCache('password') ?? ''
})

// 获取输入框对象
const formRef = ref<InstanceType<typeof ElForm>>()

const loginAction = (isKeepPassword: boolean) => {
  formRef.value?.validate((valid) => {
    // 判断输入框内数据是否符合规范
    if (valid) {
      // 1.判断是否需要记住密码
      if (isKeepPassword) {
        // TODO 将密码缓存在本地缓存中, 不安全需要升级
        localCache.setCache('name', account.name)
        localCache.setCache('password', account.password)
      } else {
        localCache.deleteCache('name')
        localCache.deleteCache('password')
      }
    }
  })
}
// setup语法糖需要对导出的函数进行声明
defineExpose({
  loginAction
})
</script>

<style scoped></style>

image-20220711002200692.png

utils路径

cache

// 封装的本地存储方法
class LocalCache {
  setCache(key: string, value: any) {
    window.localStorage.setItem(key, JSON.stringify(value))
  }

  getCache(key: string) {
    // obj => string => obj
    const value = window.localStorage.getItem(key)
    if (value) {
      return JSON.parse(value)
    }
  }

  deleteCache(key: string) {
    window.localStorage.removeItem(key)
  }

  clearCache() {
    window.localStorage.clear()
  }
}

export default new LocalCache()

由于密码在locastorage中是以明文的形式存储,我们应该对密码进行加密处理

安装crypot

npm install crypto-js --save-dev

创建crypyt工具类

crypyt

//DES 加密
import CryptoJS from 'crypto-js'
const normolKey = '88888888'
class crypto {
  // 加密
  encryptByDES(message: string, key = normolKey) {
    const keyHex = CryptoJS.enc.Utf8.parse(key)
    const encrypted = CryptoJS.DES.encrypt(message, keyHex, {
      mode: CryptoJS.mode.ECB,
      padding: CryptoJS.pad.Pkcs7
    })
    return encrypted.ciphertext.toString()
  }
  // 解密
  decryptByDES(ciphertext: string, key = normolKey) {
    const keyHex = CryptoJS.enc.Utf8.parse(key)
    const decrypted = CryptoJS.DES.decrypt(
      {
        ciphertext: CryptoJS.enc.Hex.parse(ciphertext)
      },
      keyHex,
      {
        mode: CryptoJS.mode.ECB,
        padding: CryptoJS.pad.Pkcs7
      }
    )
    const result_value = decrypted.toString(CryptoJS.enc.Utf8)
    return result_value
  }
}

export default new crypto()

login-acount

在存储的时候加密在获取的时候解密

<template>
  <div class="login-account">
    <el-form label-width="60px" :rules="rules" :model="account" ref="formRef">
      <el-form-item label="账号" prop="name">
        <el-input v-model="account.name" />
      </el-form-item>
      <el-form-item label="密码" prop="password">
        <el-input v-model="account.password" show-password />
      </el-form-item>
    </el-form>
  </div>
</template>

<script setup lang="ts">
import { defineExpose, reactive, ref } from 'vue'
import { ElForm } from 'element-plus'
// 工具类
import localCache from '@/utils/cache'
import crypt from '@/utils/crypt'

import { rules } from '../config/account-config'


// 初始化数据
const account = reactive({
  name: localCache.getCache('name') ?? '',
  password: crypt.decryptByDES(localCache.getCache('password')) ?? ''
})

// 获取输入框对象
const formRef = ref<InstanceType<typeof ElForm>>()

const loginAction = (isKeepPassword: boolean) => {
  formRef.value?.validate((valid) => {
    // 判断输入框内数据是否符合规范
    if (valid) {
      // 1.判断是否需要记住密码
      if (isKeepPassword) {
        // TODO 将密码缓存在本地缓存中, 不安全需要升级
        localCache.setCache('name', account.name)
        localCache.setCache('password', crypt.encryptByDES(account.password))
      } else {
        localCache.deleteCache('name')
        localCache.deleteCache('password')
      }
    }
  })
}
// setup语法糖需要对导出的函数进行声明
defineExpose({
  loginAction
})
</script>

<style scoped></style>

通过vuex进行身份验证

store目录结构

image-20220711012032069.png

store/index

// store.ts
import { createStore } from 'vuex'
import { rootState } from './types'
import login from './login/login'

export const store = createStore<rootState>({
  state: {
    name: 'codermjjh',
    token: ''
  },
  getters: {},
  mutations: {},
  actions: {},
  modules: {
    login
  }
})

store/type

// 为 store state 声明类型
export interface rootState {
  name: string
  token: string
}

store/login/login

创建login中的module对象,声明action函数

import { Module } from 'vuex'

import { ILoginState } from './types'

import { rootState } from '../types'

const loginModule: Module<ILoginState, rootState> = {
  namespaced: true,
  state: {
    token: '',
    userInfo: {}
  },
  getters: {},
  mutations: {},
  actions: {
    // 登录
    accountLoginAction({ commit }, payload: any) {
      console.log('accountLoginAction', payload)
    },
    // 注册
    registerAction({ commit }, payload: any) {
      console.log('registerAction', payload)
    }
  }
}

export default loginModule

store/login/type

export interface ILoginState {
  token: string
  userInfo: any
}

login-panel

需要在login-panel中对登录或者是注册的请求进行筛选

<template>
  <div class="login_panel">
    <h1 class="title"></h1>
    <el-tabs type="border-card" stretch class="tabs" v-model="currentTab">
      <el-tab-pane name="account">
        <template #label>
          <span class="tabs-label">
            <el-icon><Coordinate /></el-icon>
            <span>登录</span>
          </span>
        </template>
        <login-account ref="accountRef" />
      </el-tab-pane>
      <el-tab-pane name="register">
        <template #label>
          <span class="tabs-label">
            <el-icon><EditPen /></el-icon>
            <span>注册</span>
          </span>
        </template>
        <login-register ref="registerRef" />
      </el-tab-pane>
    </el-tabs>
    <div class="account-control">
      <el-checkbox v-model="isKeepPassword">记住密码</el-checkbox>
      <el-link type="primary">忘记密码</el-link>
    </div>

    <el-button type="primary" class="login-btn" @click="handleLoginClick">
      立即登录
    </el-button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Coordinate, EditPen } from '@element-plus/icons-vue'
import LoginAccount from './login-account.vue'
import LoginRegister from './login-register.vue'

const isKeepPassword = ref(true)
const accountRef = ref<InstanceType<typeof LoginAccount>>()
const registerRef = ref<InstanceType<typeof LoginRegister>>()
const currentTab = ref('account')
// 调用子函数响应事件
const handleLoginClick = () => {
  if (currentTab.value === 'account') {
    accountRef.value?.loginAction(isKeepPassword.value)
  } else {
    // 调用注册逻辑
    console.log('注册', currentTab.value)
    registerRef.value?.registerAction()
  }
}
</script>

<style scoped>
.login_panel {
  margin-bottom: 150px;
  width: 320px;
}
.title {
  text-align: center;
}

.account-control {
  margin-top: 10px;
  display: flex;
  justify-content: space-between;
}

.login-btn {
  width: 100%;
  margin-top: 10px;
}
</style>

导入store,并且调用对应函数

login-account

<template>
  <div class="login-account">
    <el-form label-width="60px" :rules="rules" :model="account" ref="formRef">
      <el-form-item label="账号" prop="name">
        <el-input v-model="account.name" />
      </el-form-item>
      <el-form-item label="密码" prop="password">
        <el-input v-model="account.password" show-password />
      </el-form-item>
    </el-form>
  </div>
</template>

<script setup lang="ts">
import { defineExpose, reactive, ref } from 'vue'
import { useStore } from 'vuex'
import { ElForm } from 'element-plus'
// 工具类
import localCache from '@/utils/cache'
import crypt from '@/utils/crypt'

import { rules } from '../config/account-config'

const store = useStore()

// 初始化数据
const account = reactive({
  name: localCache.getCache('name') ?? '',
  password: crypt.decryptByDES(localCache.getCache('password')) ?? ''
})

// 获取输入框对象
const formRef = ref<InstanceType<typeof ElForm>>()

const loginAction = (isKeepPassword: boolean) => {
  formRef.value?.validate((valid) => {
    // 判断输入框内数据是否符合规范
    if (valid) {
      // 1.判断是否需要记住密码
      if (isKeepPassword) {
        // TODO 将密码缓存在本地缓存中, 不安全需要升级
        localCache.setCache('name', account.name)
        localCache.setCache('password', crypt.encryptByDES(account.password))
      } else {
        localCache.deleteCache('name')
        localCache.deleteCache('password')
      }

      // 2.开始进行登录验证
      // 调用stroe中的方法,将account中的数据结构后传入
      store.dispatch('login/accountLoginAction', { ...account })
    }
  })
}
// setup语法糖需要对导出的函数进行声明
defineExpose({
  loginAction
})
</script>

<style scoped></style>

login-register

<template>
  <div class="login-register">
    <el-form label-width="80px" :rules="rules" :model="account" ref="formRef">
      <el-form-item label="账号" prop="name">
        <el-input v-model="account.name" />
      </el-form-item>
      <el-form-item label="密码" prop="password">
        <el-input v-model="account.password" show-password />
      </el-form-item>
      <el-form-item label="确认密码" prop="password">
        <el-input v-model="account.passwordagain" show-password />
      </el-form-item>
    </el-form>
  </div>
</template>

<script setup lang="ts">
import { defineExpose, ref, reactive } from 'vue'
import { useStore } from 'vuex'
import { ElForm } from 'element-plus'

import { rules } from '../config/account-config'
const store = useStore()

const account = reactive({
  name: '',
  password: '',
  passwordagain: ''
})

const formRef = ref<InstanceType<typeof ElForm>>()

const registerAction = () => {
  formRef.value?.validate((valid) => {
    if (valid && account.password == account.passwordagain) {
      console.log('我被注册啦', account)
      store.dispatch('login/registerAction', { ...account })
    } else {
      console.log('两次密码需要一致')
    }
  })
}
defineExpose({
  registerAction
})
</script>

<style scoped></style>

配置默认路径以及解决跨域问题

vue.config.js

添加devServer属性

devServer: {
  proxy: {
    '^/api': {
      target: 'http://152.136.185.210:5000',
        pathRewrite: {
          '^/api': ''
        },
          changeOrigin: true
    }
  }
},

创建service/login工具类

image-20220711022153177.png

service/logintype

export interface IAccount {
  name: string
  password: string
}

export interface ILoginResult {
  id: number
  name: string
  token: string
}

export interface IDataType<T = any> {
  code: number
  data: T
}

service/login/login

import mjRequest from '../index'

import { IAccount, IDataType, ILoginResult } from './type'

enum LoginAPI {
  AccountLogin = '/login',
  LoginUserInfo = '/users/', // 用法: /users/1
  UserMenus = '/role/' // 用法: role/1/menu
}

export function accountLoginRequest(account: IAccount) {
  return mjRequest.post<IDataType<ILoginResult>>({
    url: LoginAPI.AccountLogin,
    data: account
  })
}

export function requestUserInfoById(id: number) {
  return mjRequest.get<IDataType>({
    url: LoginAPI.LoginUserInfo + id,
    showLoading: false
  })
}

export function requestUserMenusByRoleId(id: number) {
  return mjRequest.get<IDataType>({
    url: LoginAPI.UserMenus + id + '/menu',
    showLoading: false
  })
}

调用工具类获取数据

store/login/login

import { Module } from 'vuex'
import { ILoginState } from './types'
import { rootState } from '../types'
import {
  accountLoginRequest,
  requestUserInfoById,
  requestUserMenusByRoleId
} from '@/service/login/login'
import localCache from '@/utils/cache'
import router from '@/router'
import { IAccount } from '@/service/login/type'
const loginModule: Module<ILoginState, rootState> = {
  namespaced: true,
  state: {
    token: '',
    userInfo: {},
    userMenus: []
  },
  getters: {},
  mutations: {
    changeToken(state, token: string) {
      state.token = token
    },
    changeUserInfo(state, userInfo: any) {
      state.userInfo = userInfo
    },
    changeUserMenus(state, userMenus: any) {
      state.userMenus = userMenus
    }
  },
  actions: {
    // 登录
    async accountLoginAction({ commit }, payload: IAccount) {
      // 1.实现登录逻辑
      const loginResult = await accountLoginRequest(payload)
      const { id, token } = loginResult.data
      commit('changeToken', token)
      localCache.setCache('token', token)

      // 2.请求用户信息
      const userInfoResult = await requestUserInfoById(id)
      const userInfo = userInfoResult.data
      commit('changeUserInfo', userInfo)
      localCache.setCache('userInfo', userInfo)

      // 3.请求用户菜单
      const userMenusResult = await requestUserMenusByRoleId(userInfo.role.id)
      const userMenus = userMenusResult.data
      commit('changeUserMenus', userMenus)
      localCache.setCache('userMenus', userMenus)

      // 4.跳到首页
      router.push('/main')
    },
    // 注册
    registerAction({ commit }, payload: any) {
      console.log('registerAction', payload)
    }
  }
}

export default loginModule

store/login/login

export interface ILoginState {
  token: string
  userInfo: any
  userMenus: any
}

配置Token

service/index.ts

requestInterceptor: (config) => {
  // console.log('请求成功的拦截')
  const token = localCache.getCache('token')
  if (token) {
    config.headers ? (config.headers.Authorization = `Bearer ${token}`) : ''
  }
  return config
},