vue3+typescript+pinia+数据可视化大屏项目笔记

vite项目初始化

pnpm create vite (具体配置去看vite官)

找不到模块“path”或其相应的类型声明

安装pnpm i -D @types/node

Module '"d:/web/vue-project/vue3_admin/src/components/HelloWorld.vue"' has no default export.

vscode插件 Vetur(v0.35.0)不支持最新写法 卸载 并 安装 Volar 插件

配置@路径

在vite.config.ts配置

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve('./src')
    }
  }
})

在tsconfig.json配置,在compilerOptions下新增对象

"baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
    "paths": {//路径映射,相对于baseUrl
      "@/*":["src/*"]
    }

router

在src文件夹下,创建router文件夹,并创建index.ts

默认情况下,所有路由是不区分大小写的,并且能匹配带有或不带有尾部斜线的路由。例如,路由 /users 将匹配 /users/users/、甚至 /Users/。可以通过 strict 和 sensitive 选项来修改,它们既可以应用在整个全局路由上,又可以应用于当前路由上

import { createRouter, createWebHashHistory } from 'vue-router'
import { commonRoutes } from './routes'

const router = createRouter({
  history: createWebHashHistory(),
  routes: commonRoutes,
  strict: true
})

export default router

 在router文件夹下创建routes.ts

export const commonRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index.vue'),
    name: 'login',
    sensitive: true
  },
  {
    path: '/',
    redirect: '/home'
  },
  {
    path: '/home',
    component: () => import('@/views/home/index.vue'),
    name: 'Home'
  },
  // 404
  {
    path: '/404',
    component: () => import('@/views/404/index.vue'),
    name: '404'
  },
  // acl
  {
    path: '/acl',
    name: 'Acl',
    redirect: '/acl/user',
    children: [
      {
        path: '/acl/user',
        component: () => import('@/views/acl/user/index.vue'),
        name: 'User'
      },
      {
        path: '/acl/role',
        component: () => import('@/views/acl/role/index.vue'),
        name: 'Role'
      },
      {
        path: '/acl/permission',
        component: () => import('@/views/acl/permission/index.vue'),
        name: 'Permission',
        sensitive: true
      }
    ]
  },
  // product
  {
    path: '/product',
    name: 'Product',
    redirect: '/product/trademark',
    children: [
      {
        path: '/product/trademark',
        component: () => import('@/views/product/trademark/index.vue'),
        name: 'Trademark'
      },
      {
        path: '/product/attr',
        component: () => import('@/views/product/attr/index.vue'),
        name: 'Attr'
      },
      {
        path: '/product/spu',
        component: () => import('@/views/product/spu/index.vue'),
        name: 'Spu'
      },
      {
        path: '/product/sku',
        component: () => import('@/views/product/sku/index.vue'),
        name: 'Sku'
      }
    ]
  },
  {
    path: '/screen',
    component: import('@/views/screen/index.vue'),
    name: 'Screen'
  },
  {
    path: '/:pathMatch(.*)*',
    redirect: '/404',
    name: 'Any'
  }
]

在main.ts中导入,然后app.use(router)

在App.vue

<template>
  <div>
    <router-view></router-view>
  </div>
</template>

<script setup lang="ts"></script>

<style scoped></style>

安装sass

pnpm i -D sass sass-loader

<style scoped lang="scss">

</style>

使用sass变量

主要用途是将我们的variable.scss中的scss常量加载到全局,这样我们可以在style标签中,随意使用这些scss常量,在vite.config.ts中进行配置

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve('./src')
    }
  },
//看这段
  css: {
    preprocessorOptions: {
      scss: {
        javascriptEnabled: true,
        additionalData: '@import "./src/styles/variable.scss";'
      }
    }
  }
})

reset.scss

*,
*:after,
*:before {
  box-sizing: border-box;

  outline: none;
}

html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
  font: inherit;
  font-size: 100%;

  margin: 0;
  padding: 0;

  vertical-align: baseline;

  border: 0;
}

article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
  display: block;
}

body {
  line-height: 1;
}

ol,
ul {
  list-style: none;
}

blockquote,
q {
  quotes: none;

  &:before,
  &:after {
    content: '';
    content: none;
  }
}

sub,
sup {
  font-size: 75%;
  line-height: 0;

  position: relative;

  vertical-align: baseline;
}

