效果

无token情况下 访问dashboard
image.png

刷新跳转到登录页,url上redirect携带的是之前访问过的路径 之前query也未丢失
image.png

登录成功后
image.png
登录成功后 重定向回 dashboard url参数依然存在 (url参数都是 useRouteQuery hook功劳 在下面有实现)

4-1 安装nProgress进度条

  1. npm install --save nprogress
  2. # ts
  3. npm install -D @types/nprogress

4-2 添加路由验证

创建permission.ts

添加路由导航钩子,进行登录验证

src/permission.ts

  1. import router from '@/router'
  2. import nProgress from 'nprogress'
  3. import 'nprogress/nprogress.css' // progress bar style
  4. import { getToken } from './utils/auth'
  5. nProgress.configure({ showSpinner: false })
  6. const whiteList = ['/login'] // 白名单
  7. router.beforeEach((to) => {
  8. nProgress.start()
  9. const hasToken = getToken()
  10. if (hasToken) { // 有token代表已登录
  11. if (to.path === '/login') {
  12. nProgress.done()
  13. return {
  14. path: '/',
  15. replace: true
  16. }
  17. }
  18. nProgress.done()
  19. return true
  20. } else {
  21. if (whiteList.includes(to.path)) {
  22. nProgress.done()
  23. return true
  24. }
  25. nProgress.done()
  26. return {
  27. path: '/login',
  28. query: {
  29. redirect: to.path,
  30. ...to.query
  31. }
  32. }
  33. }
  34. })
  35. router.afterEach(() => {
  36. nProgress.done()
  37. })

4-3 登录后跳转逻辑修改

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. import useRouteQuery from './hooks/useRouteQuery'
  61. type IElFormInstance = InstanceType<typeof ElForm>
  62. export default defineComponent({
  63. name: 'Login',
  64. setup() {
  65. const store = useStore()
  66. const router = useRouter()
  67. const loading = ref(false) // 登录加载状态
  68. // form ref
  69. const loginFormRef = ref<IElFormInstance | null>(null)
  70. // form username ref
  71. const usernameRef = ref<HTMLInputElement | null>(null)
  72. // form password ref
  73. const passwordRef = ref<HTMLInputElement | null>(null)
  74. const loginState = reactive({
  75. loginForm: {
  76. username: '',
  77. password: ''
  78. },
  79. loginRules: {
  80. username: [
  81. {
  82. required: true,
  83. trigger: 'blur',
  84. message: '请输入用户名!'
  85. }
  86. ],
  87. password: [
  88. {
  89. required: true,
  90. trigger: 'blur',
  91. message: '请输入密码!'
  92. }
  93. ]
  94. },
  95. passwordType: 'password'
  96. })
  97. // 显示密码
  98. const showPwd = () => {
  99. loginState.passwordType = loginState.passwordType === 'password' ? 'text' : 'password'
  100. }
  101. // 重定向router query处理
  102. const { redirect, otherQuery } = useRouteQuery()
  103. // 登录
  104. const handleLogin = () => {
  105. (loginFormRef.value as IElFormInstance).validate((valid) => {
  106. if (valid) {
  107. loading.value = true
  108. store.dispatch('user/login', loginState.loginForm).then(() => {
  109. // 登录成功后跳转之前被访问页或首页
  110. router.push({
  111. path: redirect.value || '/',
  112. query: otherQuery.value
  113. })
  114. }).finally(() => {
  115. loading.value = false
  116. })
  117. } else {
  118. console.log('error submit!!')
  119. }
  120. })
  121. }
  122. // 自动获取焦点
  123. onMounted(() => {
  124. if (loginState.loginForm.username === '') {
  125. (usernameRef.value as HTMLInputElement).focus()
  126. } else if (loginState.loginForm.password === '') {
  127. (passwordRef.value as HTMLInputElement).focus()
  128. }
  129. })
  130. return {
  131. loading,
  132. loginFormRef,
  133. handleLogin,
  134. showPwd,
  135. usernameRef,
  136. passwordRef,
  137. ...toRefs(loginState)
  138. }
  139. }
  140. })
  141. </script>
  142. <style lang="scss">
  143. $bg:#283443;
  144. $light_gray:#fff;
  145. $cursor: #fff;
  146. .login-container {
  147. .el-form-item {
  148. border: 1px solid #dcdee2;
  149. border-radius: 5px;
  150. .el-input {
  151. display: inline-block;
  152. height: 40px;
  153. width: 85%;
  154. input {
  155. background: transparent;
  156. border: 0;
  157. -webkit-appearance: none;
  158. border-radius: 0px;
  159. padding: 12px 5px 12px 15px;
  160. height: 40px;
  161. }
  162. }
  163. }
  164. .no-autofill-pwd { // 解决自动填充问题
  165. .el-input__inner { // 模仿密码框原点
  166. -webkit-text-security: disc !important;
  167. }
  168. }
  169. }
  170. </style>
  171. <style lang="scss" scoped>
  172. $bg:#2d3a4b;
  173. $dark_gray:#889aa4;
  174. $light_gray:#eee;
  175. .login-container {
  176. min-height: 100%;
  177. width: 100%;
  178. overflow: hidden;
  179. background-image: url('../../assets/body.svg');
  180. background-repeat: no-repeat;
  181. background-position: 50%;
  182. background-size: 100%;
  183. .login-form {
  184. position: relative;
  185. width: 500px;
  186. max-width: 100%;
  187. margin: 0 auto;
  188. padding: 140px 35px 0;
  189. overflow: hidden;
  190. box-sizing: border-box;
  191. .svg-container {
  192. padding: 0 10px;
  193. }
  194. .show-pwd {
  195. font-size: 16px;
  196. cursor: pointer;
  197. margin-left: 7px;
  198. }
  199. .admin-logo {
  200. display: flex;
  201. align-items: center;
  202. justify-content: center;
  203. margin-bottom: 20px;
  204. .logo {
  205. width: 60px;
  206. height: 60px;
  207. }
  208. .name {
  209. font-weight: normal;
  210. margin-left: 10px;
  211. }
  212. }
  213. }
  214. }
  215. </style>

