TDesign——投放时间段组件(48 * 7 位字符串)

发布时间 2023-06-06 17:34:14作者: 。思索

前言

基于tdesign vue-next + ts实现
参考: byte-weektime-picker

内容

<!-- eslint-disable no-bitwise -->
<template>
  <div class="weektime">
    <div class="weektime-main">
      <div class="weektime-hd">
        <div class="weektime-hd-title">星期\时间</div>
        <div class="weektime-hd-con">
          <div class="weektime-hd-con-top">
            <div class="weektime-date-range">00:00 - 12:00</div>
            <div class="weektime-date-range">12:00 - 24:00</div>
          </div>
          <div class="weektime-hd-con-bottom">
            <span v-for="hour in 24" :key="hour" class="weektime-date-cell">{{ hour - 1 }}</span>
          </div>
        </div>
      </div>
      <div class="weektime-bd">
        <div class="week-body">
          <div v-for="week in weekDays" :key="week" class="week-item">{{ week }}</div>
        </div>
        <div class="time-body" @mousedown="handleMousedown" @mouseup="handleMouseup" @mousemove="handleMousemove">
          <t-tooltip v-for="(i, key) in weekTimes" :key="key" :content="tipTitle(key)">
            <div
              :data-index="key"
              class="time-cell"
              :class="{
                active: list[key] === '1',
                'pre-active': preViewIndex.includes(key),
                disable: disableTimes.includes(key),
              }"
            ></div>
          </t-tooltip>
        </div>
      </div>
    </div>
    <div class="weektime-help">
      <div class="weektime-help-tx">
        <div class="weektime-help-bd">
          <span class="color-box"></span>
          <span class="text-box">未选</span>
          <span class="color-box color-active"></span>
          <span class="text-box">已选</span>
        </div>
        <div class="weektime-help-ft" @click="initList">清空选择</div>
      </div>
      <div class="weektime-help-select">
        <p v-for="(week, key) in weekDays" v-show="showTimeText[key]" :key="key">
          <span class="weektime-help-week-tx">{{ week + ':' }}</span>
          <span>{{ showTimeText[key] }}</span>
        </p>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';

const props = defineProps({
  value: {
    type: String,
  },
  // 自定义开始时段,0-47,每1代表半小时,星期一到星期日都生效
  startTime: {
    type: Number,
  },
  // 自定义结束时段,0-47,每1代表半小时,星期一到星期日都生效
  endTime: {
    type: Number,
  },
  // 自定义禁用时段,数字数组,0-335
  customDisableTimes: {
    type: Array,
  },
});

const DayTimes = 24 * 2;
const list = ref([]);
const isMove = ref(false);
const weekTimes = ref(7 * DayTimes);
const weekDays = ref(['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日']);
const timeTextList = ref([]);
const startIndex = ref(0);
const axis = ref({
  startX: null,
  startY: null,
  endX: null,
  endY: null,
});
const preViewIndex = ref([]);
const showTimeText = ref([]);

watch(
  () => props.value,
  (n) => {
    if (n === list.value.join('')) return;
    initList(n);
  },
);

const emit = defineEmits(['update:value']);

const disableTimes = computed(() => {
  if (Array.isArray(props.customDisableTimes) && props.customDisableTimes.every((num) => typeof num === 'number'))
    return props.customDisableTimes;
  if (props.startTime > -1 && props.endTime > -1) {
    const disabled = [];
    for (let index = 0; index < weekTimes.value; index++) {
      const firstIdx = index % DayTimes;
      if (props.startTime > firstIdx || props.endTime < firstIdx) disabled.push(index);
    }
    return disabled;
  }
  return [];
});

/**
 * 鼠标停留时提示当前时间段
 */
const tipTitle = (index) => {
  const timeIndex = index % DayTimes;
  // eslint-disable-next-line no-bitwise
  const weekIndex = ~~(index / DayTimes);
  return `${weekDays.value[weekIndex]} ${timeTextList.value[timeIndex]}~${timeTextList.value[timeIndex + 1]}`;
};

/**
 * 初始化显示的时间数组
 * @return {Array} ["00:00","00:30","01:00",...]
 */