sup {
  top: -.5em;
}

sub {
  bottom: -.25em;
}

table {
  border-spacing: 0;
  border-collapse: collapse;
}

input,
textarea,
button {
  font-family: inhert;
  font-size: inherit;

  color: inherit;
}

select {
  text-indent: .01px;
  text-overflow: '';

  border: 0;
  border-radius: 0;

  -webkit-appearance: none;
  -moz-appearance: none;
}

select::-ms-expand {
  display: none;
}

code,
pre {
  font-family: monospace, monospace;
  font-size: 1em;
}

安装pinia

pnpm i pinia

创建一个简单的pinia应用来检验pinia是否可用

在src文件夹下创建store文件夹,在store文件夹下创建index.ts

import { createPinia } from 'pinia'

const pinia = createPinia()

export default pinia

然后在main.ts中进行导入,app.use(pinia)

在store文件夹下创建module文件夹,用来管理各个模块,创建user.ts

import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => {
    return {
      count: 0
    }
  },
  actions: {
    increment() {
      this.count++
    }
  }
})

然后去一个组件中使用

<template>
  <div class="login">
    <span>值为{{ userStore.count }}</span>
    <button @click="addCount">+1</button>
  </div>
</template>

<script setup lang="ts">
import { useUserStore } from '@/store/module/user'
const userStore = useUserStore()

const addCount = () => {
  userStore.increment()
}
</script>

<style scoped lang="scss"></style>

修改elementplus中的样式

:deep(.el-form-item__label) {
  color: white
}

配置proxy和axios

在主目录创建.env.development和.env.production代表生产环境和开发环境的配置,测试环境我没有配置

开发环境的配置

NODE_ENV='development'
VITE_APP_TITLE='小余甄选'
VITE_APP_BASE_URL='/api'
VITE_SERVE="http://sph-api.atguigu.cn"

在vite.config.ts配置proxy并引入环境变量

import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
  //获取各种环境下的对应的变量
  const env = loadEnv(mode, process.cwd())
  return {
    plugins: [
      vue(),
//elementPlus的自动引入
      AutoImport({
        resolvers: [ElementPlusResolver()]
      }),
      Components({
        resolvers: [ElementPlusResolver()]
      })
    ],
    resolve: {
      alias: {
        '@': path.resolve('./src')
      }
    },
    css: {
      preprocessorOptions: {
        scss: {
          javascriptEnabled: true,
          additionalData: '@import "./src/styles/variable.scss";'
        }
      }
    },
 //看这里
    server: {
      proxy: {
        [env.VITE_APP_BASE_URL]: {
          target: env.VITE_SERVE,
          changeOrigin: true,
          rewrite: path => path.replace(/^\/api/, '')
        }
      }
    }
  }
})

axios的二次封装

import axios from 'axios'
import { ElMessage } from 'element-plus'

const instance = axios.create({
  baseURL: import.meta.env.VITE_APP_BASE_URL,
  timeout: 5000
})

// 请求拦截器
instance.interceptors.request.use(
  function (config) {
    return config
  },
  function (error) {
    return Promise.reject(error)
  }
)

// 添加响应拦截器
axios.interceptors.response.use(
  function (response) {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    return response
  },
  function (error) {
    let message = ''

    // 对错误的响应状态码进行处理
    switch (error.response.status) {
      case 401:
        message = 'token过期'
        break
      case 403:
        message = '无权访问'
        break
      case 404:
        message = '请求地址错误'
        break
      case 500:
        message = '服务器错误'
        break
      default:
        message = `未知错误${error.response.status}`
        break
    }

    ElMessage.error(message)

    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    return Promise.reject(error)
  }
)

export default instance

自动导入elementPlus遇到的bug

使用Elmessage时会报错,Cannot find name 'ElMessage'

这个时候只需在tsconfig.json中添加auto-imports.d.ts就行了

"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue","auto-imports.d.ts"],

忽略ts警告

//@ts-ignore
import nprogress from 'nprogress'

路由守卫进入死循环


错误代码

router.beforeEach(async (to, from, next) => {
  nprogress.start()

  const token = userStore.token

  // 如果存在token
  if (token) {
    next()
  } else {
    next({ path: '/login' })
  }
})

 修改后

