记录: 思考如何做一个拖动计算时间组件

背景

看到一个有意思的交互:拖动计算时间的插件,从0到24点的时间,可以左右前后拖动,并有时间选择限制,拖动的时间不能覆盖重叠;那么有什么思路该怎么做呢。学着实现一下看看,大家有什么思路可以交流学习。

效果图

先看下要实现的大致效果:

思路

  1. 基于鼠标事件,mousemove等,根据返回的事件信息,获取移动和位置数据;
  2. 有2个辅助函数,将 px 转位 time 的函数,以及 将 time 转为 px 的函数。
  3. 基于鼠标事件,记录事件开始和事件结束的数据。
  4. 判断根据事件信息拖动方向,首次以鼠标按下的点为原点,如果拖动两边的侧边条的话,例:拖动左边的侧边条,以右侧为参照物判断是向左拖动还是向右拖动,反之亦然。

问题

  1. 目前实现出来还是有bug和性能优化问题,
  2. 时间区域的整块的拖动移动的(drag事件)没有实现。

参考代码

以下上基于Vue2 实现的示例代码:

vue 复制代码
<!--
  -- @description 周计划-时间片段-时间区间
-->
<template>
  <div class="au-week-plan-group" @click.stop>
    <!-- 时间线(竖直的时间线) -->
    <div class="au-week-plan-group__panel-wrap">
      <div class="au-week-plan-group__panel-wrap">
        <div class="au-week-plan-group__tick__line" v-for="(_, index) in 24" :key="index"></div>
      </div>
    </div>

    <!-- 时间刻度(0-24点) -->
    <div class="au-week-plan-group__time__line-wrap">
      <ul class="au-week-plan-group__time__line">
        <li class="au-week-plan-group__scale" v-for="(item,index) in timeList" :key="item">
          <span class="au-week-plan-group__scale-label">
            {{ item }}
          </span>
          <span class="au-week-plan-group__scale-label au-week-plan-group__scale-label-last"
                v-if="(index === timeList.length -1)">
            24
          </span>
        </li>
      </ul>
    </div>

    <!-- 鼠标hover到星期几的时候的hover行遮罩   -->
    <div class="au-week-group__hover" :style="{top: dynamicTopOfHover + 'px'}" v-if="isShowHover"></div>

    <!-- 从星期一到星期天的列表 -->
    <div class="au-week-plan" v-for="(item, index) in weekList" :key="index"
         @mouseenter="handleMouseEnterByPlan(index)"
         @mouseleave="handleMouseLeaveByPlan($event,item,index)"
    >
      <!-- 星期几列表 -->
      <div class="au-week-plan__label">
        <span>{{ item.dayOfWeekName }}</span>
      </div>

      <!-- 动作 -->
      <div class="au-week-plan__action">
        <div class="au-week-plan__action-buttons" v-if="mode === 'edit' || mode === 'add'">
          <span class="el-popover-wrapper" :class="{'show': index === targetIndexOfHover}">
            <!-- 复制 和 清空按钮 -->
            <el-popover
              v-model="item.visiblePopoverOnCopy"
              trigger="manual"
              width="400"
              class="au-week-plan__action_buttons__copy"
              popper-class="au-week-plan__action_buttons__copy"
              placement="left"
              title="复制到"
            >
                <div class="button__copy">
                  <el-checkbox :indeterminate="isIndeterminate" v-model="checkAll" @change="handleCheckAllChange">全选</el-checkbox>
                  <el-checkbox-group v-model="copyToList" @change="handleCheckedChange">
                    <el-checkbox
                      v-for="week in weekList"
                      :key="week.dayOfWeekType"
                      :label="week.dayOfWeekType"
                    >
                      <span :class="{'is-target-week': copyTargetKey === week.dayOfWeekType}">{{ week.dayOfWeekName }}</span>
                    </el-checkbox>
                  </el-checkbox-group>
                  <div class="action__copy">
                    <el-button size="mini" @click="handleCloseCopyPopover(item)">取消</el-button>
                    <el-button size="mini" type="primary" @click="handleConfirmCopy(item)">确定</el-button>
                  </div>
                </div>
                <i class="el-icon-document-copy" slot="reference" @click="handleOpenCopyPopover(item, index)"></i>
            </el-popover>
            <i class="el-icon-delete" v-if="Array.isArray(item.data) && !!item.data.length" @click="handleClearCopy(item)"></i>
          </span>
        </div>
      </div>

      <div class="au-week-plan__wrapper"
      >
        <!-- 滑动条的区间定位,多个 au-week-plan__range-wrap -->
        <div class="au-week-plan__range-wrap"
             v-for="(range, i) in item.data"
             :key="i"
             :style="range.style"
        >
          <!-- 指定时间段中的,设置时间弹窗 -->
          <div class="el-popover-wrap">
            <!-- 查看状态 -->
            <!--  编辑状态时, 设置时间 -->
            <el-popover
              v-model="range.options.visiblePopoverOnTimeRange"
              trigger="manual"
              placement="top"
              width="226"
              class="au-week-plan__popover-time-range"
              popper-class="au-week-plan__popover-time-range"
            >
              <!-- 查看状态下的popover -->
              <div v-if="mode === 'view'" class="au-week-plan__popover">
                <div class="au-week-plan__popover--time text-center">
                    <span>{{range.startTime.value}}</span>
                    <span class="-p-l-16 -p-r-16">-</span>
                    <span>{{range.endTime.value}}</span>
                </div>
              </div>

              <!-- 编辑状态下的popover -->
              <div v-if="mode === 'edit' || mode === 'add'" class="au-week-plan__popover">
                <div class="au-week-plan__popover--time">
                  <!-- selectableRange 可选时间范围 需要控制 -->
                  <el-time-picker
                    v-model="range.startTime.value"
                    prefix-icon="none"
                    :editable="false"
                    :clearable="false"
                    format="HH:mm"
                    value-format="HH:mm"
                    :picker-options="{
                        selectableRange: range.startTime.selectableRange
                      }"
                  >
                  </el-time-picker>
                  <span class="au-week-plan__popover--dividing-line"></span>
                  <el-time-picker
                    v-model="range.endTime.value"
                    prefix-icon="none"
                    :editable="false"
                    :clearable="false"
                    format="HH:mm"
                    value-format="HH:mm"
                    :picker-options="{
                        selectableRange: range.endTime.selectableRange
                      }"
                  >
                  </el-time-picker>
                </div>
                <!-- 保存时间段操作按钮 -->
                <div class="au-week-plan__popover--action">
                  <el-button type="text" @click.stop="handleDeleteRange(item, range, i)">删除</el-button>
                  <el-button type="text" @click.stop="handleSaveRange(range)">保存</el-button>
                </div>
              </div>

              <!-- 时间段两边的拖动条以及tooltip -->
              <div
                slot="reference"
                class="au-week-plan__range"
                :class="{'is-active': (mode === 'edit' || mode === 'add') && range.options.isShowSideDragBar}"
                @click.stop.self="handleRangeClick($event,item,range)"
              >
                <!-- 编辑状态下 时间段的左右拖动 工具条 -->
                <!-- 编辑状态下, 左边拖动箭头 -->
                <el-tooltip
                  v-if="mode === 'edit' || mode === 'add'"
                  :key="range.location.width+Math.random()*10000000"
                  class="au-week-plan__drag-handle--tooltip"
                  popper-class="au-week-plan__drag-handle--tooltip"
                  placement="top"
                  effect="light"
                  :tabindex="undefined"
                  :manual="true"
                  v-model="range.options.visibleTooltipLeft"
                >
                  <div slot="content">{{range.startTime.value}}</div>
                  <div
                    class="au-week-plan__drag-handle au-week-plan__drag-handle-left"
                    @mousedown.stop="handleDragHandleLeftAtMouseDown($event, item, range)"
                  >
                  </div>
                </el-tooltip>

                <!-- 右边拖动箭头 -->
                <el-tooltip
                  v-if="mode === 'edit' || mode === 'add'"
                  :key="range.location.width+Math.random()*10000000"
                  class="au-week-plan__drag-handle--tooltip"
                  popper-class="au-week-plan__drag-handle--tooltip"
                  placement="top"
                  effect="light"
                  :tabindex="undefined"
                  :manual="true"
                  v-model="range.options.visibleTooltipRight"
                >
                  <div slot="content">{{range.endTime.value}}</div>
                  <div
                    class="au-week-plan__drag-handle au-week-plan__drag-handle-right"
                    @mousedown.stop="handleDragHandleRightAtMouseDown($event,item,range)"
                  >
                  </div>
                </el-tooltip>
              </div>
            </el-popover>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import {v4 as uuidV4} from "uuid";
