Bootstrap

vue实现图标拖拽框选功能

效果:

QQ录屏20220718094326

index.vue

<template>
  <plan-time :planData="planData" @changePlanData="changePlanData" />
</template>
<script>
import PlanTime from "@/components/PlanTime";
const pd = [
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
];
export default {
  components: { PlanTime },
  data() {
    return {
      planData: pd,
    };
  },
  methods: {
    changePlanData(id, fv) {
      this.planData = this.planData.map((v, index) => {
        return index === id ? fv : v;
      });
    },
  },
};
</script>

PlanTime.vue

<template>
  <div class="plans-div" title="" style="user-select: none">
    <span style="color: #8e8b8b; font-style: italic; line-height: 24px">
      录像时段 蓝色表示保留 灰色表示删除 单击选择或取消
      <!--      录像时段  蓝色表示保留 灰色表示删除 单击选择或取消 单击不放滑动可快速选择 单击标题可反选行或列-->
    </span>
    <div class="plans" @mousedown="handleMouseDown">
      <div class="plan-row-header">
        <div class="plan-day plan-header" style="cursor: pointer">
          <i title="点击还原" class="fa fa-refresh"></i>
        </div>
        <div class="plan-hours">
          <!-- <div
            v-for="(item, i) in time"
            :key="i"
            title="单击反选该列"
            class="plan-hour plan-header"
            @click="timeClick(item)"
          >
            {{ i }}时
          </div> -->
          <div v-for="(item, i) in time" :key="i" class="plan-hour">
            <div class="plan-title">{{ i }}时</div>
          </div>
        </div>
      </div>
      <div v-for="(item, wi) in week" class="plan-row" :key="wi">
        <!-- <div title="单击反选该行" class="plan-day" @click="dayClick(item.id)">
          {{ item.name }}
        </div> -->
        <div class="plan-day">{{ item.name }}</div>
        <div class="plan-hours">
          <div v-for="(v, ti) in time" class="plan-hour" :key="ti">
            <div
              class="plan"
              :id="'hour-' + getHourId(wi, ti)"
              :class="
                isCheck(getHourId(wi, ti))
                  ? 'plan-hour-half'
                  : 'plan-hour-half-no'
              "
              :title="setHourTitle(item, ti, isCheck(getHourId(wi, ti)))"
              @click="hourClick(getHourId(wi, ti))"
            />
            <div
              class="plan"
              :id="'hour-' + getHourId(wi, ti, true)"
              :class="
                isCheck(getHourId(wi, ti, true))
                  ? 'plan-hour-half'
                  : 'plan-hour-half-no'
              "
              :title="setHourTitle(item, ti, isCheck(getHourId(wi, ti, true)))"
              @click="hourClick(getHourId(wi, ti, true))"
            />
          </div>
        </div>
      </div>
      <div
        class="mask"
        v-show="positionList.is_show_mask"
        :style="
          'width:' +
          mask_width +
          'left:' +
          mask_left +
          'height:' +
          mask_height +
          'top:' +
          mask_top
        "
      ></div>
    </div>
  </div>
</template>