router.beforeEach(async (to, from, next) => {
  nprogress.start()

  const token = userStore.token

  // 如果存在token
  if (token) {
    console.log(1)
    next()
  } else {
    if (to.path == '/login') {
      console.log(2)
      next()
    } else {
      next({ path: '/login' })
      console.log(3)
    }
  }
})

此时点击登录按钮并没有跳转而是又返回了login页面并且打印了3,说明没有得到token,当再次点击登录才进行跳转于是做了如下修改,添加了await直到登录完成后再执行路由跳转操作

   // 需要加await
      await useStore.post_user_info_async(form)
      router.push('/home')

可以通过路由元信息进行菜单的动态显示

  {
        path: '/acl/user',
        component: () => import('@/views/acl/user/index.vue'),
        name: 'User',
        meta: {
          title: '用户管理',
          hidden: false,
          icon: ''
        }
      },

封装svg组件

<template>
  <!-- svg:图标外层容器节点,内部需要与use标签结合使用 -->
  <svg :style="{ width, height }">
    <!-- xlink:href执行用哪一个图标,属性值务必#icon-图标名字 -->
    <!-- use标签fill属性可以设置图标的颜色 -->
    <use :xlink:href="prefix + name" :fill="color"></use>
  </svg>
</template>

<script setup lang="ts">
//接受父组件传递过来的参数
defineProps({
  //xlink:href属性值前缀
  prefix: {
    type: String,
    default: '#icon-',
  },
  //提供使用的图标名字
  name: String,
  //接受父组件传递颜色
  color: {
    type: String,
    default: '',
  },
  //接受父组件传递过来的图标的宽度
  width: {
    type: String,
    default: '16px',
  },
  //接受父组件传递过来的图标的高度
  height: {
    type: String,
    default: '16px',
  },
})
</script>

<style scoped></style>

安装svg插件依赖

pnpm install vite-plugin-svg-icons -D

vite.config.ts中进行配置

import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'
export default () => {
  return {
    plugins: [
      createSvgIconsPlugin({
        // Specify the icon folder to be cached
        iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
        // Specify symbolId format
        symbolId: 'icon-[dir]-[name]',
      }),
    ],
  }
}

在main.ts中导入

import 'virtual:svg-icons-register'

刷新出现头像和名字闪烁问题

在App.vue中获取用户信息会出现此问题

<template>
  <div>
    <router-view></router-view>
  </div>
</template>

<script setup lang="ts">
import { onBeforeMount } from 'vue';
import { useUserStore } from './store/module/user';


const userStore = useUserStore()

onBeforeMount(async () => {
  await userStore.get_user_info()
})

</script>

<style scoped></style>

但在路由守卫中获取用户信息,刷新就不会出现此类问题

router.beforeEach(async (to, from, next) => {
  nprogress.start()

  const token = userStore.token
  const username = userStore.name
  // 如果存在token
  if (token) {
    if (username) {
      next()
    } else {
      try {
        await userStore.get_user_info()
        next({ ...to })
      } catch (err) {
        await userStore.post_logout()
        next({ path: '/login' })
      }
    }
  } else {
    if (to.path == '/login') {
      next()
    } else {
      next({ path: '/login' })
    }
  }
})

异步组件

// 异步组件
const Category = defineAsyncComponent(() => import('@/components/category/Category.vue'))

具体查看官网关于异步组件的描述

表单聚焦

点击添加属性值时,属性值名称应该聚焦

首先设置一个ref的数组,用来保存每一个输入框的dom

const inputArr = ref<any>([])

<el-input :ref="(vc:any) => (inputArr[$index] = vc)" v-if="row.flag" v-model.trim="row.valueName" @blur="editAttrValue(row, $index)"></el-input>

再通过nextTick,因为我们要等dom渲染完毕后才能操作dom,要不然就会报错,拿不到dom

 nextTick(() => {
    inputArr.value[attrParams.attrValueList.length - 1].focus()
  })

清空所有值


const clearAllData = () => {
  Object.assign(attrParams, {
    attrName: '', //新增的属性的名字
    attrValueList: [
      //新增的属性值数组
    ],
    categoryId: '', //三级分类的ID
    categoryLevel: 3 //代表的是三级分类
  })

}

注意Object.assign是浅拷贝,然后通过父组件调用子组件的方法进行清空

异步组件使用的注意事项

