React 之 实现 座位选择组件(可拖拉鼠标选择)

项目中遇到的组件,很容易实现,还是选择记录下来

先看看效果~

拖动框选原理: H5 自定义data属性

直接上代码:

index.jsx

import React from 'react';
import Header from './header/Header';
import Thead from './thead/Thead';

export default function TimeSelect(props) {
  return (
    <>
      <Header />
      <Thead {...props} />
    </>
  );
}

Header.jsx

import React from 'react';
import { Typography } from 'antd';
import { HOUR_LIST } from '../const';

import './index.less';

export default function Header() {
  return (
    <div className="table_header_wrapper">
      <div className="left">
        <Typography.Text>星期 \ 时间</Typography.Text>
      </div>
      <div className="right">
        <div className="top">
          <div className="before">00:00 - 12:00</div>
          <div className="after">12:00 - 24:00</div>
        </div>
        <div className="foot">
          {HOUR_LIST.map((item, index) => (
            <Typography key={index}>{item}</Typography>
          ))}
        </div>
      </div>
    </div>
  );
}

Header.less

.flexBase {
  display: flex;
  justify-content: center;
  align-items: center;
  background: #f9f9f9;
  box-shadow: 1px 0px 0px 0px #efefef inset, 0px -1px 0px 0px #efefef inset,
    1px 0px 0px 0px #efefef inset;
}

.table_header_wrapper {
  width: 100%;
  height: 60px;
  display: flex;
  border-top: 1px solid #efefef;
  .left:extend(.flexBase) {
    width: 73px;
    height: 100%;
    background: white;
  }
  .right {
    width: calc(100% - 73px);
    height: 100%;
    .top {
      display: flex;
      width: 100%;
      height: 40px;
      .before:extend(.flexBase) {
        width: 50%;
        height: 100%;
      }
      .after :extend(.flexBase) {
        width: 50%;
        height: 100%;
      }
    }
    .foot {
      width: 100%;
      height: calc(100% - 40px);
      display: grid;
      grid-template-columns: repeat(24, 1fr);
      .ant-typography {
        &:extend(.flexBase);
      }
    }
  }
}

Thead.jsx

import React, { useState, useRef } from 'react';
import classnames from 'classnames';
import { Space, Tag, Button, Tooltip, Typography } from 'antd';
import {
  WEEK_SERIRES,
  initTimerPicker,
  formatData,
  getInitalFormatList,
} from '../const';

import './index.less';

const INIT_START_POINT = { rowNum: -1, colNum: -1, deselect: false };
const INIT_END_POINT = { rowNum: -1, colNum: -1 };
function getRealStartEndPoint(startPoint, endPoint) {
  let startRowNum = startPoint.rowNum,
    startColNum = startPoint.colNum,
    endRowNum = endPoint.rowNum,
    endColNum = endPoint.colNum;
  if (startPoint.colNum > endPoint.colNum) {
    startColNum = endPoint.colNum;
    endColNum = startPoint.colNum;
  }
  if (startPoint.rowNum > endPoint.rowNum) {
    startRowNum = endPoint.rowNum;
    endRowNum = startPoint.rowNum;
  }
  return {
    startRowNum,
    startColNum,
    endRowNum,
    endColNum,
  };
}
const getActiveStatus = (data) => {
  const { each, startPoint, endPoint } = data;
  if (startPoint.rowNum === -1 || endPoint.rowNum === -1) {
    return each.selected;
  }
  const { startRowNum, startColNum, endRowNum, endColNum } =
    getRealStartEndPoint(startPoint, endPoint);

  if (
    each.rowNum >= startRowNum &&
    each.colNum >= startColNum &&
    each.rowNum <= endRowNum &&
    each.colNum <= endColNum
  ) {
    return !startPoint.deselect;
  }
  return each.selected;
};

