效果
3-1 环境变量配置
vue3-elemetn-admin 根目录下创建环境变量文件,生产环境这里只是写个假的
3-2 创建api
封装request.ts
src/api/config/request.ts
import axios from 'axios'import { getToken } from '../../utils/auth'import { ElMessage } from 'element-plus'const service = axios.create({baseURL: process.env.VUE_APP_BASE_API,timeout: 100000})service.interceptors.request.use(config => {const token = getToken()if (token) { // 携带tokenconfig.headers.Authorization = `Bearer ${token}`}return config}, error => {console.log(error)return Promise.reject(error)})service.interceptors.response.use(response => {const { code, message } = response.dataif (code !== 0) { // 错误提示ElMessage.error(message)return Promise.reject(message)}return response.data}, error => {console.log('err' + error) // for debugreturn Promise.reject(error)})export default service
封装响应类型
src/api/type.ts
export interface ApiResponse<T = any> {code: number;data: T;message?: string;}
创建user api
src/api/user.ts
import request from '@/api/config/request'import { ApiResponse } from './type'interface UserLoginData {username: string;password: string;}interface LoginResponseData {token: string;}export const login = (data: UserLoginData): Promise<ApiResponse<LoginResponseData>> => {return request.post('/auth/login',data)}
3-3 store中创建user module
添加user module

src/store/modules/user.ts
import { Module, MutationTree, ActionTree } from 'vuex'import { IRootState } from '@/store'import { login } from '@/api/user'import { setToken } from '@/utils/auth'// login paramsexport interface IUserInfo {username: string;password: string;}// 定义state类型export interface IUserState {token: string;}// mutations类型type IMutations = MutationTree<IUserState>// actions类型type IActions = ActionTree<IUserState, IRootState>// 定义stateconst state: IUserState = {token: ''}// 定义mutationconst mutations: IMutations = {SET_TOKEN(state, token: string) {state.token = token}}// 定义actionconst actions: IActions = {login({ commit }, userInfo: IUserInfo) {const { username, password } = userInforeturn new Promise((resolve, reject) => {login({ username: username.trim(), password }).then(response => {const { data } = responseconsole.log('data', data)commit('SET_TOKEN', data.token)setToken(data.token) // localStorage中保存tokenresolve(data)}).catch(error => {reject(error)})})}}// 定义user moduleconst user: Module<IUserState, IRootState> = {namespaced: true,state,mutations,actions}export default user
修改store/index.ts

src/store/index.ts
import { InjectionKey } from 'vue'import { createStore, Store, useStore as baseUseStore } from 'vuex'import createPersistedState from 'vuex-persistedstate'import app, { IAppState } from '@/store/modules/app'import tagsView, { ITagsViewState } from '@/store/modules/tagsView'import settings, { ISettingsState } from '@/store/modules/settings'import user, { IUserState } from '@/store/modules/user'import getters from './getters'// 模块声明在根状态下export interface IRootState {app: IAppState;user: IUserState;tagsView: ITagsViewState;settings: ISettingsState;}// 通过下面方式使用 TypeScript 定义 store 能正确地为 store 提供类型声明。// https://next.vuex.vuejs.org/guide/typescript-support.html#simplifying-usestore-usage// eslint-disable-next-line symbol-descriptionexport const key: InjectionKey<Store<IRootState>> = Symbol()// 对于getters在组件使用时没有类型提示// 有人提交了pr #1896 为getters创建泛型 应该还未发布// https://github.com/vuejs/vuex/pull/1896// 代码pr内容详情// https://github.com/vuejs/vuex/pull/1896/files#diff-093ad82a25aee498b11febf1cdcb6546e4d223ffcb49ed69cc275ac27ce0ccce// vuex store持久化 默认使用localstorage持久化const persisteAppState = createPersistedState({storage: window.sessionStorage, // 指定storage 也可自定义key: 'vuex_app', // 存储名 默认都是vuex 多个模块需要指定 否则会覆盖// paths: ['app'] // 针对app这个模块持久化// 只针对app模块下sidebar.opened状态持久化paths: ['app.sidebar.opened', 'app.size'] // 通过点连接符指定state路径})const persisteSettingsState = createPersistedState({storage: window.sessionStorage, // 指定storage 也可自定义key: 'vuex_setting', // 存储名 默认都是vuex 多个模块需要指定 否则会覆盖// paths: ['app'] // 针对app这个模块持久化// 只针对app模块下sidebar.opened状态持久化paths: ['settings.theme', 'settings.originalStyle', 'settings.tagsView', 'settings.sidebarLogo'] // 通过点连接符指定state路径})export default createStore<IRootState>({plugins: [persisteAppState,persisteSettingsState],getters,modules: {app,user,tagsView,settings}})// 定义自己的 `useStore` 组合式函数// https://next.vuex.vuejs.org/zh/guide/typescript-support.html#%E7%AE%80%E5%8C%96-usestore-%E7%94%A8%E6%B3%95// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-typesexport function useStore () {return baseUseStore(key)}// vuex持久化 vuex-persistedstate文档说明// https://www.npmjs.com/package/vuex-persistedstate
utils里封装token 存储操作方法
src/utils/auth.ts
const tokenKey = 'V3-Admin-Token'export const getToken = (): string | null => {return localStorage.getItem(tokenKey)}export const setToken = (token: string): void => {return localStorage.setItem(tokenKey, token)}export const removeToken = (): void => {return localStorage.removeItem(tokenKey)}
3-4 接入登录逻辑

src/views/login/index.vue
<template><div class="login-container"><el-formclass="login-form":model="loginForm":rules="loginRules"ref="loginFormRef"><div class="admin-logo"><img class="logo" src="../../assets/logo.png" alt="logo"><h1 class="name">Vue3 Admin</h1></div><el-form-item prop="username"><span class="svg-container"><svg-icon icon-class="user"></svg-icon></span><el-inputref="usernameRef"placeholder="请输入用户名"v-model="loginForm.username"autocomplete="off"tabindex="1"/></el-form-item><el-form-item prop="password"><span class="svg-container"><svg-icon icon-class="password"></svg-icon></span><el-inputref="passwordRef":class="{'no-autofill-pwd': passwordType === 'password'}"placeholder="请输入密码"v-model="loginForm.password"type="text"autocomplete="off"tabindex="2"/><span class="show-pwd" @click="showPwd"><svg-icon:icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" /></span></el-form-item><!-- 登录按钮 --><el-buttontype="primary"style=" width: 100%; margin-bottom: 30px":loading="loading"@click="handleLogin">Login</el-button></el-form></div></template><script lang="ts">import { defineComponent, ref, reactive, toRefs, onMounted } from 'vue'import { ElForm } from 'element-plus'import { useRouter } from 'vue-router'import { useStore } from '@/store'type IElFormInstance = InstanceType<typeof ElForm>export default defineComponent({name: 'Login',setup() {const store = useStore()const router = useRouter()const loading = ref(false) // 登录加载状态// form refconst loginFormRef = ref<IElFormInstance | null>(null)// form username refconst usernameRef = ref<HTMLInputElement | null>(null)// form password refconst passwordRef = ref<HTMLInputElement | null>(null)const loginState = reactive({loginForm: {username: '',password: ''},loginRules: {username: [{required: true,trigger: 'blur',message: '请输入用户名!'}],password: [{required: true,trigger: 'blur',message: '请输入密码!'}]},passwordType: 'password'})// 显示密码const showPwd = () => {loginState.passwordType = loginState.passwordType === 'password' ? 'text' : 'password'}// 登录const handleLogin = () => {console.log('login');(loginFormRef.value as IElFormInstance).validate((valid) => {if (valid) {loading.value = truestore.dispatch('user/login', loginState.loginForm).then(() => {router.push({path: '/'})}).finally(() => {loading.value = false})} else {console.log('error submit!!')}})}// 自动获取焦点onMounted(() => {if (loginState.loginForm.username === '') {(usernameRef.value as HTMLInputElement).focus()} else if (loginState.loginForm.password === '') {(passwordRef.value as HTMLInputElement).focus()}})return {loading,loginFormRef,handleLogin,showPwd,usernameRef,passwordRef,...toRefs(loginState)}}})</script><style lang="scss">$bg:#283443;$light_gray:#fff;$cursor: #fff;.login-container {.el-form-item {border: 1px solid #dcdee2;border-radius: 5px;.el-input {display: inline-block;height: 40px;width: 85%;input {background: transparent;border: 0;-webkit-appearance: none;border-radius: 0px;padding: 12px 5px 12px 15px;height: 40px;}}}.no-autofill-pwd { // 解决自动填充问题.el-input__inner { // 模仿密码框原点-webkit-text-security: disc !important;}}}</style><style lang="scss" scoped>$bg:#2d3a4b;$dark_gray:#889aa4;$light_gray:#eee;.login-container {min-height: 100%;width: 100%;overflow: hidden;background-image: url('../../assets/body.svg');background-repeat: no-repeat;background-position: 50%;background-size: 100%;.login-form {position: relative;width: 500px;max-width: 100%;margin: 0 auto;padding: 140px 35px 0;overflow: hidden;box-sizing: border-box;.svg-container {padding: 0 10px;}.show-pwd {font-size: 16px;cursor: pointer;margin-left: 7px;}.admin-logo {display: flex;align-items: center;justify-content: center;margin-bottom: 20px;.logo {width: 60px;height: 60px;}.name {font-weight: normal;margin-left: 10px;}}}}</style>
3-5 设置代理

vue.config.js
'use strict'// eslint-disable-next-line @typescript-eslint/no-var-requiresconst path = require('path')const resolve = dir => path.join(__dirname, dir)function chainWebpack(config) {// set svg-sprite-loaderconfig.module.rule('svg') // 在已有的svg loader配置中 排除掉对src/icons里svg进行转换.exclude.add(resolve('src/icons')).end()// symbolId的意义 https://segmentfault.com/a/1190000015367490config.module.rule('icons').test(/\.svg$/).include.add(resolve('src/icons')).end().use('svg-sprite-loader').loader('svg-sprite-loader')// 设置symbolId名称格式 use元素通过symbolId寻找svg图标// <svg>// <use xlink:href="#symbolId"></use>// </svg>.options({symbolId: 'icon-[name]'}).end()}module.exports = {chainWebpack,devServer: {port: 8080,open: true,overlay: {warnings: false,errors: true},proxy: {'/dev-api': {target: 'http://localhost:3003',ws: true,changeOrigin: true,pathRewrite: { '^/dev-api': '/api' }}}}}
本节参考源码
源码里方法写错了
localStorage.getItem 写成了 localStorage.get 文档里是对的
https://gitee.com/brolly/vue3-element-admin/commit/e5b4627ea60a50579fde410e343d029820e318b8