const initTimeText = () => {
  const timeTextList = [];
  const hours = [];
  const minutes = ['00', '30'];
  for (let i = 0; i <= 24; i++) {
    // eslint-disable-next-line no-unused-expressions
    i < 10 ? hours.push(`0${i}`) : hours.push(i.toString());
  }
  for (const hour of hours) {
    for (const minute of minutes) {
      timeTextList.push(`${hour}:${minute}`);
    }
  }
  return timeTextList;
};

const handleMousedown = (event) => {
  startIndex.value = event.target.getAttribute('data-index');
  // eslint-disable-next-line no-bitwise
  if (disableTimes.value.includes(~~startIndex.value)) return;
  isMove.value = true;
  axis.value.startX = startIndex.value % DayTimes;
  // eslint-disable-next-line no-bitwise
  axis.value.startY = ~~(startIndex.value / DayTimes);
};
const handleMouseup = (event) => {
  handleMousemove(event);
  resetMousemove();
};

const handleMousemove = (event) => {
  if (!isMove.value) return;
  const index = event.target.getAttribute('data-index');
  axis.value.endX = index % DayTimes;
  // eslint-disable-next-line no-bitwise
  axis.value.endY = ~~(index / DayTimes);
  preViewIndex.value = getSelectIndex();
};
const resetMousemove = () => {
  if (!isMove.value) return;
  setSelectIndex(preViewIndex.value);
  isMove.value = false;
  axis.value = {
    startX: null,
    startY: null,
    endX: null,
    endY: null,
  };
  preViewIndex.value = [];
};

/**
 * 获取拖动鼠标选择的index数组
 */
const getSelectIndex = () => {
  const indexList = [];
  const newAxis = {
    startX: Math.min(axis.value.startX, axis.value.endX),
    startY: Math.min(axis.value.startY, axis.value.endY),
    endX: Math.max(axis.value.startX, axis.value.endX),
    endY: Math.max(axis.value.startY, axis.value.endY),
  };
  for (let y = newAxis.startY; y <= newAxis.endY; y++) {
    for (let x = newAxis.startX; x <= newAxis.endX; x++) {
      indexList.push(x + y * DayTimes);
    }
  }
  return indexList.filter((v) => !disableTimes.value.includes(v));
};

/**
 * 设置选择的时间段并赋给绑定的值
 * @param {Array} indexList 选择的index数组
 */
const setSelectIndex = (indexList) => {
  if (!Array.isArray(indexList)) return;
  const listLength = indexList.length;
  const newData = list.value[startIndex.value] === '1' ? '0' : '1';
  for (let i = 0; i < listLength; i++) {
    list.value.splice(indexList[i], 1, newData);
  }
  emit('update:value', list.value.join(''));
  showSelectTime(list.value);
};

/**
 * 展示选择的时间段
 * @param {Array} list 已选择的list数组
 */
const showSelectTime = (list) => {
  if (!Array.isArray(list)) return;
  const weeksSelect = [];
  const listLength = list.length;
  showTimeText.value = [];
  if (listLength === 0) return;
  // 把 336长度的 list 分成 7 组,每组 48 个
  for (let i = 0; i < listLength; i += DayTimes) {
    weeksSelect.push(list.slice(i, i + DayTimes));
  }
  weeksSelect.forEach((item) => {
    showTimeText.value.push(getTimeText(item));
  });
};

const getTimeText = (arrIndex) => {
  if (!Array.isArray(arrIndex)) return '';
  const timeLength = arrIndex.length;
  let isSelect = false;
  let timeText = '';
  arrIndex.forEach((value, index) => {
    if (value === '1') {
      if (!isSelect) {
        timeText += timeTextList.value[index];
        isSelect = true;
      }
      if (index === timeLength - 1) timeText += `~${timeTextList.value[index + 1]}、`;
    } else if (isSelect) {
      timeText += `~${timeTextList.value[index]}、`;
      isSelect = false;
    }
  });
  return timeText.slice(0, -1);
};