export default function Thead({ value = [], onChange }) {
  const [timerPickered, setTimerPickered] = useState(initTimerPicker(value));
  const [startPoint, setStartPoint] = useState({ ...INIT_START_POINT });
  const [endPoint, setEndPoint] = useState({ ...INIT_END_POINT });
  const endPointRef = useRef(endPoint);

  const updateEndPoint = (point) => {
    endPointRef.current = { ...point };
    setEndPoint(endPointRef.current);
  };
  const handleClear = () => {
    const initData = getInitalFormatList();
    setTimerPickered(initTimerPicker(initData));
    setStartPoint({ ...INIT_START_POINT });
    setEndPoint({ ...INIT_END_POINT });
    onChange?.(initData);
  };

  const updateDataList = ({ startPoint, endPoint }) => {
    const dataList = getInitalFormatList();
    timerPickered.forEach((rows) => {
      rows.forEach((each) => {
        each.selected = getActiveStatus({
          each,
          startPoint,
          endPoint,
        });
        formatData(dataList, each);
      });
    });
    setTimerPickered([...timerPickered]);
    setStartPoint({ ...INIT_START_POINT });
    updateEndPoint({ ...INIT_END_POINT });
    return dataList;
  };

  const triggerChange = (changedValue) => {
    const data = updateDataList({ startPoint, endPoint });
    onChange?.([...data, ...(value ?? []), ...changedValue]);
  };

  const onMouseDown = ({ target: { dataset } }) => {
    const { type, rowNum, colNum, selected } = dataset;
    if ('time-boxzone' !== type) return;
    setStartPoint({
      rowNum: +rowNum,
      colNum: +colNum,
      deselect: selected === '1',
    });
    updateEndPoint({ rowNum: +rowNum, colNum: +colNum });
  };

  const onMouseUp = ({ target: { dataset } }) => {
    const { type, rowNum, colNum } = dataset;
    if (type !== 'time-boxzone') return;
    if (
      +rowNum !== endPointRef.current.rowNum ||
      +colNum !== endPointRef.current.colNum
    ) {
      updateEndPoint({ rowNum: +rowNum, colNum: +colNum });
    }
    requestAnimationFrame(() => {
      const list = updateDataList({
        startPoint,
        endPoint: endPointRef.current,
      });
      triggerChange(list);
    });
  };

  const onMouseOver = ({ target: { dataset } }) => {
    const { type, rowNum, colNum } = dataset;
    if ('time-boxzone' !== type) return;
    if (startPoint.colNum === -1) return;
    endPointRef.current = { rowNum: +rowNum, colNum: +colNum };
    requestAnimationFrame(() => {
      updateEndPoint(endPointRef.current);
    });
  };

  const onMouseLeave = () => {
    if (startPoint.rowNum === -1) return;
    setTimeout(() => {
      if (endPointRef.current.colNum === -1) {
        updateEndPoint({
          colNum: startPoint.colNum,
          rowNum: startPoint.rowNum,
        });
      }
      requestAnimationFrame(() => {
        const list = updateDataList({
          startPoint,
          endPoint: endPointRef.current,
        });
        triggerChange(list);
      });
    });
  };
  const Footer = (
    <div className="footer_wrapper">
      <Space>
        <Tag color="rgb(56,128,255)">已选</Tag>
        <Tag color="rgb(240,240,240)">未选</Tag>
        <Typography.Text type="secondary">可拖动鼠标选择时间段</Typography.Text>
      </Space>
      <Button type="link" onClick={handleClear}>
        清空
      </Button>
    </div>
  );

  return (
    <div className="thead_wrapper">
      <div className="thead_wrapper_content">
        <div className="week_series">
          {WEEK_SERIRES.map((item, index) => (
            <div className="week_series__item" key={index}>
              {item?.name}
            </div>
          ))}
        </div>
        <div
          className="time_series"
          data-type="right-boxzones"
          onMouseDown={onMouseDown}
          onMouseUp={onMouseUp}
          onMouseOver={onMouseOver}
          onMouseLeave={onMouseLeave}
        >
          {timerPickered.map((row, index) => (
            <div className="time_series__row" key={index}>
              {row.map((each, i) => (
                <Tooltip
                  key={`${index}_${i}`}
                  title={each?.name}
                  placement="bottom"
                >
                  <div
                    key={each?.key}
                    draggable={false}
                    data-row-num={each?.rowNum}
                    data-col-num={each?.colNum}
                    data-selected={each?.selected ? '1' : '0'}
                    data-type="time-boxzone"
                    className={classnames(
                      {
                        isActive: getActiveStatus({
                          each,
                          startPoint,
                          endPoint: endPointRef.current,
                        }),
                      },
                      'row_item',
                    )}
                  />
                </Tooltip>
              ))}
            </div>
          ))}
        </div>
      </div>
      {Footer}
    </div>
  );
}

index.less