const AddOrEdit = defineAsyncComponent(() => import('./AddOrEdit.vue'))

如果你是异步引入的,子组件不能调用父组件上暴露的属性,通过正常引入才能使用$parent,

或者使用两层$parent也可以获取到

修改整个reactive失去响应式

// 收集的参数
let attrParams = reactive<Attr>({
  attrName: '', //新增的属性的名字
  attrValueList: [
    //新增的属性值数组
  ],
  categoryId: categoryStore.c3Id, //三级分类的ID
  categoryLevel: 3 //代表的是三级分类
})

// 修改属性参数
const updateAttrParams = (data: Attr) => {

    attrParams = data

}

此时reactive失去了响应式

改成这样或者单个单个赋值又可以有响应式效果

// 修改属性参数
const updateAttrParams = (data: Attr) => {
  Object.assign(attrParams, data)
}

使用深拷贝

因为父组件和子组件用的是同一份数据,而对象又是引用类型,对对象里面的值修改,两者都会影响到 

// 编辑属性
const editAttr = (row: Attr) => {
  console.log(row)
  addOrEditRef.value.updateAttrParams(cloneDeep(row))
  add_edit_show.value = true
}

使用Transition组件

<el-main>
          <router-view v-slot="{ Component }">
            <Transition name="router" mode="out-in">
              <component :is="Component"></component>
            </Transition>
          </router-view>
        </el-main>

css

<style lang="scss" scoped>
.aside {
  background: $menu_background;
}
.router-enter-active {
  transition: all 0.2s ease-out;
}

.router-leave-active {
  transition: all 0.2s cubic-bezier(1, 0.5, 0.8, 1);
}

.router-enter-from,
.router-leave-to {
  transform: translateX(20px);
  opacity: 0;
}
</style>

异步组件配合Suspense组件

<Suspense>
        <Category :disabled="is_show_add !== 0"></Category>
        <template #fallback> 加载中请稍等... </template>
      </Suspense>
const Category = defineAsyncComponent(() => import('@/components/category/Category.vue'))

获取组件的ref不需要加:

<AddSPU ref="addSpuRef" :edit_row="edit_spu"></AddSPU>

获取该组件直接使用ref,而不是使用:ref,否则会出现undefined报错

使用:连接两个属性

    <el-form-item label="销售属性">
          <el-form inline>
            <el-form-item v-for="item in saleArr" :key="item.id" :label="item.saleAttrName">
              <el-select v-model="item.saleIdAndValueId" placeholder="请选择销售属性">
                <el-option v-for="i2 in item.spuSaleAttrValueList" :label="i2.saleAttrValueName" :value="`${item.id}:${i2.id}`" />
              </el-select>
            </el-form-item>
          </el-form>
        </el-form-item>

如果需要两个属性可以通过:value="`${item.id}:${i2.id}`"字符串实现,然后通过split分解就可以得到这两个属性了

路由的分配

在路由中,分为通用路由和异步路由,异步路由就是后端返回的路由,然后在用户仓库中,将返回的异步路由进行过滤匹配

// 过滤异步路由
function filterAsyncRoute(asyncRoute: any, route: any) {
  return asyncRoute.filter((item: any) => {
    if (route.includes(item.name)) {
      // 必须要加item.children否则将会咋undefined上读取length
      if (item.children && item.children.length > 0) {
        item.children = filterAsyncRoute(item.children, route)
      }
      return true
    }
  })
}

然后通过路由的addRoute方法添加到路由中

[...filterAsyncRoute(asyncRoutes, res.data.data.routes)].forEach((item: any) => {
          router.addRoute(item)
        })

对按钮添加权限

使用自定义指令,创建directives文件夹,里面存放自定义指令的文件

import pinia from '@/store'
import { useUserStore } from '@/store/module/user'

const userStore = useUserStore(pinia)

// 按钮是否显示的自定义指令
export const IsHasButton = (app: any) => {
  app.directive('has', {
    mounted(el: any, binding: any) {
      if (!userStore.buttons?.includes(binding.value)) {
        el.parentNode.removeChild(el)
      }
    }
  })
}

如果在后端返回的按钮数组中没有该按钮信息则从中移除,比如

v-has="'btn.Trademark.add'"

如果数组没btn.Trademark.add,则在dom节点中移除

设置页面的标题

