vue仿windows实现单选、多选、连选、框选效果

简介: vue实现单机单选,按Ctrl+多选,按shift+连选,鼠标框选效果。

最近博主公司出现一个需求,做一个云盘项目(或许未来的你能用得上),让仿照windows系统实现多选功能,虽然项目上就放一个Ctrl+多选,但我一开心,历经两天,其他的也都实现了。查阅各种资料,某度上天花乱坠,但很多方法还是严重不合格。所以将个人研究的公布出来,大家喜欢可借鉴,研发不易,希望给个赞👍。

框选效果
image.png
Ctrl 多选效果
image.png
Shift 连选效果
image.png

300行代码实现所有功能,简单做了一些注释,将思路给大家,专研一下,能看懂。需要注意的是,毕竟html页面实现,性能上还需继续优化。

实现功能:

1、点击Ctrl ,可多选,可取消选过文件。
2、点击shift,点击两个,中间可连选。
3、按下鼠标移动,可框选。
4、单机,可单选。
5、按下空白处,取消所有选项。

研发思路:

1、所有实现中,都使用列表中的index作为选中标识。并放进一个独立数组中,我起名为 selectList 。
2、所有list列表中添加一个标识,作为是否选中的样式判断,这样做是最方便的,标识没有自己手动添加,或让后端给加入一个标识字段,对他们来说很简单。
3、每次选中后,都将遍历所有list列表,将符合条件的index对象中,标识变为true,否则为false。
4、监听键盘事件,获取到shift 和ctrl的keycode,由于博主mac本,所以需要多加一个command 。

一、多选思路:
点击时判断 selectList 数组中有没有重复index,有则删除,没有则添加。

二、连选思路:
a、连选是随意点击两个li,并将中间的全部选中。
b、判断 selectList.length 是否大于1,判断最低是两个数,可以实现连选。
c、用户可能从前往后选,也可能从后往前选,所以判断 selectList 的第一个index 和最后一个 index 的大小,并从小到大排序,做一个for循环,将中间index都填入到 selectList 中,遍历点亮数组。

注意:本次实现,将ctrl点击的一同带入,所以和 windows 系统略有差异。

三、框选思路:
a、css禁用浏览器默认选择效果,本次用的fixed定位。
b、js实现选框的效果。
c、判断框选的覆盖范围,与list的交集,拿到交集的index。

框选避坑处:
a、现实开发中,最外层盒子和ul的高度未必统一,以及ul 的高度未必100%,我们可能回根据最外层盒子做框选效果,所以建议用fixed定位。
b、获取 li 坐标。因为所有 li 的盒子的坐标,是针对父盒子做的定位,并不是针对窗口坐标位置,所以需要加上父盒子针对窗口的坐标高度,就是li 针对窗口的坐标。
c、页面滚动时,父盒子针对窗口的坐标高度不会更新,所以监听滚动时间,实时更新变化。
b、注意性能问题,本次没有实时更新 li 坐标,而是每个 li 直接加上父盒子针对窗口的坐标高度。

代码展示:

<template>
  <div id='se'
       @mousedown.stop="handleBox">
    <ul id="ul">
      <li v-for="(item,index) in list"
          :key="index"
          @click.stop="handleClick(index)"
          :class="item.isS?'isS':''"
          class="li">{{item.name+ (index+1)}}</li>
    </ul>
    <div v-show="isShowSeBox"
         id="selection"><span>VX:VillinWeChat</span><span>🙏谢谢送个👍</span></div>
  </div>
</template>