.flexBase {
  display: flex;
  justify-content: center;
  align-items: center;
  background: #f9f9f9;
  box-shadow: 0px 0px 0px 1px #efefef;
}
.thead_wrapper {
  .thead_wrapper_content {
    display: flex;
    .week_series {
      width: 73px;
      height: 100%;
      &__item:extend(.flexBase) {
        width: 100%;
        height: 40px;
        background: white !important;
      }
    }
    .time_series {
      width: calc(100% - 73px);
      &__row {
        display: flex;
        height: 39px;
        width: 100%;
        margin-top: 1px;
        .row_item {
          width: 100%;
          height: 100%;
          margin-right: 1px;
          background: lightblue;
          cursor: pointer;
          &__content {
            display: none;
          }
          &:first-child {
            margin-left: 1px;
          }
          &:nth-child(odd) {
            background: rgb(249, 249, 249);
          }
          &:nth-child(even) {
            background: rgb(240, 240, 240);
          }
          &:hover {
            background: rgba(@primary-color, 0.4);
            transition: all 0.5s;
            .row_item__content {
              display: block;
              transition: all 0.4s;
              color: white;
            }
          }
        }
        .isActive {
          background: @primary-color !important;
          transition: all 0.5s;
        }
      }
    }
  }
  .footer_wrapper {
    display: flex;
    justify-content: space-between;
  }
}

 

const.js

import moment from 'moment';

const [_, ...rest] = moment.weekdays();
export const WEEK_SERIRES = [...rest, _].map((item, index) => ({
  name: item,
  key: index,
  value: index + 1,
}));

export const HOUR_LIST = Array.from({ length: 24 }, (v, i) => i);

// export const TIME_PICKER = Array(24)
//   .fill(0)
//   .map((k, v) => `${v}:30`);

const totalHour = 48;

export const initTimerPicker = function (value = []) {
  const result = [];
  let index = 0;
  WEEK_SERIRES.forEach((week, rowNum) => {
    const weekList = [];
    let hourSpan = 0;
    while (hourSpan < totalHour) {
      const hour = Math.floor(hourSpan / 2);
      const curHour = ('00' + hour).slice(-2);
      const nextHour = ('00' + (hour + 1)).slice(-2);
      const timer =
        index % 2 === 0
          ? `${curHour}:00-${curHour}:30`
          : `${curHour}:30-${nextHour}:00`;
      const selected = value[index] === '1';
      const timerBox = {
        key: `${week.value}_${timer}`,
        name: `${week.name} ${timer}`,
        value: index,
        rowNum: rowNum + 1,
        colNum: (index % totalHour) + 1,
        selected,
      };
      weekList.push(timerBox);
      if (!selected) {
        value[index] = '0';
      }
      index++;
      hourSpan++;
    }
    result.push(weekList);
  });
  value.length = 7 * 24 * 2;
  return result;
};

export function formatData(data, timeBox) {
  data[timeBox.value] = timeBox.selected ? '1' : '0';
  return data;
}

export function getInitalFormatList() {
  return new Array(7 * 24 * 2).fill('0');
}

Thead.less
 

.flexBase {
  display: flex;
  justify-content: center;
  align-items: center;
  background: #f9f9f9;
  box-shadow: 0px 0px 0px 1px #efefef;
}
.thead_wrapper {
  .thead_wrapper_content {
    display: flex;
    .week_series {
      width: 73px;
      height: 100%;
      &__item:extend(.flexBase) {
        width: 100%;
        height: 40px;
        background: white !important;
      }
    }
    .time_series {
      width: calc(100% - 73px);
      &__row {
        display: flex;
        height: 39px;
        width: 100%;
        margin-top: 1px;
        .row_item {
          width: 100%;
          height: 100%;
          margin-right: 1px;
          background: lightblue;
          cursor: pointer;
          &__content {
            display: none;
          }
          &:first-child {
            margin-left: 1px;
          }
          &:nth-child(odd) {
            background: rgb(249, 249, 249);
          }
          &:nth-child(even) {
            background: rgb(240, 240, 240);
          }
          &:hover {
            background: rgba(@primary-color, 0.4);
            transition: all 0.5s;
            .row_item__content {
              display: block;
              transition: all 0.4s;
              color: white;
            }
          }
        }
        .isActive {
          background: @primary-color !important;
          transition: all 0.5s;
        }
      }
    }
  }
  .footer_wrapper {
    display: flex;
    justify-content: space-between;
  }
}

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

superTiger_y

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值