router.beforeEach(async (to, from, next) => {
  // 设置标题
  document.title = `${setting.title}-${to.meta.title}`
 ...

刷新出现404

因为路由是后端返回的,使用路径变为undefined,此时将任意路由从通用路由抽离,然后和异步路由一起通过addRoute动态添加到路由中

// 通用路由
export const commonRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index.vue'),
    name: 'Login',
    meta: {
      title: '登录',
      hidden: true, //是否在菜单中显示
      icon: ''
    },
    sensitive: true
  },
  {
    path: '/',
    component: () => import('@/components/layout/index.vue'),
    redirect: '/home',
    meta: {
      title: '',
      hidden: true, //是否在菜单中显示
      icon: ''
    },
    children: [
      {
        path: '/home',
        component: () => import('@/views/home/index.vue'),
        name: 'Home',
        meta: {
          title: '首页',
          hidden: false,
          icon: 'House'
        }
      }
    ]
  },
  // 数据大屏
  {
    path: '/screen',
    component: () => import('@/views/screen/index.vue'),
    name: 'Screen',
    meta: {
      title: '数据大屏',
      hidden: false,
      icon: 'PieChart'
    }
  },

  // 404
  {
    path: '/404',
    component: () => import('@/views/404/index.vue'),
    name: '404',
    meta: {
      title: '404',
      hidden: true,
      icon: ''
    }
  }
]

// 异步路由
export const asyncRoutes = [
  // acl
  {
    path: '/acl',
    name: 'Acl',
    component: () => import('@/components/layout/index.vue'),
    redirect: '/acl/user',
    meta: {
      title: '权限管理',
      hidden: false,
      icon: 'Checked'
    },
    children: [
      {
        path: '/acl/user',
        component: () => import('@/views/acl/user/index.vue'),
        name: 'User',
        meta: {
          title: '用户管理',
          hidden: false,
          icon: ''
        }
      },
      {
        path: '/acl/role',
        component: () => import('@/views/acl/role/index.vue'),
        name: 'Role',
        meta: {
          title: '角色管理',
          hidden: false,
          icon: ''
        }
      },
      {
        path: '/acl/permission',
        component: () => import('@/views/acl/permission/index.vue'),
        name: 'Permission',
        sensitive: true,
        meta: {
          title: '菜单管理',
          hidden: false,
          icon: ''
        }
      }
    ]
  },
  // product
  {
    path: '/product',
    name: 'Product',
    component: () => import('@/components/layout/index.vue'),
    redirect: '/product/trademark',
    meta: {
      title: '商品管理',
      hidden: false,
      icon: 'InfoFilled'
    },
    children: [
      {
        path: '/product/trademark',
        component: () => import('@/views/product/trademark/index.vue'),
        name: 'Trademark',
        meta: {
          title: '品牌管理',
          hidden: false,
          icon: ''
        }
      },
      {
        path: '/product/attr',
        component: () => import('@/views/product/attr/index.vue'),
        name: 'Attr',
        meta: {
          title: '属性管理',
          hidden: false,
          icon: ''
        }
      },
      {
        path: '/product/spu',
        component: () => import('@/views/product/spu/index.vue'),
        name: 'Spu',
        meta: {
          title: 'SPU管理',
          hidden: false,
          icon: ''
        }
      },
      {
        path: '/product/sku',
        component: () => import('@/views/product/sku/index.vue'),
        name: 'Sku',
        meta: {
          title: 'SKU管理',
          hidden: false,
          icon: ''
        }
      }
    ]
  }
]

// 任意路由
export const anyRoutes = {
  path: '/:pathMatch(.*)*',
  redirect: '/404',
  name: 'Any',
  meta: {
    title: '任意',
    hidden: true,
    icon: ''
  }
}

user仓库的部分代码

[...filterAsyncRoute(asyncRoutes, res.data.data.routes), anyRoutes].forEach((item: any) => {
          router.addRoute(item)
        })

刷新页面后页面出现空白

    //万一:刷新的时候是异步路由,有可能获取到用户信息、异步路由还没有加载完毕,出现空白的效果
          next({ ...to })

不要用next()

可以看这篇文章VUE 路由守卫 next() / next({ ...to, replace: true }) / next(‘/‘) 说明-CSDN博客

切换路由时滚动到顶部