<script>
export default {
  components: {},
  data() {
    return {
      list: [
        {
          name: '列表',
          id: 1,
          isS: false,
        },
        {
          name: '列表',
          id: 2,
          isS: false,
        },
        {
          name: '列表',
          id: 3,
          isS: false,
        },
        {
          name: '列表',
          id: 4,
          isS: false,
        },
        {
          name: '列表',
          id: 5,
          isS: false,
        },
        {
          name: '列表',
          id: 6,
          isS: false,
        }, {
          name: '列表',
          id: 7,
          isS: false,
        }, {
          name: '列表',
          id: 8,
          isS: false,
        }, {
          name: '列表',
          id: 9,
          isS: false,
        }, {
          name: '列表',
          id: 10,
          isS: false,
        },
        {
          name: '列表',
          id: 11,
          isS: false,
        },
        {
          name: '列表',
          id: 12,
          isS: false,
        },
      ],
      isshift: false, // 是否按下 shift
      isctrl: false, // 是否按下 ctrl
      isonkeydown: false, // 是否按下键盘
      selectBox: null, // 框选盒子
      isShowSeBox: false, // 框选是否显示div
      selectList: [], // 每次点击,push进去点击的index
      lipL: 0,  // 框选li的父盒子针对于窗口的left
      lipT: 0, // 框选li的父盒子针对于窗口的top
    };
  },
  mounted() {
    this.selectBox = document.getElementById("selection");
    this.parentBox = document.getElementById("se");
    this.handleScroll()
  },
  computed: {},
  watch: {},
  methods: {
    handleBox(even) {
      if (!this.isctrl && !this.isshift && this.selectList.length != 0) {
        this.selectList = []
        this.loop()
      }
      // 按下时的坐标
      this.downX = even.clientX
      this.downY = even.clientY
      this.selectBox.style.left = this.downX + "px";
      this.selectBox.style.top = this.downY + "px";
      document.body.addEventListener('mousemove', this.moveselecttion)
      document.body.addEventListener('mouseup', this.mouseupSelection)
      window.addEventListener("scroll", this.handleScroll)
      window.addEventListener('keydown', this.clickKeydown)
      window.addEventListener('keyup', this.clickKeyup)
    },
    // 页面滚动  滚动时,实时更新父盒子横纵坐标
    handleScroll() {
      //获取li父元素的纵坐标(相对于窗口)
      this.lipT = this.parentBox.getBoundingClientRect().top
      //获取li父元素的横坐标(相对于窗口)
      this.lipL = this.parentBox.getBoundingClientRect().left
    },
    // 键盘按下
    clickKeydown(e) {
      switch (e.keyCode) {
        case 16:
          this.isshift = true;
          break;
        case 17: // window 键盘
          this.isctrl = true;
          break;
        case 91:  // mac command  按键
          this.isctrl = true;
          break;
      }
    },
    // 键盘松开
    clickKeyup(e) {
      this.isshift = false
      this.isctrl = false
    },
    handleClick(index) {
      // shift 连选
      if (this.isshift) {
        if (this.selectList.indexOf(index) == -1) {
          this.selectList.push(index)
        }
        if (this.selectList.length > 1) {
          // 按 shift 连选,通过判断第一次点击index和最后一次index,中间全部选中
          let le = this.selectList.length
          var min = Math.min(this.selectList[0], this.selectList[le - 1])
          var max = Math.max(this.selectList[0], this.selectList[le - 1])
          this.selectList = []  // 防止最初始的两次点击,多处的两个index
          for (let i = min; i <= max; i++) {
            this.selectList.push(i)
          }
        }
        this.loop()
      }
      // ctrl 多选 可重复单机取消
      else if (this.isctrl) {
        if (this.selectList.indexOf(index) == -1) {
          this.selectList.push(index)
        } else {
          // 此处判断是否存在高亮index,如果存在,再次点击后,则删除此index,达到二次点击取消选区效果
          this.selectList.splice(this.selectList.indexOf(index), 1)
        }
        this.loop()
      }
      // 单选
      else {
        this.selectList = []
        this.selectList.push(index)
        this.loop()
      }
    },

    // 鼠标移动事件
    moveselecttion(even) {
      this.isShowSeBox = true;
      this.selectBox.style.left = Math.min(even.clientX, this.downX) + "px";
      this.selectBox.style.top = Math.min(even.clientY, this.downY) + "px";
      this.selectBox.style.width = Math.abs(this.downX - even.clientX) + "px";
      this.selectBox.style.height = Math.abs(this.downY - even.clientY) + "px";
      this.covered()
    },
    // 鼠标松开事件
    mouseupSelection(even) {
      this.isShowSeBox = false;
      this.selectBox.style.width = 0 + "px";
      this.selectBox.style.height = 0 + "px";
      document.body.removeEventListener('mousemove', this.moveselecttion)
      document.body.removeEventListener('mouseup', this.mouseupSelection)
    },
    // 是否被覆盖
    covered() {
      let li = document.getElementsByClassName('li')
      if (!this.isctrl && !this.isctrl) {
        this.selectList = []
      }
      let l = parseInt(this.selectBox.style.left)
      let t = parseInt(this.selectBox.style.top)
      let w = parseInt(this.selectBox.style.width)
      let h = parseInt(this.selectBox.style.height)
      for (let i = 0; i < li.length; i++) {
        if (this.selectList.indexOf(i) == -1) {
          //  子元素是相对父元素定位的,所以父元素有定位高度,offsetLeft 和 offsetTop不准
          let sl = li[i].offsetWidth + li[i].offsetLeft + this.lipL;
          let st = li[i].offsetHeight + li[i].offsetTop + this.lipT;
          if (sl > l && st > t && li[i].offsetLeft + this.lipL < l + w && li[i].offsetTop + this.lipT < t + h) {
            this.selectList.push(i)
          }
          this.loop()
        }

      }
    },
    // 循环遍历,在这里,可以拿到所选的item
    loop() {
      this.list.forEach((e, index) => {
        e.isS = false
        this.selectList.filter(item => {
          if (item == index) {
            e.isS = true
          }
        })
      });
    },
  },
  destroyed() {
    document.body.removeEventListener('mousemove', this.moveselecttion)
    document.body.removeEventListener('mouseup', this.mouseupSelection)
    window.removeEventListener("scroll", this.handleScroll)
    window.removeEventListener('keydown', this.clickKeydown)
    window.removeEventListener('keyup', this.clickKeyup)
  }
}
</script>
<style lang='less' scoped>
#se {
  position: relative;
  width: 100%;
  height: 100%;
  -moz-user-select: none; /*火狐*/
  -webkit-user-select: none; /*webkit浏览器*/
  -ms-user-select: none; /*IE10*/
  -khtml-user-select: none; /*早期浏览器*/
  user-select: none;
}
ul {
  display: flex;
  flex-wrap: wrap;
  width: 500px;
  margin: 50px auto;
  li {
    box-sizing: border-box;
    padding: 5px 10px;
    border: 1px solid #ccc;
    margin: 10px;
    cursor: pointer;
  }
  .isS {
    background: #99f;
  }
}
#selection {
  position: fixed;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background: linear-gradient(
    to top left,
    rgba(218, 27, 198, 0.5),
    rgba(255, 222, 32, 0.2)
  );
  color: rgb(36, 138, 129);
  font-size: 16px;
  width: 0px;
  height: 0px;
  overflow: hidden;
}
</style>

