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(‘/‘) 说明-优快云博客
切换路由时滚动到顶部
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)
}
})
}