背景
看到一个有意思的交互:拖动计算时间的插件,从0到24点的时间,可以左右前后拖动,并有时间选择限制,拖动的时间不能覆盖重叠;那么有什么思路该怎么做呢。学着实现一下看看,大家有什么思路可以交流学习。
效果图
先看下要实现的大致效果:
思路
- 基于鼠标事件,mousemove等,根据返回的事件信息,获取移动和位置数据;
- 有2个辅助函数,将 px 转位 time 的函数,以及 将 time 转为 px 的函数。
- 基于鼠标事件,记录事件开始和事件结束的数据。
- 判断根据事件信息拖动方向,首次以鼠标按下的点为原点,如果拖动两边的侧边条的话,例:拖动左边的侧边条,以右侧为参照物判断是向左拖动还是向右拖动,反之亦然。
问题
- 目前实现出来还是有bug和性能优化问题,
- 时间区域的整块的拖动移动的(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>