import dayjs from 'dayjs'

const MAX_END_TIME = `23:59:00`
const MIX_START_TIME = `00:00:00`

// 动作类型
const ACTION_TYPE = {
  DOWN: 'DOWN',
  ORIGIN_LEFT: 'ORIGIN_LEFT',
  ORIGIN_RIGHT: 'ORIGIN_RIGHT',
  LEFT_ACTION_OF_LEFT_SIDE: 'LEFT_ACTION_OF_LEFT_SIDE', // 左边的侧边条在左边活动
  RIGHT_ACTION_OF_LEFT_SIDE: 'RIGHT_ACTION_OF_LEFT_SIDE', // 左边的侧边条在右边活动
  LEFT_ACTION_OF_RIGHT_SIDE: 'LEFT_ACTION_OF_RIGHT_SIDE', // 右边的侧边条在左边活动
  RIGHT_ACTION_OF_RIGHT_SIDE: 'RIGHT_ACTION_OF_RIGHT_SIDE', // 右边的侧边条在右边活动
}
// range数据模版
const RANGE_DATA = {
  uuid: null, // range唯一标识
  options: {
    isActiveRange: false, // 开启move标识控制
    isShowSideDragBar: false, // 是否显示侧边拖动条
    visibleTooltipLeft: false, // 显示左边拖动条
    visibleTooltipRight: false, // 显示右边拖动条
    visiblePopoverOnTimeRange: false, // 是否显示时间段popover可修改时间
  },
  location: {
    left: 0, // 当前range的left值
    width: 0, // 当前range的宽度
    right: 0, // 当前range的right值
  },
  reference: { // 参照物
    origin: { // 参照点: 原点
      oEvent: { clientX: 0 },
      oTime: {
        value: "00:00",
        selectableRange: ['00:00:00 - 23:59:00']
      }
    },
    left: {
      lTime: {
        value: "00:00",
        selectableRange: ['00:00:00 - 23:59:00'],
      },
      lEvent: { clientX: 0}
    },
    right: {
      rTime: {
        value: "00:00",
        selectableRange: ['00:00:00 - 23:59:00'],
      },
      rEvent: { clientX: 0 }
    },
    side: { // 参照点: 侧边开始位置和结束位置
      startTime: {
        value: "00:00",
        selectableRange: ['00:00:00 - 23:59:00'],
      },
      startEvent: { clientX: 0 },
      endTime: {
        value: "00:00",
        selectableRange: ['00:00:00 - 23:59:00'],
      },
      endEvent: { clientX: 0 }
    },
  },
  style: {
    left: '0px',
    width: '0px'
  },
  startTime: {
    value: '00:00',
    rawValue: '00:00',
    selectableRange: ['00:00:00 - 23:59:00'],
  },
  endTime : {
    value: '00:00',
    rawValue: '00:00',
    selectableRange: ['00:00:00 - 23:59:00'],
  },
  rect: { x: 0, y: 0, left: 0, right: 0, width: 0, height: 0, top: 0, bottom: 0 },
  startEvent: {clientX: 0 },
  endEvent: {clientX: 0, },
}
// 获取总秒数 Math.round(p(this.currentOffsetX, this.panelWidth) / 60) * 60