<script>
const pd = [
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
];
export default {
  name: "PlanTime",
  props: {
    planData: {
      type: Array,
      default: () => {
        return pd;
      },
    },
  },
  data() {
    return {
      week: [
        { id: 1, name: "星期一" },
        { id: 2, name: "星期二" },
        { id: 3, name: "星期三" },
        { id: 4, name: "星期四" },
        { id: 5, name: "星期五" },
        { id: 6, name: "星期六" },
        { id: 7, name: "星期日" },
      ],
      time: 24,
      clickX: null,
      clickY: null,
      positionList: {
        is_show_mask: false,
        box_screen_left: 0,
        box_screen_top: 0,
        start_x: 0,
        start_y: 0,
        end_x: 0,
        end_y: 0,
      },
      plans: [],
      plansDom: null,
      flag: true, //判断是执行点击事件还是mousedown事件
      firstTime: 0,
      lastTime: 0,
    };
  },
  computed: {
    mask_width() {
      return `${Math.abs(
        this.positionList.end_x - this.positionList.start_x
      )}px;`;
      // return '100px'
    },
    mask_height() {
      return `${Math.abs(
        this.positionList.end_y - this.positionList.start_y
      )}px;`;
    },
    mask_left() {
      return `${
        Math.min(this.positionList.start_x, this.positionList.end_x) -
        this.positionList.box_screen_left
      }px;`;
    },
    mask_top() {
      return `${
        Math.min(this.positionList.start_y, this.positionList.end_y) -
        this.positionList.box_screen_top
      }px;`;
    },
  },
  mounted() {
    this.plansDom = document.getElementsByClassName("plans")[0];
    this.positionList.box_screen_left = this.plansDom.getClientRects()[0].left;
    this.positionList.box_screen_top = this.plansDom.getClientRects()[0].top;
    this.plans = document.getElementsByClassName("plan");
    this.plans = Array.from(this.plans);
  },
  methods: {
    getHourId(wi, ti, f) {
      let id = wi * 48 + ti * 2;
      if (f) {
        id = wi * 48 + ti * 2 + 1;
      }
      return id;
    },
    setHourTitle(item, ti, isCheck) {
      let title =
        "蓝色表示保留(" +
        item.name +
        "(" +
        this.format(ti * 60) +
        "-" +
        this.format(ti * 60 + 30) +
        ")录像";
      if (!isCheck) {
        title =
          "灰色色表示删除(" +
          item.name +
          "(" +
          this.format(ti * 60 + 30) +
          "-" +
          this.format(ti * 60 + 60) +
          ")录像";
      }
      return title;
    },
    isCheck(id) {
      return this.planData[id];
    },
    // timeClick(v) {
    //   console.log(v);
    // },
    // dayClick(v) {
    //   console.log(v);
    // },
    hourClick(id) {
      if (!this.flag) {
        this.$emit("changePlanData", id, this.isCheck(id) ? 0 : 1);
      }
      this.flag = true;
    },
    format(seconds) {
      let min =
        Math.floor(seconds / 60) >= 10
          ? Math.floor(seconds / 60)
          : "0" + Math.floor(seconds / 60);
      seconds -= 60 * min;
      let sec = seconds >= 10 ? seconds : "0" + seconds;
      return min + ":" + sec;
    },
    handleMouseDown(event) {
      this.firstTime = new Date().getTime;
      this.lastTime = new Date().getTime;
      if (this.lastTime - this.firstTime < 1000) {
        this.flag = false;
      }
      this.positionList.is_show_mask = true;
      this.positionList.start_x = event.clientX;
      this.positionList.start_y = event.clientY;
      this.positionList.end_x = event.clientX;
      this.positionList.end_y = event.clientY;
      this.positionList.box_screen_left =
        this.plansDom.getClientRects()[0].left;
      this.positionList.box_screen_top = this.plansDom.getClientRects()[0].top;
      document.body.addEventListener("mousemove", this.handleMouseMove); //监听鼠标移动事件
      document.body.addEventListener("mouseup", this.handleMouseUp); //监听鼠标抬起事件
    },
    handleMouseMove(event) {
      this.positionList.end_x = event.clientX;
      this.positionList.end_y = event.clientY;
    },
    handleMouseUp() {
      document.body.removeEventListener("mousemove", this.handleMouseMove);
      document.body.removeEventListener("mouseup", this.handleMouseUp);
      this.positionList.is_show_mask = false;
      this.handleDomSelect();
      this.resSetXY();
    },
    handleDomSelect() {
      const dom_mask = document.querySelector(".mask");
      //getClientRects()每一个盒子的边界矩形的矩形集合
      const rect_select = dom_mask.getClientRects()[0];
      this.plans.forEach((node, index) => {
        const rects = node.getClientRects()[0];
        if (this.collide(rects, rect_select) === true) {
          this.$emit("changePlanData", index, this.isCheck(index) ? 0 : 1);
        }
      });
      console.log(this.plans);
    },
    //比较checkbox盒子边界和遮罩层边界最大最小值
    collide(rect1, rect2) {
      const maxX = Math.max(rect1.x + rect1.width, rect2.x + rect2.width);
      const maxY = Math.max(rect1.y + rect1.height, rect2.y + rect2.height);
      const minX = Math.min(rect1.x, rect2.x);
      const minY = Math.min(rect1.y, rect2.y);
      return (
        maxX - minX <= rect1.width + rect2.width &&
        maxY - minY <= rect1.height + rect2.height
      );
    },
    //清除
    resSetXY() {
      this.positionList.start_x = 0;
      this.positionList.start_y = 0;
      this.positionList.end_x = 0;
      this.positionList.end_y = 0;
    },
  },
};
</script>

<style lang="scss">
.plans-div {
  .plans {
    font-size: 12px;
    text-align: center;
    border: 0;
    width: 100%;
    min-width: 869px;
    margin: auto;
    position: relative;
    .mask {
      position: absolute;
      background: blue;
      opacity: 0.2;
      z-index: 100;
      left: 0;
      top: 0;
    }
  }

  .plan-header {
    font-weight: 700;
    height: 24px !important;
    color: #eee;
    line-height: 24px !important;
  }

  .plan-header:hover {
    background-color: #1d5d94 !important;
  }

  .plan-row-header {
    height: 24px;
    width: 100%;
  }

  .plan-row {
    height: 60px;
    width: 100%;
    border-top: 1px;
    border-style: solid none none;
    border-color: transparent;
  }

  .plan-day {
    width: 4%;
    height: 100%;
    float: left;
    background-color: #515a6e;
    color: #eee;
    line-height: 60px;
  }

  .plan-day:hover {
    background-color: #226dae;
  }

  .plan-hours {
    width: 92%;
    height: 100%;
    float: left;
  }

  .plan-hour {
    height: 100%;
    width: 4%;
    float: left;
    font-size: 12px;
    border-style: none none none solid;
    border-color: transparent;
    border-left-width: 1px;
  }

  .plan-title {
    font-weight: 700;
    height: 24px !important;
    color: #eee;
    line-height: 24px !important;
    background-color: #515a6e;

    &:hover {
      background-color: #1d5d94;
    }
  }

  .plan-hour-half {
    width: 100%;
    height: 50%;
    background-color: #009fe7;
    font-size: 10px;
    line-height: 30px;
    cursor: pointer;
  }

  .plan-hour-half-no {
    width: 100%;
    height: 50%;
    background-color: #ddd;
    font-size: 10px;
    line-height: 30px;
    cursor: pointer;
  }

  .plan-hour-half-no:hover {
    background-color: #f7f7f7;
  }

  .plan-hour-half:hover {
    background-color: #23d4ff;
  }
}
</style>

;