效果

image.png
登录成功后
image.png
token会存储在localStorage里
image.png

3-1 环境变量配置

vue3-elemetn-admin 根目录下创建环境变量文件,生产环境这里只是写个假的
image.png

3-2 创建api

image.png

封装request.ts

src/api/config/request.ts

  1. import axios from 'axios'
  2. import { getToken } from '../../utils/auth'
  3. import { ElMessage } from 'element-plus'
  4. const service = axios.create({
  5. baseURL: process.env.VUE_APP_BASE_API,
  6. timeout: 100000
  7. })
  8. service.interceptors.request.use(config => {
  9. const token = getToken()
  10. if (token) { // 携带token
  11. config.headers.Authorization = `Bearer ${token}`
  12. }
  13. return config
  14. }, error => {
  15. console.log(error)
  16. return Promise.reject(error)
  17. })
  18. service.interceptors.response.use(response => {
  19. const { code, message } = response.data
  20. if (code !== 0) { // 错误提示
  21. ElMessage.error(message)
  22. return Promise.reject(message)
  23. }
  24. return response.data
  25. }, error => {
  26. console.log('err' + error) // for debug
  27. return Promise.reject(error)
  28. })
  29. export default service

封装响应类型

src/api/type.ts

  1. export interface ApiResponse<T = any> {
  2. code: number;
  3. data: T;
  4. message?: string;
  5. }

创建user api

src/api/user.ts

  1. import request from '@/api/config/request'
  2. import { ApiResponse } from './type'
  3. interface UserLoginData {
  4. username: string;
  5. password: string;
  6. }
  7. interface LoginResponseData {
  8. token: string;
  9. }
  10. export const login = (data: UserLoginData): Promise<ApiResponse<LoginResponseData>> => {
  11. return request.post(
  12. '/auth/login',
  13. data
  14. )
  15. }

3-3 store中创建user module

添加user module

image.png
src/store/modules/user.ts

  1. import { Module, MutationTree, ActionTree } from 'vuex'
  2. import { IRootState } from '@/store'
  3. import { login } from '@/api/user'
  4. import { setToken } from '@/utils/auth'
  5. // login params
  6. export interface IUserInfo {
  7. username: string;
  8. password: string;
  9. }
  10. // 定义state类型
  11. export interface IUserState {
  12. token: string;
  13. }
  14. // mutations类型
  15. type IMutations = MutationTree<IUserState>
  16. // actions类型
  17. type IActions = ActionTree<IUserState, IRootState>
  18. // 定义state
  19. const state: IUserState = {
  20. token: ''
  21. }
  22. // 定义mutation
  23. const mutations: IMutations = {
  24. SET_TOKEN(state, token: string) {
  25. state.token = token
  26. }
  27. }
  28. // 定义action
  29. const actions: IActions = {
  30. login({ commit }, userInfo: IUserInfo) {
  31. const { username, password } = userInfo
  32. return new Promise((resolve, reject) => {
  33. login({ username: username.trim(), password }).then(response => {
  34. const { data } = response
  35. console.log('data', data)
  36. commit('SET_TOKEN', data.token)
  37. setToken(data.token) // localStorage中保存token
  38. resolve(data)
  39. }).catch(error => {
  40. reject(error)
  41. })
  42. })
  43. }
  44. }
  45. // 定义user module
  46. const user: Module<IUserState, IRootState> = {
  47. namespaced: true,
  48. state,
  49. mutations,
  50. actions
  51. }
  52. export default user

修改store/index.ts

