效果
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) { // 携带token
config.headers.Authorization = `Bearer ${token}`
}
return config
}, error => {
console.log(error)
return Promise.reject(error)
})
service.interceptors.response.use(response => {
const { code, message } = response.data
if (code !== 0) { // 错误提示
ElMessage.error(message)
return Promise.reject(message)
}
return response.data
}, error => {
console.log('err' + error) // for debug
return 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 params
export interface IUserInfo {
username: string;
password: string;
}
// 定义state类型
export interface IUserState {
token: string;
}
// mutations类型
type IMutations = MutationTree<IUserState>
// actions类型
type IActions = ActionTree<IUserState, IRootState>
// 定义state
const state: IUserState = {
token: ''
}
// 定义mutation
const mutations: IMutations = {
SET_TOKEN(state, token: string) {
state.token = token
}
}
// 定义action
const actions: IActions = {
login({ commit }, userInfo: IUserInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
login({ username: username.trim(), password }).then(response => {
const { data } = response
console.log('data', data)
commit('SET_TOKEN', data.token)
setToken(data.token) // localStorage中保存token
resolve(data)
}).catch(error => {
reject(error)
})
})
}
}
// 定义user module
const 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-description
export 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-types
export 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-form
class="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-input
ref="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-input
ref="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-button
type="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 ref
const loginFormRef = ref<IElFormInstance | null>(null)
// form username ref
const usernameRef = ref<HTMLInputElement | null>(null)
// form password ref
const 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 = true
store.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-requires
const path = require('path')
const resolve = dir => path.join(__dirname, dir)
function chainWebpack(config) {
// set svg-sprite-loader
config.module
.rule('svg') // 在已有的svg loader配置中 排除掉对src/icons里svg进行转换
.exclude.add(resolve('src/icons'))
.end()
// symbolId的意义 https://segmentfault.com/a/1190000015367490
config.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