欢迎提出宝贵意见。
喜欢请点赞!

目录
相关文章
|
JavaScript 前端开发 PHP
【Vue+NodeJS】vue路由及NodeJS环境搭建(Windows版)
Node.js是一个基于Chrome V8引擎构建的JavaScript运行时环境使用了一个事件驱动、非阻塞式I/O 的模型;可以让JavaScript在服务器端运行,它让JavaScript成为与PHP、Python、Perl、Ruby等服务端语言平起平坐的脚本语言。相对于传统的JavaScript运行环境(如浏览器),Node.js具有以下特点:事件驱动:Node.js使用事件驱动的非阻塞I/O模型,使得在处理大量并发请求时效率更高。单线程。
|
JavaScript
48Vue - 绑定 value(单选按钮、选择列表设置)
48Vue - 绑定 value(单选按钮、选择列表设置)
55 0
|
3月前
|
JavaScript
Vue使用element中table组件实现单选一行
如何在Vue中使用Element UI的table组件实现单选一行的功能。
161 5
Vue使用element中table组件实现单选一行
|
2月前
|
JavaScript Windows
windows安装vue
windows安装vue
|
5月前
|
JavaScript
vue 弹窗翻页多选
vue 弹窗翻页多选
26 2
|
5月前
|
前端开发 JavaScript 应用服务中间件
windows server + iis 部署若伊前端vue项目
5,配置url重写规则(重写后端请求) 注:如果没有Application Request Routing Cachefourcloudbdueclaim和URL重写,则是第二部的那两个插件没装上 打开iis,点击计算机->点击Application Request Routing Cache -> 打开功能
268 0
|
5月前
|
JavaScript
vue 自定义单选样式 radio
vue 自定义单选样式 radio
50 0
|
7月前
|
JavaScript
解决Vue 3 + Element Plus树形表格全选多选以及子节点勾选的问题
解决Vue 3 + Element Plus树形表格全选多选以及子节点勾选的问题
|
7月前
|
数据库
vue+elementui中,el-select多选下拉列表中,如何同时获取:value和:label的值?
vue+elementui中,el-select多选下拉列表中,如何同时获取:value和:label的值?
|
7月前
|
缓存 JavaScript 前端开发
vue + element Table的数据多选,多页选择数据回显,分页记录保存选中的数据。
vue + element Table的数据多选,多页选择数据回显,分页记录保存选中的数据。
113 0