image.png
src/store/index.ts

  1. import { InjectionKey } from 'vue'
  2. import { createStore, Store, useStore as baseUseStore } from 'vuex'
  3. import createPersistedState from 'vuex-persistedstate'
  4. import app, { IAppState } from '@/store/modules/app'
  5. import tagsView, { ITagsViewState } from '@/store/modules/tagsView'
  6. import settings, { ISettingsState } from '@/store/modules/settings'
  7. import user, { IUserState } from '@/store/modules/user'
  8. import getters from './getters'
  9. // 模块声明在根状态下
  10. export interface IRootState {
  11. app: IAppState;
  12. user: IUserState;
  13. tagsView: ITagsViewState;
  14. settings: ISettingsState;
  15. }
  16. // 通过下面方式使用 TypeScript 定义 store 能正确地为 store 提供类型声明。
  17. // https://next.vuex.vuejs.org/guide/typescript-support.html#simplifying-usestore-usage
  18. // eslint-disable-next-line symbol-description
  19. export const key: InjectionKey<Store<IRootState>> = Symbol()
  20. // 对于getters在组件使用时没有类型提示
  21. // 有人提交了pr #1896 为getters创建泛型 应该还未发布
  22. // https://github.com/vuejs/vuex/pull/1896
  23. // 代码pr内容详情
  24. // https://github.com/vuejs/vuex/pull/1896/files#diff-093ad82a25aee498b11febf1cdcb6546e4d223ffcb49ed69cc275ac27ce0ccce
  25. // vuex store持久化 默认使用localstorage持久化
  26. const persisteAppState = createPersistedState({
  27. storage: window.sessionStorage, // 指定storage 也可自定义
  28. key: 'vuex_app', // 存储名 默认都是vuex 多个模块需要指定 否则会覆盖
  29. // paths: ['app'] // 针对app这个模块持久化
  30. // 只针对app模块下sidebar.opened状态持久化
  31. paths: ['app.sidebar.opened', 'app.size'] // 通过点连接符指定state路径
  32. })
  33. const persisteSettingsState = createPersistedState({
  34. storage: window.sessionStorage, // 指定storage 也可自定义
  35. key: 'vuex_setting', // 存储名 默认都是vuex 多个模块需要指定 否则会覆盖
  36. // paths: ['app'] // 针对app这个模块持久化
  37. // 只针对app模块下sidebar.opened状态持久化
  38. paths: ['settings.theme', 'settings.originalStyle', 'settings.tagsView', 'settings.sidebarLogo'] // 通过点连接符指定state路径
  39. })
  40. export default createStore<IRootState>({
  41. plugins: [
  42. persisteAppState,
  43. persisteSettingsState
  44. ],
  45. getters,
  46. modules: {
  47. app,
  48. user,
  49. tagsView,
  50. settings
  51. }
  52. })
  53. // 定义自己的 `useStore` 组合式函数
  54. // https://next.vuex.vuejs.org/zh/guide/typescript-support.html#%E7%AE%80%E5%8C%96-usestore-%E7%94%A8%E6%B3%95
  55. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  56. export function useStore () {
  57. return baseUseStore(key)
  58. }
  59. // vuex持久化 vuex-persistedstate文档说明
  60. // https://www.npmjs.com/package/vuex-persistedstate

utils里封装token 存储操作方法

src/utils/auth.ts

  1. const tokenKey = 'V3-Admin-Token'
  2. export const getToken = (): string | null => {
  3. return localStorage.getItem(tokenKey)
  4. }
  5. export const setToken = (token: string): void => {
  6. return localStorage.setItem(tokenKey, token)
  7. }
  8. export const removeToken = (): void => {
  9. return localStorage.removeItem(tokenKey)
  10. }

3-4 接入登录逻辑

