今日头条选择时间段组件封装(开箱即用)

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

在这里插入图片描述

线上演示地址: 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-ifv-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>
          <td
            v-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.offsetLeft
        this.top = ele.offsetTop
      } else if (item.col <= this.col && item.row <= this.row) {
        this.width = (this.col - item.col + 1) * ele.offsetWidth
        this.height = (this.row - item.row + 1) * ele.offsetHeight
        this.left = ele.offsetLeft
        this.top = ele.offsetTop
      } else if (item.col >= this.col && item.row >= this.row) {
        this.width = (item.col - this.col + 1) * ele.offsetWidth
        this.height = (item.row - this.row + 1) * ele.offsetHeight
        if (item.col > this.col && item.row === this.row)
          this.top = ele.offsetTop
        if (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.offsetWidth
        this.height = (this.row - item.row + 1) * ele.offsetHeight
        this.top = ele.offsetTop
      } else if (item.col < this.col && item.row > this.row) {
        this.width = (this.col - item.col + 1) * ele.offsetWidth
        this.height = (item.row - this.row + 1) * ele.offsetHeight
        this.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 = 1
      if (ele) {
        this.width = ele.offsetWidth
        this.height = ele.offsetHeight
      }

      this.row = item.row
      this.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 = 0
      this.height = 0
      this.mode = 0
    },
    selectWeek(row, col, check) {
      const [minRow, maxRow] = row
      const [minCol, maxCol] = col
      this.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-weektime
    v-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 same
  let i = -1
  const len = list.length
  const arr = []

  if (!len) return
  while (++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 * 1000
  const endstamp = beginstamp + 1800000

  const 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 = weektimeData

function reverWeektime (schedule) {
    letidx = 0
    for (let i = 0; i < this.weektimeData.length; i++) {
      const children = this.weektimeData[i].child
      for (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 * 1000
  const endstamp = beginstamp + 1800000

  const 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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值