摸鱼时间少? 是时候学会用Vue自定义指令进行业务开发了

2023-10-27

文章目录


前言

祝各位码农新年快乐 头发越来越多(狗头)

兔年的第一篇博客是以业务为主,主要介绍Vue自定义指令的各种特性

以及博主如何使用这个特性在业务中一分钟实现一个需求的(节省时间用来划水)

以下内容是博主半年前实现,可能有些地方会遗漏,所以最后会放出源码,欢迎同志们指正


一、博主用Vue自定义指令在业务中实现了什么需求?

1. 首屏Loading切换指令(用来占位,支持调节Loading样式)

在这里插入图片描述

2. 复制指令

请添加图片描述

3. 文件流形式下载后端数据(转blob下载)

在这里插入图片描述

4. 防抖(支持设置延迟时间)

在这里插入图片描述

5. 按钮或菜单权限控制(支持参数)

在这里插入图片描述

6. 界面添加水印指令(支持文字以及部分样式调节)

在这里插入图片描述

二、Vue指令详解(了解代码,可以直接看第三步)

自带指令

截止到Vue3.2一共有16个自带指令,用起来让人直呼过瘾,在封装自定义指令之前,让我们来概览一下所有指令并了解其特性

  1. v-text(用于更新dom元素的 textContent)
  2. v-html(用于更新dom元素的 innerHTML)
  3. v-show(切换元素的display值,用于控制元素的展示和隐藏,值得注意的是这个在dom元素里仍是存在的,不支持<template>)
  4. v-if(根据条件来渲染dom元素,销毁/重建,支持<template>)
  5. v-else(v-if的对立条件,不满足if则走else语句)
  6. v-else-if(如果存在此条件,则判断条件先走if和elseif,最后走else)
  7. v-for(可以根据数据源渲染多个dom元素,业务中比较常见)
  8. v-on(用于给元素绑定事件,语法糖为@,可以使用语法修饰符,这里不再列举)
  9. v-bind(绑定数据和元素属性,语法糖为为: 或.(在使用 .prop 修饰符时),同样有三个语法修饰符)
  10. v-model(Vue对指定标签类型进行双向数据绑定(input/select/textarea))
  11. v-slot(使用具名插槽或需要接收prop的插槽,默认为default)
  12. v-pre(直接跳过这个元素以及子元素的编译阶段,常常用于博客论坛类网址)
  13. v-cloak(用于解决页面上插值问题,例如{{}}刚开始会在页面上显示出来)
  14. v-once(只进行一次渲染,后续刷新不会重新渲染)
  15. v-memo(3.2+新出语法,一般用于树结构,用来缓存dom以及数据,如果数据没变,dom及子节点将不会重新渲染)
  16. v-is(3.1.0被废弃,改为:is,常用于动态组件,符合条件就进行渲染)

其中v-memo3.2新增的,v-is在3.1.0中废弃
本文着重介绍自定义指令,以上可以给同志们温故知新

自定义指令

局部自定义指令注册

directives: {
  focus: {
    // 指令的定义
    inserted: function (el) {
      el.focus()
    }
  }
}

全局自定义指令注册

// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})

两种不同注册方式使用方法是一样的

<input v-focus>

指令和组件是一样的,有它自己的生命周期
同时在Vue里面会在生命周期里给我们回调一些指定的参数用于我们快捷实现需求,如下:

指令的5个生命周期:

bind 只调用一次,指令第一次绑定到元素时候调用,用这个钩子可以定义一个绑定时执行一次的初始化动作。
 
inserted:被绑定的元素插入父节点的时候调用(父节点存在即可调用,不必存在document中)
 
update: 被绑定于元素所在模板更新时调用,而且无论绑定值是否有变化,通过比较更新前后的绑定值,忽略不必要的模板更新
 
componentUpdate :被绑定的元素所在模板完成一次更新更新周期的时候调用
 
unbind: 只调用一次,指令与元素解绑的时候调用

指令的回调参数:

el:指令所绑定的元素,可以用来直接操作 DOM。
binding:一个对象,包含以下 property:
name:指令名,不包括 v- 前缀。
value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。
oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。
arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。
modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
vnode:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。
oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

三、如何实现标题中六个自定义指令(代码层面)

1.Loading指令

在这里插入图片描述

分析需求:在不考虑样式的情况下,这个功能在我看来其实就是

在恰当的时机进行dom元素插入(Loading图),而后在合适的时机销毁对应的dom元素

我们首先来实现这个需求