image.png
src/views/login/index.vue

  1. <template>
  2. <div class="login-container">
  3. <el-form
  4. class="login-form"
  5. :model="loginForm"
  6. :rules="loginRules"
  7. ref="loginFormRef"
  8. >
  9. <div class="admin-logo">
  10. <img class="logo" src="../../assets/logo.png" alt="logo">
  11. <h1 class="name">Vue3 Admin</h1>
  12. </div>
  13. <el-form-item prop="username">
  14. <span class="svg-container">
  15. <svg-icon icon-class="user"></svg-icon>
  16. </span>
  17. <el-input
  18. ref="usernameRef"
  19. placeholder="请输入用户名"
  20. v-model="loginForm.username"
  21. autocomplete="off"
  22. tabindex="1"
  23. />
  24. </el-form-item>
  25. <el-form-item prop="password">
  26. <span class="svg-container">
  27. <svg-icon icon-class="password"></svg-icon>
  28. </span>
  29. <el-input
  30. ref="passwordRef"
  31. :class="{
  32. 'no-autofill-pwd': passwordType === 'password'
  33. }"
  34. placeholder="请输入密码"
  35. v-model="loginForm.password"
  36. type="text"
  37. autocomplete="off"
  38. tabindex="2"
  39. />
  40. <span class="show-pwd" @click="showPwd">
  41. <svg-icon
  42. :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
  43. </span>
  44. </el-form-item>
  45. <!-- 登录按钮 -->
  46. <el-button
  47. type="primary"
  48. style=" width: 100%; margin-bottom: 30px"
  49. :loading="loading"
  50. @click="handleLogin"
  51. >Login</el-button>
  52. </el-form>
  53. </div>
  54. </template>
  55. <script lang="ts">
  56. import { defineComponent, ref, reactive, toRefs, onMounted } from 'vue'
  57. import { ElForm } from 'element-plus'
  58. import { useRouter } from 'vue-router'
  59. import { useStore } from '@/store'
  60. type IElFormInstance = InstanceType<typeof ElForm>
  61. export default defineComponent({
  62. name: 'Login',
  63. setup() {
  64. const store = useStore()
  65. const router = useRouter()
  66. const loading = ref(false) // 登录加载状态
  67. // form ref
  68. const loginFormRef = ref<IElFormInstance | null>(null)
  69. // form username ref
  70. const usernameRef = ref<HTMLInputElement | null>(null)
  71. // form password ref
  72. const passwordRef = ref<HTMLInputElement | null>(null)
  73. const loginState = reactive({
  74. loginForm: {
  75. username: '',
  76. password: ''
  77. },
  78. loginRules: {
  79. username: [
  80. {
  81. required: true,
  82. trigger: 'blur',
  83. message: '请输入用户名!'
  84. }
  85. ],
  86. password: [
  87. {
  88. required: true,
  89. trigger: 'blur',
  90. message: '请输入密码!'
  91. }
  92. ]
  93. },
  94. passwordType: 'password'
  95. })
  96. // 显示密码
  97. const showPwd = () => {
  98. loginState.passwordType = loginState.passwordType === 'password' ? 'text' : 'password'
  99. }
  100. // 登录
  101. const handleLogin = () => {
  102. console.log('login')
  103. ;(loginFormRef.value as IElFormInstance).validate((valid) => {
  104. if (valid) {
  105. loading.value = true
  106. store.dispatch('user/login', loginState.loginForm).then(() => {
  107. router.push({
  108. path: '/'
  109. })
  110. }).finally(() => {
  111. loading.value = false
  112. })
  113. } else {
  114. console.log('error submit!!')
  115. }
  116. })
  117. }
  118. // 自动获取焦点
  119. onMounted(() => {
  120. if (loginState.loginForm.username === '') {
  121. (usernameRef.value as HTMLInputElement).focus()
  122. } else if (loginState.loginForm.password === '') {
  123. (passwordRef.value as HTMLInputElement).focus()
  124. }
  125. })
  126. return {
  127. loading,
  128. loginFormRef,
  129. handleLogin,
  130. showPwd,
  131. usernameRef,
  132. passwordRef,
  133. ...toRefs(loginState)
  134. }
  135. }
  136. })
  137. </script>
  138. <style lang="scss">
  139. $bg:#283443;
  140. $light_gray:#fff;
  141. $cursor: #fff;
  142. .login-container {
  143. .el-form-item {
  144. border: 1px solid #dcdee2;
  145. border-radius: 5px;
  146. .el-input {
  147. display: inline-block;
  148. height: 40px;
  149. width: 85%;
  150. input {
  151. background: transparent;
  152. border: 0;
  153. -webkit-appearance: none;
  154. border-radius: 0px;
  155. padding: 12px 5px 12px 15px;
  156. height: 40px;
  157. }
  158. }
  159. }
  160. .no-autofill-pwd { // 解决自动填充问题
  161. .el-input__inner { // 模仿密码框原点
  162. -webkit-text-security: disc !important;
  163. }
  164. }
  165. }
  166. </style>
  167. <style lang="scss" scoped>
  168. $bg:#2d3a4b;
  169. $dark_gray:#889aa4;
  170. $light_gray:#eee;
  171. .login-container {
  172. min-height: 100%;
  173. width: 100%;
  174. overflow: hidden;
  175. background-image: url('../../assets/body.svg');
  176. background-repeat: no-repeat;
  177. background-position: 50%;
  178. background-size: 100%;
  179. .login-form {
  180. position: relative;
  181. width: 500px;
  182. max-width: 100%;
  183. margin: 0 auto;
  184. padding: 140px 35px 0;
  185. overflow: hidden;
  186. box-sizing: border-box;
  187. .svg-container {
  188. padding: 0 10px;
  189. }
  190. .show-pwd {
  191. font-size: 16px;
  192. cursor: pointer;
  193. margin-left: 7px;
  194. }
  195. .admin-logo {
  196. display: flex;
  197. align-items: center;
  198. justify-content: center;
  199. margin-bottom: 20px;
  200. .logo {
  201. width: 60px;
  202. height: 60px;
  203. }
  204. .name {
  205. font-weight: normal;
  206. margin-left: 10px;
  207. }
  208. }
  209. }
  210. }
  211. </style>

3-5 设置代理

image.png
vue.config.js

  1. 'use strict'
  2. // eslint-disable-next-line @typescript-eslint/no-var-requires
  3. const path = require('path')
  4. const resolve = dir => path.join(__dirname, dir)
  5. function chainWebpack(config) {
  6. // set svg-sprite-loader
  7. config.module
  8. .rule('svg') // 在已有的svg loader配置中 排除掉对src/icons里svg进行转换
  9. .exclude.add(resolve('src/icons'))
  10. .end()
  11. // symbolId的意义 https://segmentfault.com/a/1190000015367490
  12. config.module
  13. .rule('icons')
  14. .test(/\.svg$/)
  15. .include.add(resolve('src/icons'))
  16. .end()
  17. .use('svg-sprite-loader')
  18. .loader('svg-sprite-loader')
  19. // 设置symbolId名称格式 use元素通过symbolId寻找svg图标
  20. // <svg>
  21. // <use xlink:href="#symbolId"></use>
  22. // </svg>
  23. .options({
  24. symbolId: 'icon-[name]'
  25. })
  26. .end()
  27. }
  28. module.exports = {
  29. chainWebpack,
  30. devServer: {
  31. port: 8080,
  32. open: true,
  33. overlay: {
  34. warnings: false,
  35. errors: true
  36. },
  37. proxy: {
  38. '/dev-api': {
  39. target: 'http://localhost:3003',
  40. ws: true,
  41. changeOrigin: true,
  42. pathRewrite: { '^/dev-api': '/api' }
  43. }
  44. }
  45. }
  46. }

本节参考源码

源码里方法写错了
localStorage.getItem 写成了 localStorage.get 文档里是对的
image.png
https://gitee.com/brolly/vue3-element-admin/commit/e5b4627ea60a50579fde410e343d029820e318b8