// eslint-disable-next-line consistent-return
const initList = (value: any) => {
  const reg = new RegExp(`^[01]{${weekTimes.value}}$`);
  if (value && reg.test(value)) {
    list.value = value.split('');
    return showSelectTime(list.value);
  }
  list.value = new Array(weekTimes.value).fill('0');
  emit('update:value', list.value.join(''));
  showSelectTime(list.value);
};

onMounted(() => {
  timeTextList.value = initTimeText();
  document.addEventListener('mouseup', resetMousemove);
  initList(props.value);
});

onUnmounted(() => {
  document.removeEventListener('mouseup', resetMousemove);
});
</script>

<style lang="less" scoped>
div,
span,
p {
  margin: 0;
  padding: 0;
  border: 0;
  font-weight: normal;
  vertical-align: baseline;
  -webkit-tap-highlight-color: transparent;
  -ms-tap-highlight-color: transparent;
  -moz-box-sizing: border-box;
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
}
.weektime {
  width: 658px;
  font-size: 14px;
  line-height: 32px;
  color: #515a6e;
  user-select: none;
}
.weektime .weektime-main {
  border: 1px solid #dcdee2;
  position: relative;
}
.weektime .weektime-hd {
  display: flex;
  background: #f8f8f9;
}
.weektime .weektime-hd-title {
  display: flex;
  align-items: center;
  padding: 0 6px;
  width: 80px;
  height: 65px;
}
.weektime .weektime-hd-con {
  flex: 1;
  display: flex;
  -webkit-box-orient: vertical;
  flex-direction: column;
}
.weektime .weektime-hd-con-top {
  display: flex;
  border-bottom: 1px solid #dcdee2;
}
.weektime .weektime-date-range {
  width: 288px;
  height: 32px;
  line-height: 32px;
  text-align: center;
  border-left: 1px solid #dcdee2;
}
.weektime .weektime-hd-con-bottom {
  display: flex;
}
.weektime .weektime-date-cell {
  width: 24px;
  height: 32px;
  line-height: 32px;
  text-align: center;
  border-left: 1px solid #dcdee2;
}
.weektime .weektime-bd {
  display: flex;
}
.weektime .week-body {
  width: 80px;
  flex-shrink: 0;
}
.weektime .week-item {
  border-top: 1px solid #dcdee2;
  text-align: center;
  height: 30px;
  line-height: 30px;
}
.weektime .time-body {
  width: 576px;
  height: 210px;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  position: relative;
}
.weektime .time-cell {
  position: relative;
  width: 12px;
  height: 30px;
  border-left: 1px solid #efefef;
  border-top: 1px solid #efefef;
  overflow: hidden;
  transition: all 0.3s ease;
  outline-width: 0;
}
.weektime .time-cell.active {
  background: #2d8cf0;
}
.weektime .time-cell.disable {
  cursor: no-drop;
}
.weektime .time-cell::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: transparent;
  opacity: 0.5;
  transition: all 866ms ease;
  z-index: 99999;
}
.weektime .pre-active::after {
  background: #113860;
}
.weektime .disable::after {
  background: #cccccc;
}
.time-area {
  width: 576px;
  height: 210px;
  position: absolute;
  top: 0;
  left: 0;
  z-index: 100;
  background: transparent;
}
.weektime .weektime-help {
  width: 658px;
  border: 1px solid #dcdee2;
  border-top: none;
  padding: 5px 15px;
}
.weektime .weektime-help-tx {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.weektime .weektime-help-week-tx {
  color: #999;
}

.weektime .weektime-help-bd {
  display: flex;
  align-items: center;
  -webkit-box-pack: start;
  -ms-flex-pack: start;
  justify-content: flex-start;
  padding: 4px 0;
}
.weektime .weektime-help .color-box {
  width: 14px;
  height: 20px;
  background: #fff;
  border: 1px solid #dddddd;
  display: block;
  margin-right: 6px;
}
.weektime .weektime-help-bd .color-box.color-active {
  background: #2d8cf0;
}
.weektime .weektime-help .text-box {
  margin-right: 15px;
}
.weektime .weektime-help .weektime-help-ft {
  color: #2d8cf0;
  cursor: pointer;
}
</style>