// 插入到目标元素
const insertDom = (parent, el) => {
  parent.appendChild(el.mask)
}
//控制元素的创建和销毁
const toggleLoading = (el, binding) => {
  //这个binding.value其实就是自定义指令的传参
  if (binding.value) {
    Vue.nextTick(() => {
      // 插入到目标元素
      insertDom(el, el)
    })
  } else {
    el.mask && el.mask.parentNode && el.mask.parentNode.removeChild(el.mask)
  }
  el.style.position = 'relative'
}

OK 一个动态创建和销毁元素的逻辑已经完成

值得注意的是博主在里面加入了一句el.style.position = 'relative'

这个代码是给父级dom设置相对定位,便于我们之后的Loading图使用定位的方式进行居中

随后我们需要一个真实的元素在页面中间进行跳动,这个地方其实用div标签也能实现需求,

但是显然不尽善尽美,于是博主动起了小脑袋瓜

有没有办法可以把一个组件的所有属性以及dom元素进行继承呢?

答案显然是可以的,它就是Vueextend函数

这个语法会创建一个Vue默认的构造器,拥有默认模板同时,会把里面的数据进行替换

首先我们需要自己创建一个组件(即是界面中的Loading组件)

在这里插入图片描述

样式直接贴代码,纯css就能实现,有兴趣的同学可以自己去拓展

<!--
 * @Descripttion:
 * @version:
 * @Author: 崔战神
 * @Date: 2022-06-22 16:20:25
 * @LastEditors: 崔战神
 * @LastEditTime: 2022-06-28 14:38:02
-->
<template>
  <div class="loading" >
    <div></div>
    <div></div>
  </div>
</template>

