项目中遇到的组件,很容易实现,还是选择记录下来
先看看效果~
拖动框选原理: 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;
}
}