const router = createRouter({
  history: createWebHashHistory(),
  routes: commonRoutes,
  strict: true,
  // 因为会用之前页面滚动的位置
  scrollBehavior() {
    return { top: 0, left: 0 }
  }
})

优化:退出后用户可以返回当前访问过页面

在退出登录添加参数

// 退出登录
const logout = async () => {
  await userStore.post_logout()
  router.push({ path: '/login', query: { redirect: route.path } })
  ElMessage.success('退出成功')
}

在路由守卫中也是如此

import { createRouter, createWebHashHistory } from 'vue-router'
import { commonRoutes } from './routes'
import pinia from '@/store'
import { useUserStore } from '@/store/module/user'
import setting from '@/setting'

//@ts-ignore
import nprogress from 'nprogress'
//引入进度条样式
import 'nprogress/nprogress.css'

nprogress.configure({ showSpinner: false })

const userStore = useUserStore(pinia)

const router = createRouter({
  history: createWebHashHistory(),
  routes: commonRoutes,
  strict: true,
  // 因为会用之前页面滚动的位置
  scrollBehavior() {
    return { top: 0, left: 0 }
  }
})

router.beforeEach(async (to, from, next) => {
  // 设置标题
  document.title = `${setting.title}-${to.meta.title}`
  nprogress.start()

  const token = userStore.token
  const username = userStore.name
  // 如果存在token
  if (token) {
    // 如果去登录页面
    if (to.path == '/login') {
      next({ path: '/' })
    } else {
      // 如果存在用户名字
      if (username) {
        next()
      } else {
        // 没有就重新请求
        try {
          await userStore.get_user_info()
          //万一:刷新的时候是异步路由,有可能获取到用户信息、异步路由还没有加载完毕,出现空白的效果
          next({ ...to })
        } catch (err) {
          await userStore.post_logout()
          next({ path: '/login', query: { redirect: to.path } })
        }
      }
    }
  } else {
    // 没有token
    if (to.path == '/login') {
      next()
    } else {
      // 优化用户体验,用户从上个页面退出后再次登录可以再次访问该页面
      next({ path: '/login', query: { redirect: to.path } })
    }
  }
})

router.afterEach(() => {
  nprogress.done()
})

export default router

登录页登录只要

router.push({ path: (route.query.redirect as string) || '/' })

此方法不好,如果没有该页面的用户就无法跳转

 let redirect = route.query.redirect as string

      let name = redirect.split('/')[1]
      // 如果该用户的路由权限中没有该路径则跳往首页
      if (useStore.routes.includes(name.slice(0, 1).toUpperCase() + name.slice(1))) {
        router.push({ path: redirect || '/' })
      } else {
        router.push('/')
      }

全屏显示


// 点击全屏按钮
const fullScreen = () => {
  //DOM对象的一个属性:可以用来判断当前是不是全屏模式[全屏:true,不是全屏:false]
  let full = document.fullscreenElement
  if (!full) {
    document.documentElement.requestFullscreen()
  } else {
    //变为不是全屏模式->退出全屏模式
    document.exitFullscreen()
  }
}

elementplus的暗黑模式

在main.ts导入

import 'element-plus/theme-chalk/dark/css-vars.css'

然后在该按钮

// 暗黑模式
const changeDark = () => {
  let html = document.documentElement
  mode.value ? (html.className = 'dark') : (html.className = '')
}

具体看element plus官网

换主题颜色

// 改变主题颜色
const changColor = () => {
  //通知js修改根节点的样式对象的属性与属性值
  const html = document.documentElement
  html.style.setProperty('--el-color-primary', color.value)
  settingStore.color = color.value
}

添加防抖自定义指令

// 封装防抖指令
import type { DirectiveBinding } from 'vue'

interface ElType extends HTMLElement {
  HandleClick: () => any
}

export const debounce = (app: any) => {
  app.directive('debounce', {
    mounted(el: ElType, binding: DirectiveBinding) {
      // 先判断类型
      if (typeof binding.value !== 'function') {
        throw '值应该是个函数'
      }

      let timer: any = null
      el.HandleClick = () => {
        if (timer) clearTimeout(timer)

        timer = setTimeout(() => {
          binding.value()
        }, 1000)
      }
      el.addEventListener('click', el.HandleClick)
    },
    beforeUnmount(el: ElType) {
      el.removeEventListener('click', el.HandleClick)
    }
  })
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值