<style>
.loading,
.loading > div {
  position: relative;
  box-sizing: border-box;
}
.loading {
  display: block;
  font-size: 0;
  color: #accbee;
  position: absolute;
  background-image: -webkit-gradient(linear, 0 0, 0 bottom, from(#accbee), to(#e7f0fd));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  top:50%;
  left:50%;
  transform: translate(-50%,-50%);
}

.loading.la-dark {
  color: #1479ff;
}

.loading > div {
  display: inline-block;
  float: none;
  background-color: currentColor;
  border: 0 solid currentColor;
}

.loading {
  width: 2rem;
  height: 2rem;
}

.loading > div:nth-child(1),
.loading > div:nth-child(2) {
  position: absolute;
  left: 0;
  width: 100%;
}

.loading > div:nth-child(1) {
  top: -25%;
  z-index: 1;
  height: 100%;
  border-radius: 10%;
  animation: square-jelly-box-animate 0.6s -0.1s linear infinite;
}

.loading > div:nth-child(2) {
  bottom: -9%;
  height: 10%;
  background: #1479ff;
  border-radius: 50%;
  opacity: 0.2;
  animation: square-jelly-box-shadow 0.6s -0.1s linear infinite;
}

.loading.la-sm {
  width: 1rem;
  height: 1rem;
}

.loading.la-2x {
  width: 4rem;
  height: 4rem;
}

.loading.la-3x {
  width: 6rem;
  height: 6rem;
}

@keyframes square-jelly-box-animate {
  17% {
    border-bottom-right-radius: 10%;
  }

  25% {
    transform: translateY(25%) rotate(22.5deg);
  }

  50% {
    border-bottom-right-radius: 100%;
    transform: translateY(50%) scale(1, 0.9) rotate(45deg);
  }

  75% {
    transform: translateY(25%) rotate(67.5deg);
  }

  100% {
    transform: translateY(0) rotate(90deg);
  }
}

@keyframes square-jelly-box-shadow {
  50% {
    transform: scale(1.25, 1);
  }
}
</style>

组件目录结构如图:

在这里插入图片描述
万事俱备,只欠东风
我们接下来在指令代码中,引入这个Loading的组件

const Mask = Vue.extend(Loading) //创建一个vue默认的构造器,会拥有默认模板的同时,对函数指定数据进行替换

在这里插入图片描述
这时我们已经成功通过Vue.extend()来拿到了目录中Loading组件的所有内容

接下来我们需要用一个变量承载Mask这个实例的同时,替换掉指定的dom内容

bind: function(el, binding, vNode) {
	//给Loading组件一个载体
    const mask = new Mask({
      el: document.createElement('div'),
      data(){
        return {}
      }
    })
    //如果自定义指令传的有参数,就用参数的位置,否则进行居中
    let top=binding.arg==undefined?'50%':binding.arg.top;
    let left=binding.arg==undefined?'50%':binding.arg.left;
    let position=binding.position==undefined?'absolute':binding.arg.position;
    [mask.$el.style.top,mask.$el.style.position,mask.$el.style.left]=[top,position,left]
    // 用一个变量接住mask实例的同时替换指定的dom内容
    el.instance = mask
    el.mask = mask.$el
    el.maskStyle = {}
    binding.value && toggleLoading(el, binding)
  },

这时候其实一个简单的Loading效果已经完成了

但是我们发现在实际使用中v-customLoading绑定的值变化时

这个组件并不会进行切换,这个时候需要用到我们的另外一个生命周期update

// 所在组件的 VNode 更新时调用--比较更新前后的值
  update: function(el, binding) {
    if (binding.oldValue !== binding.value) {
      toggleLoading(el, binding)
    }
  },

这个生命周期类似于watch方法,用于监听

在这里我们只要观察到新旧值不一样,就进行dom操作(销毁或创建)

同时我们也需要在指令解绑之后进行对应dom的销毁

 // 指令与元素解绑时调用
  unbind: function(el, binding) {
    el.instance && el.instance.$destroy()
  }

使用方法:

<div v-customLoading="loading"  ></div>
 loading: true,
//带自定义参数方法
<div v-customLoading:[customStyle]=loading ></div>
loading: true,
customStyle: {
   top: '50%', left: '50%', position: 'absolute',
 },

一定要注意,带参数和不带参数的指令绑定方式是不一样的,这个参数生命周期里回调参数为binding.arg

效果预览:
在这里插入图片描述

2.复制指令

有做过复制内容功能的同志应该都会知道
目前前端复制功能基本上大同小异(创建一个只读的textarea标签),使其不显示在可视区域内
而后以select()方法选中textareadocument.execCommand语法进行复制到粘贴板,博主这里也不例外

// 复制指令
Vue.directive('copy', {
  // 第一次绑定到元素时调用
  bind(el,binding) {
    // console.warn(binding.arg,'是否有对应参数')
    el.$value = binding.value
    let success=binding.arg==undefined?'复制成功':binding.arg.success
    let error=binding.arg==undefined?'复制失败':binding.arg.error
    let empty=binding.arg==undefined?'复制失败':binding.arg.empty
    el.handler = () => {
      // 值为空的对应提示
      if (!el.$value) {
        Message.error(empty)
        return
      }
      // 动态创建 textarea 标签
      const textarea = document.createElement('textarea')
      // 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域
      textarea.readOnly = 'readonly'
      textarea.style.position = 'absolute'
      textarea.style.left = '-9999px'
      // 将要 copy 的值赋给 textarea 标签的 value 属性
      textarea.value = el.$value
      // 将 textarea 插入到 body 中
      document.body.appendChild(textarea)
      // 选中值并复制
      textarea.select()
      textarea.setSelectionRange(0,textarea.value.length)
      const result = document.execCommand('Copy')
      if (result) {
        Message.success(success)
      }else{
        Message.error(error)
      }
      document.body.removeChild(textarea)
    }
    // 绑定点击事件
    el.addEventListener('click', el.handler)
  },
  // 当传进来的值更新的时候触发
  componentUpdated(el, { value }) {
    el.$value = value
  },
  // 指令与元素解绑的时候,移除事件绑定
  unbind(el) {
    el.removeEventListener('click', el.handler)
  },
});

在这里插入图片描述
在这里插入图片描述支持三种不同状态的提示文字自定义

效果预览:
在这里插入图片描述

3.文件流下载指令

依旧是老生常谈的url转blob地址方式进行下载

// 以文件流的形式下载数据
Vue.directive('downloadUrl', {
  // 第一次绑定到元素时调用
  bind(el, binding) {
    // console.warn(binding)
    if (binding.value.url) {
        el.addEventListener('click', () => {
            const a = document.createElement('a')
            const url = binding.value.url // 完整的url则直接使用
            // 这里是将url转成blob地址,
            fetch(url).then(res => res.blob()).then(blob => { // 将链接地址字符内容转变成blob地址
                a.href = URL.createObjectURL(blob)
                a.download = `${binding.value.fileName}` || '' // 下载文件的名字
                document.body.appendChild(a)
                a.click()
                //在资源下载完成后 清除 占用的缓存资源
                window.URL.revokeObjectURL(a.href);
                document.body.removeChild(a);
            })
        })
    }
}
});

我这里是遍历出来的,这里暴露了三个自定义属性(文件名,文件地址,文件类型)

 <el-link v-downloadUrl="{fileName:item.fileName,url:item.signedPath,type:item.fileType}"  target="_blank" >下载</el-link>

效果预览:

在这里插入图片描述

4.防抖指令

// 防抖指令,默认延迟是1s
Vue.directive('debounce', {
  inserted: function (el, binding) {
    console.warn(binding)
    let timer
    el.addEventListener('mouseup', () => {
      if (timer) {
        clearTimeout(timer)
      }
      timer = setTimeout(() => {
        binding.value()
      },binding.arg==undefined?1000:binding.arg)
    })
  },
});

使用方法:

<div v-debounce:[1000]="test"></div>
test(){
 console.warn('防抖测试)
}

效果预览:

在这里插入图片描述

5.权限指令(通过给的指定字段是否在权限数组中控制dom展示)

// 权限指令
Vue.directive('permission', {
  // 首次挂载时候调用
  bind: function(el, binding, vNode) {
    console.warn(vNode,'dom结构')
    let permission = binding.value; //所具有的权限,要跟所有的权限进行对比,没有在其中则不展示
    if (permission && binding.arg!==undefined ) {
      let hasPermission = binding.arg.includes(binding.value);
      if (!hasPermission) {
        // 没有权限 移除Dom元素
        el.parentNode && el.parentNode.removeChild(el)
      }
    }
  },
});

使用方式:
在这里插入图片描述
在这里插入图片描述
如果对应字符串在权限数组里面,则按钮展示,否则不展示

效果预览:
在这里插入图片描述

6.水印指令(生成指定水印)

// 水印指令
Vue.directive('waterMarker', {
  // 首次挂载时候调用
  bind: function (el, binding) {
    var tempCanvas = document.createElement('canvas')
    el.appendChild(tempCanvas)
    tempCanvas.width = 200
    tempCanvas.height = 150
    tempCanvas.style.display = 'none'
    var cans = tempCanvas.getContext('2d')
    cans.rotate((-20 * Math.PI) / 180)
    cans.font = binding.value.font || '16px Microsoft JhengHei'
    cans.fillStyle = binding.value.textColor || 'rgba(180, 180, 180, 0.3)'
    cans.textAlign = 'left'
    cans.textBaseline = 'Middle'
    cans.fillText(binding.value.text, tempCanvas.width / 10, tempCanvas.height / 2)
    el.style.backgroundImage = 'url(' + tempCanvas.toDataURL('image/png') + ')'
  },
});

这里是创建canvas标签再通过fillText进行文字填充,而后设为指定dom背景图的方式实现

暴露了两个属性字族字体颜色

使用方法如下:

<div v-waterMarker="{text:'崔战神',textColor:'rgba(180, 180, 180, 0.4)'}"></div>

预览效果:
在这里插入图片描述

四、相关源码

customInstructions.js

/* eslint-disable */
/*
 * @Descripttion:
 * @version:
 * @Author: 崔战神
 * @Date: 2022-06-28 09:22:28
 * @LastEditors: 崔战神
 * @LastEditTime: 2022-07-05 15:52:28
 */
import Vue from 'vue';
import Loading from '../../components/Loading.vue'
import { Message } from 'element-ui';
const Mask = Vue.extend(Loading) //创建一个vue默认的构造器,会拥有默认模板的同时,对函数指定数据进行替换
// 这里是用绑定数据来进行展示的,所以用的时候要v-xxx='true'
const toggleLoading = (el, binding) => {
  if (binding.value) {
    Vue.nextTick(() => {
      // 插入到目标元素
      insertDom(el, el)
    })
  } else {
    el.mask && el.mask.parentNode && el.mask.parentNode.removeChild(el.mask)
  }
  el.style.position = 'relative'
}

// 插入到目标元素
const insertDom = (parent, el) => {
  parent.appendChild(el.mask)
}
// 自定义loading指令
Vue.directive('customLoading', {
  // 第一次绑定到元素时调用
  bind: function(el, binding, vNode) {
    const mask = new Mask({
      el: document.createElement('div'),
      data(){
        return {}
      }
    })
    let top=binding.arg==undefined?'50%':binding.arg.top;
    let left=binding.arg==undefined?'50%':binding.arg.left;
    let position=binding.position==undefined?'absolute':binding.arg.position;
    [mask.$el.style.top,mask.$el.style.position,mask.$el.style.left]=[top,position,left]
    // 用一个变量接住mask实例的同时替换指定的dom内容
    el.instance = mask
    el.mask = mask.$el
    el.maskStyle = {}
    binding.value && toggleLoading(el, binding)
  },
  // 所在组件的 VNode 更新时调用--比较更新前后的值
  update: function(el, binding) {
    if (binding.oldValue !== binding.value) {
      toggleLoading(el, binding)
    }
  },
  // 指令与元素解绑时调用
  unbind: function(el, binding) {
    el.instance && el.instance.$destroy()
  }
});
// 复制指令
Vue.directive('copy', {
  // 第一次绑定到元素时调用
  bind(el,binding) {
    // console.warn(binding.arg,'是否有对应参数')
    el.$value = binding.value
    let success=binding.arg==undefined?'复制成功':binding.arg.success
    let error=binding.arg==undefined?'复制失败':binding.arg.error
    let empty=binding.arg==undefined?'复制失败':binding.arg.empty
    el.handler = () => {
      // 值为空的对应提示
      if (!el.$value) {
        Message.error(empty)
        return
      }
      // 动态创建 textarea 标签
      const textarea = document.createElement('textarea')
      // 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域
      textarea.readOnly = 'readonly'
      textarea.style.position = 'absolute'
      textarea.style.left = '-9999px'
      // 将要 copy 的值赋给 textarea 标签的 value 属性
      textarea.value = el.$value
      // 将 textarea 插入到 body 中
      document.body.appendChild(textarea)
      // 选中值并复制
      textarea.select()
      textarea.setSelectionRange(0,textarea.value.length)
      const result = document.execCommand('Copy')
      if (result) {
        Message.success(success)
      }else{
        Message.error(error)
      }
      document.body.removeChild(textarea)
    }
    // 绑定点击事件
    el.addEventListener('click', el.handler)
  },
  // 当传进来的值更新的时候触发
  componentUpdated(el, { value }) {
    el.$value = value
  },
  // 指令与元素解绑的时候,移除事件绑定
  unbind(el) {
    el.removeEventListener('click', el.handler)
  },
});
// 以文件流的形式下载数据
Vue.directive('downloadUrl', {
  // 第一次绑定到元素时调用
  bind(el, binding) {
    // console.warn(binding)
    if (binding.value.url) {
        el.addEventListener('click', () => {
            const a = document.createElement('a')
            const url = binding.value.url // 完整的url则直接使用
            // 这里是将url转成blob地址,
            fetch(url).then(res => res.blob()).then(blob => { // 将链接地址字符内容转变成blob地址
                a.href = URL.createObjectURL(blob)
                a.download = `${binding.value.fileName}` || '' // 下载文件的名字
                document.body.appendChild(a)
                a.click()
                //在资源下载完成后 清除 占用的缓存资源
                window.URL.revokeObjectURL(a.href);
                document.body.removeChild(a);
            })
        })
    }
}
});
// 防抖指令,默认延迟是1s
Vue.directive('debounce', {
  inserted: function (el, binding) {
    console.warn(binding)
    let timer
    el.addEventListener('mouseup', () => {
      if (timer) {
        clearTimeout(timer)
      }
      timer = setTimeout(() => {
        binding.value()
      },binding.arg==undefined?1000:binding.arg)
    })
  },
});
// 权限指令
Vue.directive('permission', {
  // 首次挂载时候调用
  bind: function(el, binding, vNode) {
    console.warn(vNode,'dom结构')
    let permission = binding.value; //所具有的权限,要跟所有的权限进行对比,没有在其中则不展示
    if (permission && binding.arg!==undefined ) {
      let hasPermission = binding.arg.includes(binding.value);
      if (!hasPermission) {
        // 没有权限 移除Dom元素
        el.parentNode && el.parentNode.removeChild(el)
      }
    }
  },
});
// 水印指令
Vue.directive('waterMarker', {
  // 首次挂载时候调用
  bind: function (el, binding) {
    var tempCanvas = document.createElement('canvas')
    el.appendChild(tempCanvas)
    tempCanvas.width = 200
    tempCanvas.height = 150
    tempCanvas.style.display = 'none'
    var cans = tempCanvas.getContext('2d')
    cans.rotate((-20 * Math.PI) / 180)
    cans.font = binding.value.font || '16px Microsoft JhengHei'
    cans.fillStyle = binding.value.textColor || 'rgba(180, 180, 180, 0.3)'
    cans.textAlign = 'left'
    cans.textBaseline = 'Middle'
    cans.fillText(binding.value.text, tempCanvas.width / 10, tempCanvas.height / 2)
    el.style.backgroundImage = 'url(' + tempCanvas.toDataURL('image/png') + ')'
  },
});

main.js

import './assets/js/customInstructions';

Loading.vue组件

<!--
 * @Descripttion:
 * @version:
 * @Author: 崔战神
 * @Date: 2022-06-22 16:20:25
 * @LastEditors: 崔战神
 * @LastEditTime: 2022-06-28 14:38:02
-->
<template>
  <div class="loading" >
    <div></div>
    <div></div>
  </div>
</template>

<style>
.loading,
.loading > div {
  position: relative;
  box-sizing: border-box;
}
.loading {
  display: block;
  font-size: 0;
  color: #accbee;
  position: absolute;
  background-image: -webkit-gradient(linear, 0 0, 0 bottom, from(#accbee), to(#e7f0fd));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  top:50%;
  left:50%;
  transform: translate(-50%,-50%);
}

.loading.la-dark {
  color: #1479ff;
}

.loading > div {
  display: inline-block;
  float: none;
  background-color: currentColor;
  border: 0 solid currentColor;
}

.loading {
  width: 2rem;
  height: 2rem;
}

.loading > div:nth-child(1),
.loading > div:nth-child(2) {
  position: absolute;
  left: 0;
  width: 100%;
}

.loading > div:nth-child(1) {
  top: -25%;
  z-index: 1;
  height: 100%;
  border-radius: 10%;
  animation: square-jelly-box-animate 0.6s -0.1s linear infinite;
}

.loading > div:nth-child(2) {
  bottom: -9%;
  height: 10%;
  background: #1479ff;
  border-radius: 50%;
  opacity: 0.2;
  animation: square-jelly-box-shadow 0.6s -0.1s linear infinite;
}

.loading.la-sm {
  width: 1rem;
  height: 1rem;
}

.loading.la-2x {
  width: 4rem;
  height: 4rem;
}

.loading.la-3x {
  width: 6rem;
  height: 6rem;
}

@keyframes square-jelly-box-animate {
  17% {
    border-bottom-right-radius: 10%;
  }

  25% {
    transform: translateY(25%) rotate(22.5deg);
  }

  50% {
    border-bottom-right-radius: 100%;
    transform: translateY(50%) scale(1, 0.9) rotate(45deg);
  }

  75% {
    transform: translateY(25%) rotate(67.5deg);
  }

  100% {
    transform: translateY(0) rotate(90deg);
  }
}

@keyframes square-jelly-box-shadow {
  50% {
    transform: scale(1.25, 1);
  }
}
</style>

项目目录结构:

在这里插入图片描述

祝各位码农新年快乐,明天,又是充满希望的一天!

最后放上一张镇楼图
在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

摸鱼时间少? 是时候学会用Vue自定义指令进行业务开发了 的相关文章

  • 删除

    好的 我有一个小菜单栏 菜单内的三个元素还有更多的子菜单 但是菜单栏中的元素之间有不必要的间距 而且我创建的子菜单有不必要的背景宽度 我在代码中将其涂成白色以使读者理解 由于这种不必要的宽度 即使鼠标悬停在不可见的宽度上 在本例中为白色 本
  • 如何修复“没有这样的文件或目录,lstat 'scss/'”?

    我正在尝试遵循 youtube 上的简单教程他尝试使用终端运行 npm 脚本 sass 文件 当我执行命令时npm 运行 sass显示错误消息错误 ENOENT 没有这样的文件或目录 lstat scss 我认为问题在于文件的路径或文件的权
  • 为什么我的图像下方有空间? [复制]

    这个问题在这里已经有答案了 图像在下面获得了神秘的空白空间 即使padding 0 margin 0被应用 示范 http jsfiddle net cLETP 红色边框应该包围图像 但底部有空间 造成这种情况的原因是什么 如何删除该空间
  • 用变量字符串设置槽的简单方法?

    有许多slot例子 但没有克莱尔和简单 因为我需要 我需要类似的东西 var x Hello slot x 这就是我需要的 在一个具体的例子中 https jsfiddle net 2qdh3x3v https jsfiddle net 2
  • 每 2 行后使 html 表格的边框变厚

    我创建了一个包含一些内容的表 其中有 12 行和 2 列 我想显示边框 但每 4 行之后 我想将水平边框设置为比正常情况更粗 怎么做 请在这里帮忙 试试这个选择器 table tr nth of type 4n td border bott
  • Vue中有类似React.Fragment的东西吗?

    在 React 中我可以做这样的事情 parent component export default return div div 1 div div
  • 网站在 iPhone 屏幕右侧显示空白区域

    我遇到问题http eiglaw com http eiglaw com iPhone 屏幕右侧显示约 25 像素宽的空白 边框 我在 stackoverflow 上研究了这个问题 这些帖子是相关的 但是当我尝试提供的各种解决方案时 我无法
  • Bootstrap:如何将按钮组对齐在中心(垂直)

    在下面的代码中
  • 具有 100% 高度行和 Internet Explorer 9 的表格

    我有以下示例 div style height 150px background color AAAAFF div
  • SVG 动画不适用于 Chrome 中的 favicon

    In the SVG 图标已设置 这是旋转内圆的SVG文件的内容 Favicon 动画在 Chrome 中根本不起作用 如何让它在 Chrome 中工作 在 Firefox 中可以正常工作 在 Edge 中则不行 但 Chrome 是最重要
  • jQuery 生成 div 和碰撞检测

    所以我的学校作业项目快要结束了 我只是错过了两件我似乎无法弄清楚的主要事情 1 如何生成具有随机位置的间隙的管道障碍物 以便鸟可以飞过 尝试使用一个函数来更改间隙位置的管道 div 的 css right attr 并在以下情况下移除管道它
  • 左侧导航菜单上部隐藏

    当滚动到页面最底部时 左侧导航菜单的顶部将被隐藏 The image before scrolling is shown below 滚动后的效果如下图 我不确定此问题的确切原因 因此我将不胜感激任何有关识别和解决该问题的建议或帮助 为了确
  • 引导导航栏菜单与文本重叠

    我使用最新版本的引导程序 当我调整屏幕浏览器的大小时 使用小切换按钮下拉导航栏时 导航栏会重叠页面上的文本 而不是向下推页面内容 我已经多次研究过这个问题 我尝试将 padding bottom 放在导航栏上 将 padding top 放
  • 在 React 中切换 css 类

    如何使用布尔值切换 React 中元素上 css 类的存在 在 Angular 2 中我可以这样做 class red isRed 如何在 React 中做熟悉的事情 在 React 中 元素使用如下语法获取它们的类 div div 但请注
  • CSS:仅显示字符串的前两个字母

    是否可以使用纯 CSS 仅显示字符串的前两个字母 到目前为止我已经尝试过 没有成功 first letter 仅针对第一个字母 无论如何对我不起作用 nth of everything 需要额外的 javascript 文本溢出 省略号 正
  • 当共享相同的行和列时,将网格项设置为不重叠

    现在 当两个网格项共享相同的行和列时 它们会相互重叠 div class some grid container div Item 1 div div Item 2 div div 我如何让它们不重叠 当共享相同的行和列时 其行为可能类似于
  • 先按行再按列布局 div

    我有一个容器div and 3 div里面如下 div div 1 div div 2 div div 3 div div 不知道每一个的内容div内部 但它们的高度和宽度是可变的 集装箱的高度由最高的决定div inside 我想展示这些
  • Vue js动态导入组件

    我有以下父组件 它必须呈现动态子组件列表
  • 如何处理 Nuxt 中导致页面渲染崩溃的 apollo 客户端错误?

    我目前正在维护一个生产 Nuxt js Vue 应用程序 该应用程序集成了 GraphQL Apollo 客户端 该客户端遇到页面渲染错误 为了增加获得回复的机会 我构建了一个简单的代码示例 仅展示我们遇到的问题 谢谢大家 源代码 Clie
  • 将图像作为框架放置在 iframe 周围?

    我有一个网站尝试以移动格式显示 但在宽屏幕上 我确信 iframe 是正确的选择 我正在尝试将 iframe 加载到 iPhone 的图像中 你可以看一个例子here http webfro gs south tour iframe tes

随机推荐

  • VSCode调试PHP

    1 安装PHP Debug 2 配置php ini xdebug zend extension D DEVTOOLS PHP php72 ext php xdebug 2 7 1 7 2 vc15 x86 64 dll zend exten
  • 计算机网络(十)CSMA详解

    随机访问截止访问控制 所有用户可随机发送信息 发送信息时占全部带宽 分类 CSMA 先听再说 CSMA CD 先听再说 边听边说 CSMA CA CSMA协议 载波监听多路访问协议 CS 载波监听 每个站点在发送数据之前要检测一下总线上是否
  • Python中列表元素的操作(追加、插入、删除、赋值、排序、拷贝)

    一 列表元素的追加和插入 append 追加一个元素到列表 extend 列表拉伸 追加多个元素到列表 insert 在指定索引处插入元素 二 列表元素的删除 pop a 弹出列表中索引为a的元素 默认为列表最后一个元素 pop 弹出的值可
  • FreeRTOS创建动态任务(SRAM动态内存)

    动态内存的管理方法 在SRAM中定义一个大的数组 也就是堆内存 供FreeRTOS的动态内存分配函数使用 在第一次使用的时候 系统会将定义的内存进行初始化 这些代码在FreeRTOS提供的内存管理方案中实现 系统所有堆得大小 define
  • ndk-build配置、Android Studio jni的配置以及jni常见问题的解决

    最近项目用到了jni比较频繁 android studio 配置jni也是必须的 但不知道是不是运气问题 我在自己电脑使用jni一点问题都没有 可以说是无障碍 但是 一使用公司电脑配置就出现了一大片编译报错 编译不通过的问题 抱着不怕搞事情
  • 268道Go语言面试真题及详解+100例代码实例+DDD实践

    Go最近动静挺大的 刚刚发布的1 18包含以下几大特性 1 泛型 2 模糊测试 Fuzzing 3 工作空间 Workspaces 4 20 性能提升 Apple M1 ARM64 和 PowerPC64 用户开心了 由于 Go 1 17
  • SSH(ssh: connect to host localhost port 22: Connection refused)问题的解决

    centos默认并没有安装ssh服务 如果通过ssh链接centos 需要自己手动安装openssh server 判断是否安装ssh服务 可以通过如下命令进行 输入 ssh localhost 如果 输出 ssh connect to h
  • 如何使用计算机查询本机网卡信息,本机mac地址查询的三种方法

    现在电脑非常流行 大部分的学生以及白领或者说每一个家庭几乎都有一台电脑 不过大家对于电脑的认识却没有这么高的普及度 很多人对于它的了解仅仅停留在使用电脑看视频用软件的层面 对于电脑自身的认识不是很多 例如本机mac地址查询这个问题就难倒了很
  • ubuntu 12.04安装OpenGL

    安装 建立基本编译环境 首先不可或缺的 就是编译器与基本的函式库 如果系统没有安装的话 请依照下面的方式安装 sudo apt get install build essential 安装OpenGL Library 接下来要把我们会用到的
  • torchvision详细介绍

    前言 深度学习道路漫漫 唯有不断总结 脚踏实地才能造就一番就成 也不断勉励自己 不要放弃 相信自己可以的 共勉 torchvision简介 torchvision是pytorch的一个图形库 它服务于PyTorch深度学习框架的 主要用来构
  • 基础不牢地动山摇之IO流1(File、FilelnputStream、FileOutputStream)

    目录 文件与文件流理解 创建文件常用的三种方式 File构造方法 获取文件信息 目录的操作和文件删除 1 删除文件 2 删除目录 3 创建多级目录 IO流原理及流的分类 原理 分类 IO流体系图 常用的类 InputStream 字节输入流
  • matlab最小二乘法_最小二乘法原理详解

    本文是 Least squares approximation 的学习笔记 这个视频从线性代数的角度 对最小二乘法的原理讲解的通俗易懂 1 提出问题 如上图所示 A 是一个n行k列的矩阵 每行可以看作是一个观测数据 或者一个训练样本 的输入
  • 包区别 版本_Lerna-如何优雅地管理多个npm包

    关于 Lerna Lerna A tool for managing JavaScript projects with multiple packages lerna js org 对于 lerna 的两段描述 A tool for man
  • 锐捷路由技术系列

    1 锐捷路由技术 锐捷路由器基本功能的初始化配置 主机名 推荐配置 Ruijie config hostnameNAME txt 将设备命名为NAME txt 接口描述 推荐配置 XWRJ config interfaceinterface
  • Linux less命令和Linux head命令

    less 工具也是对文件或其它输出进行分页显示的工具 应该说是linux正统查看文件内容的工具 功能极其强大 less 的用法比起 more 更加的有弹性 在 more 的时候 我们并没有办法向前面翻 只能往后面看 但若使用了 less 时
  • python之类、对象详解,实例化代码示例,构造函数与析构函数,私有属性和方法

    世界万物节皆可分类 世界万物皆可对象 只要对象 肯定属于某种类 只要对象 肯定有属性 类 具有相同属性 方法对象的抽象 对象 类的实例化 每个对象可有不同属性 类的三大特性 封装 将数据方法放到类里 类就变成了一个胶囊或者容器 继承 一个类
  • 安装Cpython解释器(day02)

    安装Cpython解释器 Python解释器目前已支持所有主流操作系统 在Linux Unix Mac系统上自带Python解释器 在Windows系统上需要安装一下 具体步骤如下 1 1 下载python解释器 打开官网 https ww
  • Sql执行平时都很快但是偶尔就会很慢

    Sql执行平时都很快但是偶尔就会很慢 记录一下在翻看MySQL技术文章的资料 觉得很不错就自己记录一下 大部分来源于网络 SQL执行变慢的原因 一条Sql执行很慢 那是每次执行都慢还是偶尔慢 简单的总结一下 一 针对偶尔慢的原因 数据库在刷
  • 蓝桥杯零基础冲过赛-第22天

    注意 因为蓝桥杯大部分题目都会涉及到数据规模过大问题 所以大整数是解决数据规模过大的问题的其中一种最简便的方式 核心 竖式个位对齐原理 文章目录 大整数加法 大整数减法 大整数乘法 大整数除法 大整数余数 大整数加法 意义 因为数据类型有s
  • 摸鱼时间少? 是时候学会用Vue自定义指令进行业务开发了

    文章目录 前言 一 博主用Vue自定义指令在业务中实现了什么需求 1 首屏Loading切换指令 用来占位 支持调节Loading样式 2 复制指令 3 文件流形式下载后端数据 转blob下载 4 防抖 支持设置延迟时间 5 按钮或菜单权限