// var e = v(this.limitRange[0])
//   , t = v(this.limitRange[1]);
// return {
//   selectableRange: "".concat(e, ":00 - ").concat(t, ":00")
// }
//   , p = function(e, t) {
//   return e * h / t
// }
//   , m = function(e, t) {
//   return e * t / h
// }
//   , v = function(e) {
//   e = 86400 === e ? 86340 : e;
//   var t = f(parseInt(e / 3600, 10))
//     , n = f(parseInt(e % 3600 / 60, 10));
//   return t + ":" + n
// }
//   , g = function(e) {
//   var t = e.split(":");
//   return 3600 * parseInt(t[0], 10) + 60 * parseInt(t[1], 10)
// }
//   , _ = function(e) {
//   e = 86400 === e ? 86340 : e;
//   var t = f(parseInt(e / 3600, 10))
//     , n = f(parseInt(e % 3600 / 60, 10));
//   return new Date(2018,1,1,t,n)
// }
//   , y = function(e) {
//   var t = new Date(e);
//   return 3600 * t.getHours() + 60 * t.getMinutes()
// }
//   , b = function(e) {
//   return e >= 86340 && e < 86400 ? e - e % 60 + 59 : e
// }
export default {
  name: "index",
  props: {
    mode: {
      type: String,
      default: 'add',
      validate(val) {
        return ['add', 'edit', 'view'].includes(val)
      }
    },
    mainPageElement: {
      type: String,
      default: '.avue-main'
    },
    maxRangeTotal: {
      type: Number,
      default: 8
    },
    timeRange: { // 传进来的数据
      type: Array,
      default: () => []
    },
  },
  data() {
    return {
      weekName: {'monday': '星期一', 'tuesday': '星期二', 'wednesday': '星期三', 'thursday': '星期四', 'friday': '星期五', 'saturday': '星期六', 'sunday': '星期日'},
      timeList: ["00", "02", "04", "06", "08", "10", "12", "14", "16", "18", "20", "22"],
      weekList: [
        {dayOfWeekType: 'monday', dayOfWeekName: '星期一', visiblePopoverOnCopy: false,
          data: []
        },
        {dayOfWeekType: 'tuesday', dayOfWeekName: '星期二', visiblePopoverOnCopy: false,
          data: []
        },
        {dayOfWeekType: 'wednesday', dayOfWeekName: '星期三', visiblePopoverOnCopy: false,
          data: []
        },
        {dayOfWeekType: 'thursday', dayOfWeekName: '星期四', visiblePopoverOnCopy: false,
          data: []
        },
        {dayOfWeekType: 'friday', dayOfWeekName: '星期五', visiblePopoverOnCopy: false,
          data: []
        },
        {dayOfWeekType: 'saturday', dayOfWeekName: '星期六', visiblePopoverOnCopy: false,
          data: []
        },
        {dayOfWeekType: 'sunday', dayOfWeekName: '星期日', visiblePopoverOnCopy: false,
          data: []
        },
      ],
      isActOnTargetElement: false,
      actionType: null, // 动作类型
      isMouseUp: false,
      isMoved: false, // 是否在mousedown后在mousemove中移动过
      isPointerDown: false, // 判断是否moving标志
      activeRow: [], // 激活的行信息
      activeRange: {}, // 当前激活的range
      timer: null, // 判断是鼠标点击还是按住拖动的时间控制器
      clickTimer: null, // 鼠标点击的定时器
      isHandleRightMoving: false, // 是否右侧移动中
      isHandleLeftMoving: false, // 是否左侧移动中
      dynamicTopOfHover: 80, // 鼠标hover的时候,动态计算top
      targetIndexOfHover: 0, // hover的时候目标的索引,也作为当前是第几行的数据标识
      isShowHover: false, // hover 的时候是否显示
      copyToList: [], // 复制到(需要复制的目标列表)
      copyTargetKey: null, // 要复制的目标KEY
      isIndeterminate: true, // 复制到弹窗中的全选和半选状态的checkbox的样式控制
      checkAll: false, // 复制到弹窗中的全选和半选状态的checkbox的样式的全选状态控制
    }
  },
  watch: {
    // 外面传进来的数据
    timeRange: {
      deep: true,
      handler(data) {
        let hasData = Array.isArray(data) && data.length > 0
        if (hasData) {
          this.convertDataAtIncoming(data)
        }
      }
    }
  },
  computed: {
    getParentRect() {
      return document.querySelector('.au-week-plan__wrapper').getBoundingClientRect() || Object.create({})
    },
    getTimeLineRowHeight() {
      const timeLineNode = document.querySelector('.au-week-plan-group__time__line-wrap')
      const targetNode = getComputedStyle(timeLineNode)
      return Number.parseInt(targetNode.height) || 0
    },
  },
  mounted() {
      this.init()
  },
  beforeDestroy() {
    clearTimeout(this.timer)
    // clearTimeout(this.clickTimer)
    document.removeEventListener('click', this.globalClick)
    document.removeEventListener('mousedown', this.handleMouseDown)
    document.removeEventListener('mouseup', this.handleListenerMouseUp)
  },
  methods: {
    init() {
      // main page的 点击事件
      document.addEventListener('click', this.globalClick)
      // add mousedown
      document.addEventListener('mousedown', this.handleMouseDown)
      // mouseup
      document.addEventListener('mouseup', this.handleListenerMouseUp)
    },
    /**
     * @description 全局点击事件
     */
    globalClick() {
        if (this.isActOnTargetElement) {
          this.isActOnTargetElement = false
        } else {
          this.handleClearActiveState()
        }
    },
    /**
     * @description 移除无效的宽度的range
     */
    invalidRemove() {
      if(!Object.keys(this.activeRange).length) return
      if (!Math.floor(this.activeRange.location.width)) {
        let rIndex = this.activeRow.data.findIndex(item => item.uuid === this.activeRange.uuid)
        this.activeRow.data.splice(rIndex, 1)
      }
    },
    /**
     * @description 关闭所有popover弹出窗口
     */
    closeAllPopover() {
      this.weekList.forEach(item => {
        item.visiblePopoverOnCopy = false
        item.data.forEach(range => {
          range.options.visiblePopoverOnTimeRange = false
          range.options.isShowSideDragBar = false
          range.options.visibleTooltipLeft = false
          range.options.visibleTooltipRight = false
        })
      })
    },
    closeOtherPopoverByRow(weekList = this.weekList,range) {
      weekList.forEach(row => {
        row.data.forEach(r => {
          r.options.visibleTooltipLeft = false
          r.options.visibleTooltipRight = false
          if (r.uuid === range.uuid) {
            r.options.visiblePopoverOnTimeRange = true
          } else {
            r.options.visiblePopoverOnTimeRange = false
            r.options.isShowSideDragBar = false
          }
        })
      })
    },
    /**
     * @description 鼠标移入 星期的那一行
     * @param index 当前行在 weekLine 中的索引
     */
    handleMouseEnterByPlan(index) {
      // 鼠标移入实现hover行高亮的效果
      this.isShowHover = true
      this.targetIndexOfHover = index
      this.getHoverEffect(index)
    },
    /**
     * @description 获取hover的时候,动态hover行的top高度
     * @param index 移动时在 weekLine 中的索引
     */
    getHoverEffect(index = 0) {
      let indexHeight = index * 40
      this.dynamicTopOfHover = this.getTimeLineRowHeight + indexHeight
    },
    /**
     * @description 鼠标离开 星期的那一行
     * @param event 事件对象
     * @param row 当前行数据
     * @param index 当前行在 weekLine 中的索引
     */
    handleMouseLeaveByPlan(event, row, index) {
      // 为了保证移动顺滑的top动画,在离开的时间判断,是否是离开了au-week-plan元素区域
      const {clientX, clientY} = event
      const element = document.elementFromPoint(clientX, clientY)
      const isInside = element.className.includes('au-week-plan__wrapper')
      if (!isInside) {
        this.isShowHover = false
      }
    },
    handleClearActiveState() {
      this.activeRow = []
      this.activeRange = Object.create(null)
      this.closeAllPopover()
    },
    /**
     * @description 时间段滑块被激活
     * @param event 事件对象
     * @param row row 行对象
     * @param range range本身
     */
    handleRangeClick(event, row, range) {
      // TODO 点击激活range
      this.activeRow = row
      this.activeRange = range
      // ------------ 控制操作 ----------------
      // clearTimeout(this.clickTimer)
      // this.isMoved = true
      this.closeOtherPopoverByRow(this.weekList, range)
      range.options.isShowSideDragBar = true

      range.startTime.value = this.formatTimeIntoHourAndMinutes(range.startTime.rawValue)
      range.endTime.value = this.formatTimeIntoHourAndMinutes(range.endTime.rawValue)
      if (this.mode === 'edit') {
        // ----------- 重新调整startEvent 和 entEvent 的 clientX  ----------------
        range.rect = this.getParentRect
        let rangeClientRect = event.target.getBoundingClientRect()
        range.startEvent = {
          clientX: this.getFormatNumber(rangeClientRect.left),
        }
        range.endEvent = {
          clientX: this.getFormatNumber(rangeClientRect.right),
        }
        range.reference.left.lEvent = JSON.parse(JSON.stringify(range.startEvent))
        range.reference.left.lTime = JSON.parse(JSON.stringify(range.startTime))
        range.reference.right.rEvent = JSON.parse(JSON.stringify(range.endEvent))
        range.reference.right.rTime = JSON.parse(JSON.stringify(range.endTime))
        // -------- 更新开始和结束时间的,可选时间段 ----------------
        this.updateClickToSelectableRange(range)
      }
    },
    /**
     * @description 鼠标按下时左侧拖动条
     * @param event 事件对象
     * @param row 行数据
     * @param range 索引
     */
    handleDragHandleLeftAtMouseDown(event, row, range) {
      event.cancelBubble = true
      event.stopPropagation()
      if(event.target.classList.contains('au-week-plan__drag-handle-left')){
        this.isHandleLeftMoving = true
        this.activeRange = range
        this.activeRow = row
        document.addEventListener('mousemove', this.handleDragHandleLeftAtMouseMove)
      }
    },
    handleDragHandleLeftAtMouseMove(event) {
      try {
          if(this.isHandleLeftMoving) {
            const range = this.activeRange
            // --------- 关闭弹窗,显示两侧提示事件 ---------
            range.options.visiblePopoverOnTimeRange = false
            range.options.visibleTooltipLeft = true
            range.options.visibleTooltipRight = true
            // 操作左侧拖动条,以当前的range结束点为目标参照物,判断移动方向
            const { rEvent } = range.reference.right
            let diff = Math.floor(event.clientX - rEvent.clientX)
            if(diff > 0) {  // 向右移动
              this.actionType = ACTION_TYPE.RIGHT_ACTION_OF_LEFT_SIDE
              this.rightActionOnReferenceOfRight(event,range)
            } else {  // 向左移动
              this.actionType = ACTION_TYPE.LEFT_ACTION_OF_LEFT_SIDE
              this.leftActionOnReferenceOfRight(event,range)
            }
          }
        // document.addEventListener('mouseup', this.handleListenerMouseUp)
      } catch (e) {
        console.error(e)
      }
    },
    // 左侧拖动条在左边(以结束时间为参考点)活动
    leftActionOnReferenceOfRight(event, range) {
      // 更新range和前后range,并获取到最大left和width 和最大左侧clientX
      const allowMaxInfo = this.updateRangeInfo(range,ACTION_TYPE.LEFT_ACTION_OF_LEFT_SIDE)

      // 判断event事件的拖动距离的 left 和 width 并限制最大拖动
      // 记录保存有效值的当前range的结束event信息并限制event信息
      range.startEvent = {
        clientX: Math.max(this.getFormatNumber(event.clientX),allowMaxInfo.prevEndClientX),
      }
      // 获取结束位置为参考点位置
      const { rEvent, rTime } = range.reference.right
      range.endTime.value = rTime.value
      range.endTime.rawValue = rTime.rawValue
      // 修正参照物值
      range.reference.left.lTime = JSON.parse(JSON.stringify(range.startTime))
      range.reference.left.lEvent = JSON.parse(JSON.stringify(range.startEvent))

      // 计算left 和 width
      range.location.left = this.getFormatNumber(Math.abs(range.startEvent.clientX - range.rect.left))
      range.style.left = `${range.location.left}px`
      range.location.width = Math.abs(range.startEvent.clientX - rEvent.clientX)
      range.style.width = `${range.location.width}px`
      range.location.right = this.getFormatNumber(range.location.left + range.location.width)
      range.startTime.value = this.transferPxToTime(range.location.left, range.rect.width, 'hm')
      range.startTime.rawValue = this.transferPxToTime(range.location.left, range.rect.width)


    },
    rightActionOnReferenceOfRight(event, range) {
      // 更新range和前后range,并获取到最大left和width 和最大左侧clientX
      const allowMaxInfo = this.updateRangeInfo(range,ACTION_TYPE.RIGHT_ACTION_OF_LEFT_SIDE)

      // 判断event事件的拖动距离的 left 和 width 并限制最大拖动
      // 记录保存有效值的当前range的结束event信息并限制event信息
      range.endEvent = {
        clientX: Math.min(this.getFormatNumber(event.clientX), allowMaxInfo.nextStartClientX),
      }
      // 获取结束位置为参考点位置
      const { rEvent, rTime } = range.reference.right
      // 交换位置
      range.startEvent = JSON.parse(JSON.stringify(rEvent))
      // 交换时间 取出初始化的结束时间,赋值给当前range结束时间
      range.startTime.value = rTime.value
      range.startTime.rawValue = rTime.rawValue
      // 修正参照物的值
      range.reference.left.lEvent = JSON.parse(JSON.stringify(range.startEvent))
      range.reference.left.lTime = JSON.parse(JSON.stringify(range.startTime))

      // 计算left 和 width
      range.location.left = this.getFormatNumber(rEvent.clientX - range.rect.left)
      range.style.left = `${range.location.left}px`
      range.location.width = this.getFormatNumber(Math.abs(range.endEvent.clientX - rEvent.clientX))
      range.style.width = `${range.location.width}px`
      range.location.right = this.getFormatNumber(range.location.left + range.location.width)
      range.endTime.value = this.transferPxToTime(range.location.left + range.location.width, range.rect.width, 'hm')
      range.endTime.rawValue = this.transferPxToTime(range.location.left + range.location.width, range.rect.width)

    },
    /**
     * @description 右侧拖动条 - mousedown
     * @param event
     * @param row
     * @param range
     */
    handleDragHandleRightAtMouseDown(event, row, range) {
      event.cancelBubble = true
      event.stopPropagation()
      if(event.target.classList.contains('au-week-plan__drag-handle-right')){
        this.isHandleRightMoving = true
        this.activeRange = range
        this.activeRow = row
        document.addEventListener('mousemove',this.handleDragHandleRightAtMouseMove)
      }
    },
    /**
     * @description 按住右侧拖动条滚动
     * @param event
     */
    handleDragHandleRightAtMouseMove(event) {
        try {
          if (this.isHandleRightMoving) {
            const range = this.activeRange
            // --------- 关闭弹窗,显示两侧提示事件 ---------
            range.options.visiblePopoverOnTimeRange = false
            range.options.visibleTooltipLeft = true
            range.options.visibleTooltipRight = true

            // 以开始点为目标参照物,判断移动方向
            const { lEvent } = range.reference.left
            let diff = Math.floor(event.clientX - lEvent.clientX)
            if(diff > 0) {  // 向右移动
              this.actionType = ACTION_TYPE.RIGHT_ACTION_OF_RIGHT_SIDE
              this.rightActionOnReferenceOfLeft(event,range)
            } else {  // 向左移动
              this.actionType = ACTION_TYPE.LEFT_ACTION_OF_RIGHT_SIDE
              this.leftActionOnReferenceOfLeft(event,range)
            }
          }
          document.addEventListener('mouseup', this.handleListenerMouseUp)
        } catch (e) {
          console.error(e)
        }
    },
    rightActionOnReferenceOfLeft(event, range) {
      // 更新range和前后range,并获取到最大left和width 和最大左侧clientX
      const allowMaxInfo = this.updateRangeInfo(range,ACTION_TYPE.RIGHT_ACTION_OF_RIGHT_SIDE)

      // 判断event事件的拖动距离的 left 和 width 并限制最大拖动
      // 记录保存有效值的当前range的结束event信息并限制event信息
      range.endEvent = {
        clientX: Math.min(this.getFormatNumber(event.clientX), allowMaxInfo.nextStartClientX),
      }
      // 右侧拖动条向右(以开始位置为参照物)移动 获取开始位置的参照物

      const { lEvent, lTime } = range.reference.left
      range.startTime.value = lTime.value
      range.startTime.rawValue = lTime.rawValue
      // 更新右侧参照物的信息
      range.reference.right.rEvent = JSON.parse(JSON.stringify(range.endEvent))
      range.reference.right.rTime = JSON.parse(JSON.stringify(range.endTime))

      range.location.left = lEvent.clientX - range.rect.left
      range.style.left = `${range.location.left}px`
      range.location.width = Math.abs(range.endEvent.clientX - lEvent.clientX)
      range.style.width = `${range.location.width}px`
      range.location.right = this.getFormatNumber(range.endEvent.clientX - range.rect.left)
      range.endTime.value = this.transferPxToTime(range.location.right, range.rect.width, 'hm')
      range.endTime.rawValue = this.transferPxToTime(range.location.right, range.rect.width)


    },
    leftActionOnReferenceOfLeft(event, range) {
      // 更新range和前后range,并获取到最大left和width 和最大左侧clientX
      const allowMaxInfo = this.updateRangeInfo(range,ACTION_TYPE.LEFT_ACTION_OF_RIGHT_SIDE)
      // 判断event事件的拖动距离的 left 和 width 并限制最大拖动
      // 记录保存有效值的当前range的结束event信息并限制event信息
      range.startEvent = {
        clientX: Math.max(this.getFormatNumber(event.clientX), allowMaxInfo.prevEndClientX),
      }
      // 左侧点开始位置为参照物,变化交换开始、结束事件位置
      const { lTime, lEvent } = range.reference.left
      range.endEvent = JSON.parse(JSON.stringify(lEvent))
      // 交换时间 取出初始化的开始时间,赋值给当前range结束时间
      range.endTime.value = lTime.value
      range.endTime.rawValue = lTime.rawValue
      // 更新右侧参照物的信息
      // range.reference.right.rEvent = JSON.parse(JSON.stringify(lTime))
      // range.reference.right.rTime = JSON.parse(JSON.stringify(lEvent))

      // 计算left,width等信息
      range.location.left = this.getFormatNumber(range.startEvent.clientX - range.rect.left)
      range.style.left = `${range.location.left}px`
      range.location.width = this.getFormatNumber(Math.abs(lEvent.clientX - range.startEvent.clientX))
      range.style.width = `${range.location.width}px`
      range.location.right = this.getFormatNumber(range.location.left + range.location.width)
      range.startTime.value = this.transferPxToTime(range.location.left, range.rect.width, 'hm')
      range.startTime.rawValue = this.transferPxToTime(range.location.left, range.rect.width)

    },
    /**
     * @description mousedown触发
     * @param event
     */
    handleMouseDown(event) {
      event.cancelBubble = true
      event.stopPropagation()
      if(event.target.className !== document.querySelector('.au-week-plan__wrapper').className) return
      if(this.mode === 'add' || this.mode === 'edit') {
        if(this.isActOnTargetElement) {
          this.handleClearActiveState()
          this.isPointerDown = false
          this.isMoved = false
          this.isHandleRightMoving = false
          this.isHandleLeftMoving = false
          return
        }

        this.timer = setTimeout(() => {
          this.isPointerDown = true
          this.isMoved = true
          try {
            // 先构建数据,临时存储在range变量上,如果是按住一会再移动,就push到row ,如果按住不放马上移动则讲activeRange再push进来
            let range = JSON.parse(JSON.stringify(RANGE_DATA))
            // range的唯一标识uuid
            range.uuid = uuidV4()
            // 显示侧边拖动条
            range.options.isShowSideDragBar = true
            // 显示两侧时间的提示
            range.options.visibleTooltipLeft = true
            range.options.visibleTooltipRight = true
            // 保存节点的开始event信息
            range.startEvent = {
              clientX: this.getFormatNumber(event.clientX),
            }
            // 保存rect信息
            range.rect = event.target.getBoundingClientRect()

            // 设置起始时间
            let startPx = this.getFormatNumber(range.startEvent.clientX - range.rect.left)
            range.startTime.value = this.transferPxToTime(startPx, range.rect.width, 'hm')
            range.startTime.rawValue = this.transferPxToTime(startPx, range.rect.width)

            // 初始化限制时间
            range.startTime.selectableRange = [`${MIX_START_TIME}-${MAX_END_TIME}`]
            // 计算left在父节点中的px位置
            range.location.left = Math.max(this.getFormatNumber(event.clientX - range.rect.left), 0)
            range.style.left = `${range.location.left}px`

            // 更新记录参照点信息
            range.reference.origin.oTime = JSON.parse(JSON.stringify(range.startTime))
            range.reference.origin.oEvent = JSON.parse(JSON.stringify(range.startEvent))

            // push 到对应行row的data中
            this.weekList[this.targetIndexOfHover].data.push(range)

            // 设置并保存当前active的range
            this.activeRange = range
            // 保存行信息
            this.activeRow = this.weekList[this.targetIndexOfHover]
            console.error(" this.weekList ", this.weekList)
            // 绑定 鼠标move事件
            document.addEventListener('mousemove', this.handleMouseMove)
            document.addEventListener('mouseup', this.handleListenerMouseUp)
          } catch (e) {
            console.error(e)
          }
        },0)
      }
    },
    handleMouseMove(event) {
      if(this.isPointerDown) {
        // console.error(" mouse down event ------>>> ", event)
        // 数据对象
        const range = this.activeRange
        // 以按下的原点为目标, 判断是从左往后拖动,还是从右往左拖动
        const { oEvent } = range.reference.origin // 获取参照物原点的事件信息
        let diff = Math.floor(event.clientX - oEvent.clientX)
        if (diff > 0) { // 从左到右拖动
          this.actionType = ACTION_TYPE.ORIGIN_RIGHT
          this.rightAction(event,range)
        } else { // 从右到左拖动
          this.actionType = ACTION_TYPE.ORIGIN_LEFT
          this.leftAction(event,range)
        }
      }
    },
    /**
     * @description 从左到右拖动
     * @param event
     * @param range
     */
    rightAction(event, range) {
      try {
        // 显示侧边拖动条
        range.options.isShowSideDragBar = true

        // 更新range和前后range,并获取到最大left和width和最大右侧距离
        const allowMaxInfo = this.updateRangeInfo(range, ACTION_TYPE.ORIGIN_RIGHT)

        // 判断event事件的拖动距离的 left 和 width 并限制最大拖动
        // 记录保存有效值的当前range的结束event信息并限制event信息
        range.endEvent = {
          clientX: Math.min(this.getFormatNumber(event.clientX), allowMaxInfo.nextStartClientX),
        }
        // 1.获取原点的信息,因为是向后移动,当前range的起始信息就是原点信息,并记录当前range的结束信息
        const { oTime, oEvent } = range.reference.origin
        range.startTime.value = oTime.value
        range.startTime.rawValue = oTime.rawValue
        // 记录侧边的左侧参照点的位置信息
        range.reference.left.lTime = JSON.parse(JSON.stringify(oTime))
        range.reference.left.lEvent = JSON.parse(JSON.stringify(oEvent))
        // 记录移动的信息就是结束信息
        range.reference.right.rTime = JSON.parse(JSON.stringify(range.endTime))
        range.reference.right.rEvent = JSON.parse(JSON.stringify(range.endEvent))

        // 2. 重新明确初始点的left位置(避免左右拖动的误差)
        range.location.left = this.getFormatNumber(Math.max(oEvent.clientX - range.rect.left, allowMaxInfo.allowMaxLeft))
        range.style.left = `${range.location.left}px`
        range.location.width = this.getFormatNumber(Math.min(range.endEvent.clientX - oEvent.clientX, allowMaxInfo.allowMaxWidth))
        range.style.width = `${range.location.width}px`
        range.location.right = this.getFormatNumber(range.location.left + range.location.width)
        range.endTime.value = this.transferPxToTime(range.location.right, range.rect.width, 'hm')
        range.endTime.rawValue = this.transferPxToTime(range.location.right, range.rect.width)

        // 监听鼠标滑动或者拖动的鼠标抬起事件
        document.addEventListener('mouseup', this.handleListenerMouseUp)

      } catch (e) {
        console.error(e)
      }
    },
    leftAction(event, range) {
      try {
        // 显示侧边拖动条
        range.options.isShowSideDragBar = true
        // 更新range和前后range,并获取到最大left和width 和最大左侧clientX
        const allowMaxInfo = this.updateRangeInfo(range,ACTION_TYPE.ORIGIN_LEFT)
        // 判断event事件的拖动距离的 left 和 width 并限制最大拖动
        // 记录保存有效值的当前range的结束event信息并限制event信息
        range.startEvent = {
          clientX: Math.max(this.getFormatNumber(event.clientX), allowMaxInfo.prevEndClientX),
        }
        // 以按下的原点参照物,因为是往左边移动,所以原点就是结束点,
        const { oTime, oEvent } = range.reference.origin
        range.endEvent = JSON.parse(JSON.stringify(oEvent))
        range.endTime.value = oTime.value
        range.endTime.rawValue = oTime.rawValue
        // 因为是往左边移动, 起始点信息就是移动的信息
        range.reference.left.lTime = JSON.parse(JSON.stringify(range.startTime))
        range.reference.left.lEvent = JSON.parse(JSON.stringify(range.startEvent))
        // 因为是往左边移动,所以原点就是结束点,
        range.reference.right.rEvent = JSON.parse(JSON.stringify(oEvent))
        range.reference.right.rTime = JSON.parse(JSON.stringify(oTime))

        // 计算left,width等信息
        range.location.left = this.getFormatNumber(Math.max(event.clientX - range.rect.left, allowMaxInfo.allowMaxLeft))
        range.style.left = `${range.location.left}px`
        range.location.width = this.getFormatNumber(Math.min(Math.abs(range.endEvent.clientX - event.clientX), allowMaxInfo.allowMaxWidth))
        range.style.width = `${range.location.width}px`
        range.location.right = this.getFormatNumber(range.location.left + range.location.width)
        range.startTime.value = this.transferPxToTime(range.location.left, range.rect.width, 'hm')
        range.startTime.rawValue = this.transferPxToTime(range.location.left, range.rect.width)

        // 监听鼠标滑动或者拖动的鼠标抬起事件
        document.addEventListener('mouseup', this.handleListenerMouseUp)
      } catch (e) {
        console.error(e)
      }
    },
    /**
     * @description 更新当前range位置信息,可选时间,并且更新前后range的位置和可选时间
     */
    updateRangeInfo(range, type = this.actionType) {
      const getPrevRange = this._closestSmaller(range.location.left)
      const getNextRange = this._closestLarger(range.location.left)
      switch (type) {
        case ACTION_TYPE.ORIGIN_LEFT: // 更新并返回 首次按下往左滑动
        case ACTION_TYPE.LEFT_ACTION_OF_RIGHT_SIDE: // 右侧拖动条被拖动左边来了,右拖动条在左边活动
        case ACTION_TYPE.LEFT_ACTION_OF_LEFT_SIDE: // 左侧拖动条往左边拖动
         return this.updateActionOfOriginLeft(range,getPrevRange,getNextRange)
        case ACTION_TYPE.ORIGIN_RIGHT: // 更新并返回 首次按下往右滑动
        case ACTION_TYPE.RIGHT_ACTION_OF_RIGHT_SIDE: // 右侧拖动条在右边活动
        case ACTION_TYPE.RIGHT_ACTION_OF_LEFT_SIDE: // 左侧拖动条往右边拖动
          return this.updateActionOfOriginRight(range,getPrevRange,getNextRange)
        default:
      }
    },
    updateHasPreviousAndRange(range, getPrevRange) {
      let [,endOfStart] = range.startTime.selectableRange.join().split("-")
      range.startTime.selectableRange = Array.of(`${getPrevRange.endTime.rawValue.trim()}-${endOfStart.trim()}`)
      let [prevStartOfEnd,] = getPrevRange.endTime.selectableRange.join().split("-")
      getPrevRange.endTime.selectableRange = Array.of(`${prevStartOfEnd.trim()}-${range.startTime.rawValue}`)
    },
    updateHasNextAndRange(range, getNextRange) {
      let [,nextEndOfStart] = getNextRange.startTime.selectableRange.join().split("-")
      let [startOfEnd,] = range.endTime.selectableRange.join().split("-")
      range.endTime.selectableRange = Array.of(`${startOfEnd.trim()}-${getNextRange.startTime.rawValue.trim()}`)
      getNextRange.startTime.selectableRange = Array.of(`${range.endTime.rawValue.trim()}-${nextEndOfStart.trim()}`)
    },
    updateActionOfOriginRight(range,getPrevRange,getNextRange) {
      let allowMaxLeft = 0
      let allowMaxWidth = this.getFormatNumber(range.rect.width - this.getFormatNumber(range.startEvent.clientX - range.rect.left))
      let nextStartClientX = range.rect.right
      if (getPrevRange) {
        this.updateHasPreviousAndRange(range, getPrevRange)
      }
      if (getNextRange) {
        allowMaxWidth = this.getFormatNumber(getNextRange.startEvent.clientX - range.startEvent.clientX)
        nextStartClientX = getNextRange.startEvent.clientX
        this.updateHasNextAndRange(range, getNextRange)
      }
      return {
        allowMaxLeft,
        allowMaxWidth,
        nextStartClientX
      }
    },
    updateActionOfOriginLeft(range,getPrevRange,getNextRange) {
      let allowMaxLeft = 0
      let allowMaxWidth = this.getFormatNumber(range.rect.width - (range.rect.width - this.getFormatNumber(range.endEvent.clientX - range.rect.left)))
      let prevEndClientX = range.rect.left
      if(getPrevRange) {
        allowMaxLeft = getPrevRange.location.right
        allowMaxWidth = this.getFormatNumber(range.endEvent.clientX - getPrevRange.location.right - range.rect.left)
        prevEndClientX = getPrevRange.endEvent.clientX
        this.updateHasPreviousAndRange(range, getPrevRange)
      }
      if(getNextRange) {
        this.updateHasNextAndRange(range, getNextRange)
      }
      return {
        allowMaxLeft,
        allowMaxWidth,
        prevEndClientX
      }
    },
    updateClickToSelectableRange(range) {
      const getPrevRange = this._closestSmaller(range.location.left)
      const getNextRange = this._closestLarger(range.location.left)
      if(getPrevRange) {
        this.updateHasPreviousAndRange(range, getPrevRange)
      }
      if (getNextRange) {
        this.updateHasNextAndRange(range, getNextRange)
      }
    },
    /**
     * @description 监听滑动或者拖动结束后鼠标抬起事件
     */
    handleListenerMouseUp(event) {
      event.cancelBubble = false
      event.preventDefault()
      event.stopPropagation()
      console.error(" 鼠标抬起...... event mouse up  ", )
      this.isActOnTargetElement = this.isPointerDown || this.isMoved || this.isHandleLeftMoving || this.isHandleRightMoving &&
        (this.activeRange.startEvent.clientX <= event.clientX <= this.activeRange.endEvent.clientX);


      console.error(" mouseup 中的 this.isActOnTargetElement ", this.isActOnTargetElement)

      this.isPointerDown = false
      this.isMoved = false
      this.isHandleRightMoving = false
      this.isHandleLeftMoving = false
      clearTimeout(this.timer)
      // clearTimeout(this.clickTimer)
      // 激活当前range,其他range关闭
      this.activeRange && this.activeCurrentAndCloseOther(this.activeRange)
      // 如果宽度说0 或者只是按住没移动过超过1px的距离,就移除
      !Math.floor(this.activeRange?.location?.width || 0) && this.invalidRemove()

      // 移除按住拖动的操作
      document.removeEventListener('mousemove', this.handleMouseMove)
      // 移除右侧的拖动操作
      document.removeEventListener('mousemove', this.handleDragHandleRightAtMouseMove)
      // 移除左侧的拖动操作
      document.removeEventListener('mousemove', this.handleDragHandleLeftAtMouseMove)
    },
    activeCurrentAndCloseOther(range, weekList = this.weekList) {
      // 激活当前的range,并显示当前的range的popover弹窗,关闭侧边的时间提示
      weekList.forEach(item => {
        item.data.forEach(r => {
          if(r.uuid === range.uuid) {
            r.options.visiblePopoverOnTimeRange = true
            r.options.visibleTooltipLeft = false
            r.options.visibleTooltipRight = false
          } else {
            r.options.visiblePopoverOnTimeRange = false
            r.options.visibleTooltipLeft = false
            r.options.visibleTooltipRight = false
            r.options.isShowSideDragBar = false
          }
        })
      })
    },
    /**
     * @description 删除时间段range
     * @param row
     * @param range
     * @param i 所在data中的range索引
     */
    handleDeleteRange(row, range, i) {
      this.closeAllPopover()
      row.data.splice(i,1)
    },
    /**
     * @description 保存时间段range
     * @param range 时间段本身
     */
    async handleSaveRange(range) {
      // TODO handleSaveRange 保存的时候
      // 1. 校验开始时间和结束时间(开始时间不能大于结束时间)
      this.checkStartTimeAndEndTime(range.startTime.value, range.endTime.value).then(() => {
        try {
          // 2. 校验通过后,修改控制行为(关闭弹窗或者其他的显示隐藏的逻辑)
          range.options.visiblePopoverOnTimeRange = false
          range.options.isShowSideDragBar = false
          range.options.visibleTooltipLeft = false
          range.options.visibleTooltipRight = false
          // 先拼一个值参与计算
          let [,,rawStartTimeSeconds] = range.startTime.rawValue.split(':')
          let [,,rawEndTimeSeconds] = range.endTime.rawValue.split(':')
          let startTimeValueOfRaw = range.startTime.value.concat(":"+rawStartTimeSeconds)
          let endTimeValueOfRaw = range.endTime.value.concat(":"+rawEndTimeSeconds)
          let left = this.transferTimeToPx(startTimeValueOfRaw, range.rect.width)
          let width = this.transferRangeTimeToPx(startTimeValueOfRaw, endTimeValueOfRaw, range.rect.width)
          range.location.left = left
          range.style.left = `${left}px`
          range.location.width = width
          range.style.width = `${width}px`
          range.location.right = this.getFormatNumber(left + width)
          // 3.更新 range数据 将改变后的时分,拼接上秒,把秒重置为 "00"
          // let [,,startTimeSecond] = range.startTime.rawValue.split(':')
          // let [,,endTimeSecond] = range.endTime.rawValue.split(':')
          range.startTime.rawValue = range.startTime.value.concat(`:00`)
          range.endTime.rawValue = range.endTime.value.concat(`:00`)
          // TODO 重新计算可选时间范围
          this.updateClickToSelectableRange(range)
        } catch (e) {
          console.error(e)
        }
      }).catch(e => {
        this.$message.error(e.message)
      })
    },
    /**
     * @description 校验开始时间和结束时间(开始时间不能大于结束时间)
     * @param startTime
     * @param endTime
     * @returns {Promise<Promise<void>|Promise<never>>}
     */
    async checkStartTimeAndEndTime(startTime, endTime) {
      const today = dayjs()
      const [startHour, startMinute] = startTime.split(":")
      const [endHour, endMinute] = endTime.split(":")
      const start = dayjs(today).hour(parseInt(startHour)).minute(parseInt(startMinute))
      const end = dayjs(today).hour(parseInt(endHour)).minute(parseInt(endMinute))
      return end.isAfter(start) ? Promise.resolve(true) : Promise.reject(new Error('结束时间需晚于开始时间!'))
    },

    /**
     * @description 根据left值,找出行对象中距离自己最近又小于自己的数据(即上一个数据对象)
     * @param left
     * @param data
     * @returns {null}
     * @private
     */
    _closestSmaller(left, data = this.activeRow.data) {
      let closet = null
      for(let item of data) {
        if (item.location.left < left) {
          if (!closet || item.location.left > closet.location.left) {
            closet = item
          }
        }
      }
      return closet
    },
    /**
     * @description 根据left值,找出行对象中距离自己最近又大于自己的数据(即下一个数据对象)
     * @param left
     * @param data
     * @returns {null}
     * @private
     */
    _closestLarger(left, data = this.activeRow.data) {
      let closet = null
      for(let item of data) {
        if (item.location.left > left) {
          if (!closet || item.location.left < closet.location.left) {
            closet = item
          }
        }
      }
      return closet
    },
    /**
     * @description 把距离换算成秒
     * @param positionX
     * @param containerPx
     * @returns {number}
     */
    getDistanceInToSeconds(positionX, containerPx) {
      return positionX * 86400 / containerPx
    },
    /**
     * @description 把秒换算成距离
     * @param seconds
     * @param containerPx
     * @returns {number}
     */
    getSecondsIntoDistance(seconds,containerPx) {
      return seconds * containerPx / 86400
    },
    getSeconds(e) {
      // 如果时间选择到23:59 就加上 59秒,否则就正常返回
      return e >= 86340 && e < 86400 ? e - e % 60 + 59 : e
    },
    transferPxToSeconds(px, totalPx = 0) {
      const totalTimeSeconds = 86400
      // 每个像素代表的秒数
      const secondsPerPixel = totalTimeSeconds / totalPx
      return secondsPerPixel * px
    },
    /**
     * @description 把 秒 转换成 时:分
     * @param seconds
     * @returns {number|string}
     */
    transferSecondsToHourAndMinute(seconds = 0) {
      if(typeof Number(seconds) === 'number' && !Number.isNaN(seconds)) {
        let hour = (seconds / 3600).toString()
        let minute = (seconds % 3600 / 60).toString()
        let h = Number.parseInt(hour, 10).toString().padStart(2, '0')
        let s = Number.parseInt(minute, 10).toString().padStart(2, '0')
        return `${h}:${s}`
      }
      return seconds
    },
    /**
     * @description 把 时:分格式 转换成 秒
     * @param time
     */
    transferHourAndMinuteToSeconds(time) {
      let t = time.split(':')
      return 3600 * Number.parseInt(t[0], 10) + 60 * Number.parseInt(t[1], 10)
    },


    /**
     * @description 格式化数字保留小数
     * @param num
     * @param reserve
     * @returns {*|number}
     */
    getFormatNumber(num, reserve = 3) {
      if (typeof num === 'number') {
        return Number.parseFloat(num.toFixed(reserve))
      }
      return num
    },
    /**
     * @description px 转 time
     * @param px 宽度
     * @param totalPx 总宽度
     * @param format 格式 'hm','hms'
     * @returns {string} 距离值
     */
    transferPxToTime(px = 0, totalPx = 0, format) {
      const totalSecondsInRange = 23 * 3600 + 59 * 60 + 59; // 23:59:59
      const secondsPerPixel = totalSecondsInRange / totalPx
      const allSeconds = px * secondsPerPixel
      // 这里不需要处理24:00:00,因为我们总的时间是23:59:59
      const hours = Math.floor(allSeconds / 3600)
      const minutes = Math.floor((allSeconds % 3600) / 60)
      const seconds = Math.floor(allSeconds % 60)
      let now = dayjs().hour(hours).minute(minutes).second(seconds)
      switch (format) {
        case 'hms':
          return now.format('HH:mm:ss')
        case 'hm':
          return now.format('HH:mm')
        default:
          return now.format('HH:mm:00')
      }
    },
    /**
     * @description time 转 px
     * @returns {number} px
     */
    transferTimeToPx(time, totalPx = 0) {
      const minute = 60
      const minuteOnHour = 60*60
      const totalSecondsInRange = 23 * 3600 + 59 * 60 + 59; // 23:59:59
      const pixelsPerSecond = totalPx / totalSecondsInRange;
      const [h, m, s = 0] = time.split(":")
      const all = h * minuteOnHour + m * minute + Number(s)
      return this.getFormatNumber(all * pixelsPerSecond)
    },
    /**
     * @description 计算时间段范围所占的px
     * @param startTime
     * @param endTime
     * @param total
     * @returns {*|number}
     */
    transferRangeTimeToPx(startTime, endTime, total) {
      const startPx = this.transferTimeToPx(startTime, total)
      const endPx = this.transferTimeToPx(endTime, total)
      return this.getFormatNumber(Math.abs(endPx - startPx))
    },

    formatTimeIntoHourAndMinutes(rawValue) {
      if (typeof rawValue === 'string') {
        let [h = "00", m = "00"] = rawValue.split(":")
        return `${h}:${m}`
      } else {
        return rawValue
      }
    },
    /**
     * @description 将小时转换为秒
     * @param hour
     * @returns {*|number}
     */
    transferHourIntoSeconds(hour) {
      const h = Number(hour)
      const isValidNumber = typeof h === 'number' && !Number.isNaN(h)
      if(isValidNumber) {
        return h * 60 * 60
      } else {
        return hour
      }
    },
    /**
     * @description 将分钟转换为秒
     * @param minute
     * @returns {*|number}
     */
    transferMinuteIntoSeconds(minute) {
      const m = Number(minute)
      const isValidNumber = typeof m === 'number' && !Number.isNaN(m)
      if(isValidNumber) {
        return m * 60
      } else {
        return minute
      }
    },
    /**
     * @description 复制到弹窗 - 打开
     * @param row 当前行数据
     * @param index 当前行在集合中的数据索引
     */
    handleOpenCopyPopover(row, index) {
      this.handleClearActiveState()
      this.isShowCopyPopover(row)

      // 要复制的目标行KEY,获取到当前行的key
      this.copyTargetKey = this.weekList[index].dayOfWeekType
      // 默认设置当前行索引的选项为选中状态
      this.copyToList = Array.of(this.copyTargetKey) || []
      // 判断半选还是全选的状态
      this.checkAll = this.copyToList.length === this.weekList.length;
      this.isIndeterminate = this.copyToList.length > 0 && this.copyToList.length < this.weekList.length
    },
    /**
     * @description 复制到弹窗 - 关闭
     */
    handleCloseCopyPopover(item) {
      this.isShowCopyPopover(item, false)
      this.copyTargetKey = null
      this.copyToList = []
    },
    /**
     * @description 复制到弹窗 - 确定
     * @param row 当前行数据
     */
    handleConfirmCopy(row) {
      try {
        let getTargetObj = this.weekList.find(item => item.dayOfWeekType === this.copyTargetKey)
        if (getTargetObj) {
          // target data
          let data = getTargetObj.data || Array.of()
          // 替换weekList下的所有 data, 并且更新range的uuid
          for(let i = 0; i < this.weekList.length; i++) {
            const row = this.weekList[i]
            row.visiblePopoverOnCopy = false
            if(this.copyToList.includes(row.dayOfWeekType)) {
              row.data = JSON.parse(JSON.stringify(data))
              for(let k = 0; k < row.data.length; k++) {
                row.data[k].uuid = uuidV4()
              }
            }
          }
        }
      } catch (e) {
        console.error(e)
      }
    },
    /**
     * @description 复制到弹窗 - 清空
     * @param row 行数据
     */
    handleClearCopy(row) {
      row.data = Array.of()
    },
    /**
     * @description 切换复制到弹窗的显示与隐藏
     * @param row 行数据
     * @param show 是否展示,true, false
     */
    isShowCopyPopover(row, show = true) {
      this.weekList.map(item =>
        row.dayOfWeekType === item.dayOfWeekType
          ? item.visiblePopoverOnCopy = show
          : item.visiblePopoverOnCopy = false
      )
    },
    /**
     * @description 复制到 - 中的 - 全选半选change
     * @param val
     */
    handleCheckAllChange(val) {
      this.copyToList = val ? this.weekList.map(item => item.dayOfWeekType) : []
      this.isIndeterminate = false
    },
    /**
     * @description 复制到 - 中的 - 选项change
     * @param value 选中的列表
     */
    handleCheckedChange(value) {
      let checkedCount = value.length
      this.checkAll = checkedCount === this.weekList.length
      this.isIndeterminate = checkedCount > 0 && checkedCount < this.weekList.length;
    },
    /**
     * @description 处理传进来的数据,进行转换成组件的数据
     * @param data 传到这个组件的数据
     * @returns {Array}
     */
    convertDataAtIncoming(data = []) {
      console.error("后端接口的数据 ------->>>> ", data)
      // 将外面传进来的数据转换为组件需要的数据即this.weekList
       let rect = document.querySelector('.au-week-plan__wrapper').getBoundingClientRect()
       let result = data.map(item => {
         let list =  item.timeRange.map(time => {
           let [start, end] = time.split("-")
           let temp = JSON.parse(JSON.stringify(RANGE_DATA))
           // 设置range唯一标识
           temp.uuid = uuidV4()
           // 设置数据
           let left = this.transferTimeToPx(start, rect.width)
           let width = this.transferRangeTimeToPx(start, end, rect.width)
           let right = this.transferTimeToPx(end, rect.width)
           temp.rect = rect
           temp.startTime.value = this.formatTimeIntoHourAndMinutes(start)
           temp.endTime.value = this.formatTimeIntoHourAndMinutes(end)
           temp.startTime.rawValue = start
           temp.endTime.rawValue = end
           temp.location.left = left
           temp.location.right = right
           temp.location.width = width
           temp.style.left = `${left}px`
           temp.style.width = `${width}px`
           let startOX = this.getFormatNumber(this.transferTimeToPx(start, rect.width))
           let startX = Math.max(this.getFormatNumber(startOX + rect.left),rect.left)
           let endOX = this.getFormatNumber(this.transferTimeToPx(end, rect.width))
           let endX = Math.min(this.getFormatNumber(endOX + rect.left), rect.right)
           temp.startEvent = {clientX: startX }
           temp.endEvent = {clientX: endX }
           return temp
         })
        return {
           dayOfWeekType: item.dayWeek,
           dayOfWeekName: this.weekName[item.dayWeek],
           visiblePopoverOnCopy: false,
           data: list
         }
       })
       this.weekList = result
      console.error(" 传进来的data进行数据格式化后------>>>>>>: ", result)
    },
    /**
     * @description 清空
     */
    clearAll() {
      this.weekList.map(item => item.data = Array.of())
    },

    /**
     * @description 暴露数据
     * @returns {Array}
     */
    modelData() {
      return this.weekList
    },

  },
}
</script>
<style scoped lang="scss">
.au-week-plan-group {
  position: relative;
  padding: 1px;
  /*时间线(竖直的时间线)*/
  .au-week-plan-group__panel-wrap {
    padding-left: 86px;
    padding-right: 62px;
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    border: 1px solid #e8e8e8;

    .au-week-plan-group__panel-wrap {
      width: 100%;
      height: 100%;

      .au-week-plan-group__tick__line {
        width: 4.1667%; /*24条竖直的时间分割线,所以一份宽度是4.166%*/
        height: calc(100% - 32px);
        margin-top: 32px;
        border-left: 1px solid #e5e5e5;
        float: left;
      }

      /*偶数线短一点*/
      .au-week-plan-group__tick__line:nth-child(2n) {
        height: 6px;
      }
    }
  }

  /*时间刻度(0-24d点)*/
  .au-week-plan-group__time__line-wrap {
    padding-left: 86px;
    padding-right: 62px;
    position: relative;
    background-color: rgba(0, 0, 0, .06);

    .au-week-plan-group__time__line {
      overflow: hidden;
      width: 100%;
      height: 32px;
      line-height: 32px;
      padding: 0;
      margin: 0;
      pointer-events: none;
      user-select: none;
      white-space: nowrap;

      .au-week-plan-group__scale {
        display: block;
        width: 8.333%; /*分为12份,所以宽度是8.333%*/
        float: left;

        .au-week-plan-group__scale-label {
          color: #333
        }

        .au-week-plan-group__scale-label-last {
          float: right;
          margin-right: 2px;
        }
      }
    }
  }

  /*鼠标hover到星期几的时候的hover行遮罩*/
  .au-week-group__hover {
    height: 40px;
    position: absolute;
    top: 0;
    right: 62px;
    left: 0;
    background-color: rgba(233, 239, 253, .7);
    transition: top .15s;
  }

  /*从星期一到星期天的列表*/
  .au-week-plan {
    padding-left: 86px;
    padding-right: 62px;
    position: relative;
    height: 40px;
    user-select: none;

    .au-week-plan__label {
      width: 86px;
      position: absolute;
      left: 0;
      overflow: hidden;
      padding: 0 8px;
      color: #333;
      line-height: 40px;
      text-align: center;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    /*动作*/
    .au-week-plan__action {
      width: 62px;
      height: 100%;
      position: absolute;
      right: 0;
      background-color: rgba(0, 0, 0, .06);
      display: flex;
      align-items: center;
      justify-content: center;

      .au-week-plan__action-buttons {
        //margin: 4px;
        display: flex;
        justify-content: center;

        i[class^="el-icon"] {
          padding: 4px;
        }

        .el-popover-wrapper {
          display: none;
          cursor: pointer;

          &.show {
            display: inline-block;
            transition: all .15s;
          }
        }
      }
    }

    /*滑动条容器*/
    .au-week-plan__wrapper {
      height: 100%;
      position: absolute;
      left: 86px;
      right: 62px;
      z-index: 3;
      cursor: pointer;

      .au-week-plan__range-wrap {
        position: absolute;
        min-width: 1px;
        height: 40px;
        cursor: pointer;

        .au-week-plan__range {
          position: absolute;
          z-index: 3;
          top: 8px;
          width: 100%;
          height: 24px;
          background-color: #3d6ce5;
          cursor: pointer;
          &.is-active {
            z-index: 10;
            transition: all .03s linear;
            .au-week-plan__drag-handle-left {
              left: 0;
              cursor: w-resize;
              display: block;
            }
            .au-week-plan__drag-handle-right {
              right: 0;
              cursor: e-resize;
              display: block;
            }
          }
          .au-week-plan__drag-handle {
            position: absolute;
            z-index: 11;
            top: -1px;
            bottom: -1px;
            width: 6px;
            display: none;
            background-color: #2CD8FF;
          }
        }
      }
    }
  }
}

/* 复制按钮 中的 popover 弹窗的样式 */
.au-week-plan__action_buttons__copy {
  width: 400px;
  margin-right: 10px;
  transform-origin: right center;

  .button__copy {
    .is-target-week {
      font-weight: bold;
    }
    .el-checkbox {
      margin: 0 15px 5px 0 !important;
      position: relative;
      display: inline-block;
      user-select: none;
      vertical-align: sub;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .el-checkbox-group {
      display: inline-block;
      font-size: 0;
      line-height: 1.4;
      vertical-align: middle;

      .el-checkbox {
        font-size: 14px;
        display: inline-block;
        cursor: pointer;
        line-height: 1.4;
        user-select: none;
        vertical-align: sub;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
      }
    }

    .action__copy {
      text-align: right;
      margin: 0;

      .el-button {
        width: 44px;
        height: 26px;
      }
    }
  }
}

/* 时间段中的时间区域popover弹窗*/
.au-week-plan__popover-time-range {
  //margin-bottom: 10px;
  //transform-origin: center top;
  //margin-top: 10px;

  .au-week-plan__popover {
    padding: 4px;
    .au-week-plan__popover--time {
      white-space: nowrap;
      .el-date-editor {
        width: 86px;
        &.el-input{
          position: relative;
          display: inline-block;

        }
        :deep(input.el-input__inner) {
          text-align: center !important;
          padding: 0 20px !important;
        }

      }
      .au-week-plan__popover--dividing-line {
        position: relative;
        top: -4px;
        display: inline-block;
        width: 8px;
        height: 1px;
        margin: 0 8px;
        background-color: #8B8F97;
      }
    }
    .au-week-plan__popover--action {
      margin: 10px 0 0;
      text-align: right;
    }
  }
}


</style>
<style lang="scss">
/*左右拖动条的tooltip弹出样式*/
.au-week-plan__drag-handle--tooltip {
  border: none !important;
  border: 0 !important;
  background-color: #f5f5f5;
  box-shadow: 0 0 2px 0 rgba(0,0,0,.2), 0 2px 8px 0 rgba(0, 0, 0, .12);
  .popper__arrow {
    border: none;
    border: 0;
  }
}
</style>
相关推荐
前端大白话2 分钟前
Vue2和Vue3语法糖差异大揭秘:一文读懂,开发不纠结!
javascript·vue.js·设计模式
剽悍一小兔3 分钟前
小程序发布后,不能强更的情况下,怎么通知到用户需要去更新?
前端
115432031q3 分钟前
基于SpringBoot+Vue实现的旅游景点预约平台功能十三
java·前端·后端
JiangJiang4 分钟前
🧠 面试官:受控组件都分不清?还敢说自己写过 React?
前端·react.js·面试
tianchang4 分钟前
JS 中 Map 的概念与使用
前端·javascript
Jenlybein4 分钟前
[ Javascript 面试题 ]:提取对应的信息,并给其赋予一个颜色,保持幂等性
前端·javascript·面试
Carlos_sam4 分钟前
Opnelayers:向某个方向平移指定的距离
前端·javascript
夜熵5 分钟前
JavaScript 中的 this
前端·面试
绅士玖7 分钟前
Vue.js 核心特性解析:响应式原理与组合式API实践
vue.js
前端小巷子8 分钟前
CSS 单位指南
前端·css