今日头条选择时间段组件封装(开箱即用)
你可能看到或是使用过今日头条的选择时间段组件,什么,你没有用过,请先看图

线上演示地址: andt-components
先解释一下选择器功能
- 组件一周时段可选,每半个小时一个粒度,可以连续选择,可点选
- 当鼠标拉开的选框是透明部分,松开鼠标可支持选择部分选中与取消
- 已选时间段可以分段统计,如果连续累加,如果不连续逗号分隔
- 选框可以8个方向拉伸,东,南,西,北,东北,东南,西北,西南
- 当起始的单元是选中的,那么拉的选框为其反值(取消),反之也行
- 可一键清除所有选择
- 粒度可支持一个小时等大粒度
抛砖引玉
先说到这,如果想了解实现原理
请在下面留言评论
— 2019-12-05 更新 —
实现逻辑
- 拖拽选择UI部分,根据 星期一 到 星期日 生成 7个 7 * 48 的二维数据
- 已选择时间段 更具选中的item 生成区间段的时间,相邻的合并
- 如何去实现鼠标的拖拽及点选
- 清空数据,循环一遍生成默认值
思路
- 1、 拖拽选择UI部分
如图实现这个组件的关键是构建好7*24 的数据结构,当然如果是半小时粒度 那就是 7 * 48 ,每一个粒度是个对象,应该包含其实时间 如: 第一个 :
start: '00:00',
end: '00:30',
week: '星期一',
value: '00:00 ~ 00:30',
row: 0,
col: 0
依次类推出第一行的48个参数项,但是换行后就是"星期二"组的了,vue 在数据上应该构建成其子集才好渲染,所以 [ ‘星期一’, ‘星期二’, ‘星期三’, ‘星期四’, ‘星期五’, ‘星期六’, ‘星期日’] 用map 生成 一个二维数组 :
value: '星期一',
row: 0,
children: [] // item 7 * 48 项
- 2、已选择时间段
生成对应的周一 - 周日的 选中数据
return this.weektimeData.map((item) => {return {id: item.row,week: item.value,value: splicing(item.child)}
})
splicing 用来合并相邻的item , 文末有代码 处理合并功能
<div v-if="selectState" class="c-weektime-time"><div v-for="t in selectValue" :key="t.id"><p v-if="t.value"><span class="g-tip-text">{{ t.week }}:</span><span>{{ t.value }}</span></p></div>
</div>
组件中用 v-if 和 v-for 来循环渲染结果,而 需使用vue计算属性 computed 来同步计算出用户的选中结果
拖拽事件
开始做这个组件时,我也很没底,不知到这是不是用了什么高级的东西,其实拖拽在html5之前就支持了 ,关键使用的 事件名称: mouseenter, mousedown, mouseup 分别对应了 按下鼠标 , 按住拖动, 松开鼠标
实现这个动画只要用css3的过渡补间动画 transition 就可以让样式的变动产生柔和动画效果,那横拖就是算出点击的 cell 单元开始,计算出移动后的col (列)之差, 就给拖动的div设置其固定宽度的倍数,就可以得到拖动得选中框了,在其松开鼠标时,对这个 cell 单元做选中、非选中得状态更改,就实现了组件得基本功能了
细节
1、 拖拽得8个方向
colEnd - colStart > 0 右
colEnd - colStart < 0 左
rowEnd - rowStart > 0 下
rowEnd - rowStart < 0 上
以上都是考虑另一方是相等得情况,是正方向
如果是斜方向呢,这个可以查看源码自行理解
注意:反方向不是增加宽度,高度;而是更改其top left 值
2、拖动时选中文本被拖动,而不触发鼠标进入(mouseenter)事件,主要解决方式,把其相关得css 样式,设置成 user-select: none
3、单个选中的处理
4、原选中的状态,再一次区域选中时,应该处理状态根据选中第一个选择的状态
清空数据
很简单,把原始数据更新成默认状态
clearWeektime() {this.weektimeData.forEach((item) => {item.child.forEach((t) => {this.$set(t, 'check', false)})})
},
公开源代码
vue 中的template
<template><div class="c-weektime"><div class="c-schedue"></div><div:class="{ 'c-schedue': true, 'c-schedue-notransi': mode }":style="styleValue"></div><table :class="{ 'c-min-table': colspan < 2 }" class="c-weektime-table"><thead class="c-weektime-head"><tr><th rowspan="8" class="week-td">星期/时间</th><th :colspan="12 * colspan">00:00 - 12:00</th><th :colspan="12 * colspan">12:00 - 24:00</th></tr><tr><td v-for="t in theadArr" :key="t" :colspan="colspan">{{ t }}</td></tr></thead><tbody class="c-weektime-body"><tr v-for="t in data" :key="t.row"><td>{{ t.value }}</td><tdv-for="n in t.child":key="`${n.row}-${n.col}`":data-week="n.row":data-time="n.col":class="selectClasses(n)"@mouseenter="cellEnter(n)"@mousedown="cellDown(n)"@mouseup="cellUp(n)"class="weektime-atom-item"></td></tr><tr><td colspan="49" class="c-weektime-preview"><div class="g-clearfix c-weektime-con"><span class="g-pull-left">{{selectState ? '已选择时间段' : '可拖动鼠标选择时间段'}}</span><a @click.prevent="$emit('on-clear')" class="g-pull-right">清空选择</a></div><div v-if="selectState" class="c-weektime-time"><div v-for="t in selectValue" :key="t.id"><p v-if="t.value"><span class="g-tip-text">{{ t.week }}:</span><span>{{ t.value }}</span></p></div></div></td></tr></tbody></table></div>
</template>
vue 中的script
<script>
const createArr = (len) => {return Array.from(Array(len)).map((ret, id) => id)
}
export default {name: 'DragWeektime',props: {value: {type: Array,required: true},data: {type: Array,required: true},colspan: {type: Number,default() {return 2}}},data() {return {width: 0,height: 0,left: 0,top: 0,mode: 0,row: 0,col: 0,theadArr: []}},computed: {styleValue() {return {width: `${this.width}px`,height: `${this.height}px`,left: `${this.left}px`,top: `${this.top}px`}},selectValue() {return this.value},selectState() {return this.value.some((ret) => ret.value)},selectClasses() {return (n) => (n.check ? 'ui-selected' : '')}},created() {this.theadArr = createArr(24)},methods: {cellEnter(item) {const ele = document.querySelector(`td[data-week='${item.row}'][data-time='${item.col}']`)if (ele && !this.mode) {this.left = ele.offsetLeftthis.top = ele.offsetTop} else if (item.col <= this.col && item.row <= this.row) {this.width = (this.col - item.col + 1) * ele.offsetWidththis.height = (this.row - item.row + 1) * ele.offsetHeightthis.left = ele.offsetLeftthis.top = ele.offsetTop} else if (item.col >= this.col && item.row >= this.row) {this.width = (item.col - this.col + 1) * ele.offsetWidththis.height = (item.row - this.row + 1) * ele.offsetHeightif (item.col > this.col && item.row === this.row)this.top = ele.offsetTopif (item.col === this.col && item.row > this.row)this.left = ele.offsetLeft} else if (item.col > this.col && item.row < this.row) {this.width = (item.col - this.col + 1) * ele.offsetWidththis.height = (this.row - item.row + 1) * ele.offsetHeightthis.top = ele.offsetTop} else if (item.col < this.col && item.row > this.row) {this.width = (this.col - item.col + 1) * ele.offsetWidththis.height = (item.row - this.row + 1) * ele.offsetHeightthis.left = ele.offsetLeft}},cellDown(item) {const ele = document.querySelector(`td[data-week='${item.row}'][data-time='${item.col}']`)this.check = Boolean(item.check)this.mode = 1if (ele) {this.width = ele.offsetWidththis.height = ele.offsetHeight}this.row = item.rowthis.col = item.col},cellUp(item) {if (item.col <= this.col && item.row <= this.row) {this.selectWeek([item.row, this.row], [item.col, this.col], !this.check)} else if (item.col >= this.col && item.row >= this.row) {this.selectWeek([this.row, item.row], [this.col, item.col], !this.check)} else if (item.col > this.col && item.row < this.row) {this.selectWeek([item.row, this.row], [this.col, item.col], !this.check)} else if (item.col < this.col && item.row > this.row) {this.selectWeek([this.row, item.row], [item.col, this.col], !this.check)}this.width = 0this.height = 0this.mode = 0},selectWeek(row, col, check) {const [minRow, maxRow] = rowconst [minCol, maxCol] = colthis.data.forEach((item) => {item.child.forEach((t) => {if (t.row >= minRow &&t.row <= maxRow &&t.col >= minCol &&t.col <= maxCol) {this.$set(t, 'check', check)}})})}}
}
</script>
样式文件
<style lang="less" scoped>
.c-weektime {min-width: 640px;position: relative;display: inline-block;
}
.c-schedue {background: #598fe6;position: absolute;width: 0;height: 0;opacity: 0.6;pointer-events: none;
}
.c-schedue-notransi {transition: width 0.12s ease, height 0.12s ease, top 0.12s ease,left 0.12s ease;
}
.c-weektime-table {border-collapse: collapse;th {vertical-align: inherit;font-weight: bold;}tr {height: 30px;}tr,td,th {user-select: none;border: 1px solid #dee4f5;text-align: center;min-width: 12px;line-height: 1.8em;transition: background 0.2s ease;}.c-weektime-head {font-size: 12px;.week-td {width: 70px;}}.c-weektime-body {font-size: 12px;td {&.weektime-atom-item {user-select: unset;background-color: #f5f5f5;}&.ui-selected {background-color: #598fe6;}}}.c-weektime-preview {line-height: 2.4em;padding: 0 10px;font-size: 14px;.c-weektime-con {line-height: 46px;user-select: none;}.c-weektime-time {text-align: left;line-height: 2.4em;p {max-width: 625px;line-height: 1.4em;word-break: break-all;margin-bottom: 8px;}}}
}
.c-min-table {tr,td,th {min-width: 24px;}
}
.g-clearfix {&:after,&:before {clear: both;content: ' ';display: table;}
}
.g-pull-left {float: left;
}
.g-pull-right {float: right;
}
.g-tip-text {color: #999;
}
</style>
使用方法
<drag-weektimev-model="mult_timeRange":data="weektimeData"@on-clear="clearWeektime"/>
import weektimeData from './data/weektime_data'
import DragWeektime from '@/components/drag-weektime'... components: { DragWeektime }
... computed: {mult_timeRange() {return this.weektimeData.map((item) => {return {id: item.row,week: item.value,value: splicing(item.child)}})},
}function splicing(list) {let samelet i = -1const len = list.lengthconst arr = []if (!len) returnwhile (++i < len) {const item = list[i]if (item.check) {if (item.check !== Boolean(same)) {arr.push(...['、', item.begin, '~', item.end])} else if (arr.length) {arr.pop()arr.push(item.end)}}same = Boolean(item.check)}arr.shift()return arr.join('')
}
数据源的生成 weektime_data.js
const formatDate = (date, fmt) => {const o = {'M+': date.getMonth() + 1,'d+': date.getDate(),'h+': date.getHours(),'m+': date.getMinutes(),'s+': date.getSeconds(),'q+': Math.floor((date.getMonth() + 3) / 3),S: date.getMilliseconds()}if (/(y+)/.test(fmt)) {fmt = fmt.replace(RegExp.$1,(date.getFullYear() + '').substr(4 - RegExp.$1.length))}for (const k in o) {if (new RegExp('(' + k + ')').test(fmt)) {fmt = fmt.replace(RegExp.$1,RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length))}}return fmt
}const createArr = (len) => {return Array.from(Array(len)).map((ret, id) => id)
}const formatWeektime = (col) => {const timestamp = 1542384000000 // '2018-11-17 00:00:00'const beginstamp = timestamp + col * 1800000 // col * 30 * 60 * 1000const endstamp = beginstamp + 1800000const begin = formatDate(new Date(beginstamp), 'hh:mm')const end = formatDate(new Date(endstamp), 'hh:mm')return `${begin}~${end}`
}const data = ['星期一','星期二','星期三','星期四','星期五','星期六','星期日'
].map((ret, index) => {const children = (ret, row, max) => {return createArr(max).map((t, col) => {return {week: ret,value: formatWeektime(col),begin: formatWeektime(col).split('~')[0],end: formatWeektime(col).split('~')[1],row,col}})}return {value: ret,row: index,child: children(ret, index, 24)}
})export default data
总结
以上是我根据产品原型实现一个自定义组件的全部过程,功能不算很复杂,但主要是学会如何使用vue 去构建自己的业务组件,以上组件应用场景很多,如:
后台管理系统,旅游系统,广告投放系统,营销系统…等,现在流行Vue,React , Anagular 三大框架,下面看看怎么使用Vue实现
更新反显
媒体给的‘1’‘0’组成的48个字符串,怎么返显呢,这个问题很多人问,其实知道了原理,用字符串处理函数就可以解决,以下贴出我实现的人码,方法很多,仅供参考
index.vue 文件内容
import weektimeData from './weektimeData.js'
this.weektimeData = weektimeDatafunction reverWeektime (schedule) {letidx = 0for (let i = 0; i < this.weektimeData.length; i++) {const children = this.weektimeData[i].childfor (let j = 0; j < children.length; j++) {const n = schedule.substr(idx, 1)this.$set(this.weektimeData[i].child[j], 'check', Boolean(parseInt(n)))idx++}}}// formItem.schedule_time 11110000000011111111 (头条使用48 个1与0 代表选中)
this.reverWeektime(formItem.schedule_time)
weektimeData.js 文件内容
import { formatDate, creteArr } from '@/common/js/utils'const formatWeektime = col => {const timestamp = 1542384000000 // '2018-11-17 00:00:00'const beginstamp = timestamp + col * 1800000 // col * 30 * 60 * 1000const endstamp = beginstamp + 1800000const begin = formatDate(new Date(beginstamp), 'hh:mm')const end = formatDate(new Date(endstamp), 'hh:mm')return `${begin}~${end}`
}const data = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'].map((ret, index) => {const children = (ret, row, max) => {return creteArr(max).map((t, col) => {return {week: ret,value: formatWeektime(col),begin: formatWeektime(col).split('~')[0],end: formatWeektime(col).split('~')[1],row: row,col: col}})}return {value: ret,row: index,child: children(ret, index, 48)}
})export default data
欢迎关注我的开源仓库
GITHUB:xiejunping (Cabber) · GitHub
微信二维码: 扫码添加好友,交个朋友
开源更新
https://github.com/xiejunping/andt-components
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