创建useRouteQuery hook

获取url query 并得到redirect参数 redirec参数或作为登录成功后跳转路径

src/views/login/hooks/useRouteQuery.ts

  1. import { ref, Ref, watchEffect } from 'vue'
  2. import { useRoute, LocationQueryRaw } from 'vue-router'
  3. interface RouteQuery {
  4. redirect: Ref<string>;
  5. otherQuery: Ref<LocationQueryRaw | undefined>;
  6. }
  7. const useRouteQuery = (): RouteQuery => {
  8. const route = useRoute()
  9. const query = route.query
  10. const redirect = ref('')
  11. const otherQuery = ref<LocationQueryRaw | undefined>(undefined)
  12. const getOtherQuery = (query: LocationQueryRaw) => {
  13. return Object.keys(query || {}).filter(q => q !== 'redirect').reduce((obj, key) => {
  14. obj[key] = query[key]
  15. return obj
  16. }, {} as LocationQueryRaw)
  17. }
  18. otherQuery.value = getOtherQuery(query)
  19. // 不使用watch(route) 原因说明:
  20. // 尤大回应 https://www.gitmemory.com/issue/vuejs/vue-next/2027/685247838
  21. // https://blog.csdn.net/weixin_47339511/article/details/117221559
  22. // 修复使用watch 监听route 性能开销问题
  23. watchEffect(() => {
  24. const query = route.query
  25. if (query) {
  26. redirect.value = query.redirect as string
  27. otherQuery.value = getOtherQuery(query as LocationQueryRaw)
  28. }
  29. })
  30. return {
  31. redirect,
  32. otherQuery
  33. }
  34. }
  35. export default useRouteQuery

本节参考源码

https://gitee.com/brolly/vue3-element-admin/commit/a4471f25ee5d88125dc7ee3569b2122beb384a07