万事开头难,一直都想写博客,分享下自己的东西,科室无奈于工作比较忙,也不知道写点啥好,太浅了吧,觉得没意思,太深吧,我也不会,刚好公司最近要我们封装一批组件,就借花献佛拿出来分享一下吧!之前也没写过凑合看吧
哈哈。。。
首先来看下这个组件的样子, 它长这样:
主要功能包括:
- 组件内部展示内容随意编辑,以便组件更好的复用(timeLineHTML);
- 点击左右箭头可以使时间轴左右滑动,每次移动量以单元格数量为准(moveCount),不能点击时鼠标指针给与相应的提示,鼠标按住时间轴中间任意位置拖动可实现相同效果(mouseSliding);
- 组件可以自动适应父元素宽高并且屏幕宽度变化时做到响应(height);
接下来看下代码和思路吧!
首先观察一下组件的样式左右两个按钮且宽度固定,响应式改变的主要是中间部分的宽度,
antd组价选择使用Layout布局来写,代码如下:
布局定下来了估计剩下的都会写我就不啰嗦了,需要注意的是我们在componentDidMount生命周期中记录下页面初次更新时,每一个单元格的位置作为初始位置,
下面主要说下鼠标触发的滚动是怎么是实现的;
废话不多说直接上代码:
// 时间轴滚动事件
timeLineClick = (direction) => {
const {moveCount} = this.props;
let self = this;
// 移动过程中禁止点击
if (!this.allowClick) return;
// 因使用scrollLeft 未达到最大值时scrollLeft已经达到最大 临界判断
let lastPositionLeft = $('.diagnosis_box').eq(this.timeLineHTMLArr.length - 1).position().left;
let lastWidth = $('.diagnosis_box').eq(this.timeLineHTMLArr.length - 1).width();
let timeLinBoxWidth = $('#time_line_box').width() + _padding;
if (direction === 'left' && $('.diagnosis_box').eq(0).position().left >= _padding) {
return;
}
else if (direction === 'right') {
if (Math.floor(lastPositionLeft + lastWidth) <= timeLinBoxWidth) {
return
}
}
this.allowClick = false;
// 因拖拽屏幕会导致scrollLeft 有所变动 所这里重新定位下 this.timeLineCounter
let currentPosition = $('#time_line_box').scrollLeft() + _padding;
if (this.isResize || (direction === 'left' && Math.floor(lastPositionLeft + lastWidth) <= timeLinBoxWidth)) {
this.timeLineHTMLArr.some((item, index) => {
if (index < this.timeLineHTMLArr.length - 1
&& item <= currentPosition
&& currentPosition <= this.timeLineHTMLArr[index + 1]
) {
this.timeLineCounter = this.isResize ? index + 1 : index
}
});
}
let count = 0;
// 根据操作移动timeline
if (direction === 'left') {
if (this.timeLineCounter >= 0) {
this.timeLineCounter = this.timeLineCounter - moveCount >= 0
? this.timeLineCounter - moveCount
: 0;
let leftCount = Math.floor((currentPosition - self.timeLineHTMLArr[self.timeLineCounter]) / 5);
let displacementTime = 200 / leftCount;
function moveLeft(count) {
setTimeout(() => {
self.isResize = false;
let counts = count + 1;
let _left = currentPosition - _padding - counts * 5;
$('#time_line_box').scrollLeft(_left);
if (leftCount > count) {
moveLeft(counts);
}
else {
self.allowClick = true;
let newLastPositionLeft = $('.diagnosis_box').eq(self.timeLineHTMLArr.length - 1).position().left;
self.setState({
left: self.timeLineCounter === 0 ? false : true,
right: Math.floor(newLastPositionLeft + lastWidth) <= timeLinBoxWidth ? false : true
});
}
}, displacementTime)
}
moveLeft(count);
}
}
else if (direction === 'right') {
if (this.timeLineCounter <= this.timeLineHTMLArr.length - 1) {
this.timeLineCounter = this.timeLineCounter + moveCount < this.timeLineHTMLArr.length - 1
? this.timeLineCounter + moveCount
: this.timeLineHTMLArr.length - 1;
let leftCount = Math.floor((self.timeLineHTMLArr[self.timeLineCounter] - currentPosition) / 5);
let displacementTime = 200 / leftCount;
function moveRight(count) {
setTimeout(() => {
self.isResize = false;
let counts = count + 1;
let _left = currentPosition - _padding + counts * 5;
$('#time_line_box').scrollLeft(_left);
if (leftCount > count) {
moveRight(counts);
}
else {
self.allowClick = true;
let newLastPositionLeft = $('.diagnosis_box').eq(self.timeLineHTMLArr.length - 1).position().left;
self.setState({
left: self.timeLineCounter === 0 ? false : true,
right: Math.floor(newLastPositionLeft + lastWidth) <= timeLinBoxWidth ? false : true
});
}
}, displacementTime)
}
moveRight(count);
}
}
};
这里的逻辑主要是鼠标点击触发的过程,需要注意的几点:
- 第一位移需要时间所以我们需要在唯一过程中,做一个节流,在位移结束前,不接受点击事件;
- 由于每一个位移单位的距离不同,就会出现单元格长的位移时间也会过长,视觉效果也不是很好,所以里面我们固定每次位移时间为200,用setTimeout递归调用;
- 在封装组件的时候遇到一个比较尴尬的问题,第一个单元格不在最左端的时候鼠标拖拽浏览器晃动改变大小,会使页面发生改变,导致计算好的位置出现偏差(使用鼠标拖拽发生位移时也需要这样处理),所以代码中会出现这样一部分处理:
目的是为了修正偏移量;剩下的就不多说了直接看代码吧!
import React, {PureComponent} from 'react';
import styles from './TimeLine.less';
import {Icon, Layout} from 'antd';
import $ from "jquery";
import PropTypes from 'prop-types';
/*
* 公共时间轴 组件 接受定制显示模块 此组件提供模拟 获取TimeLineHTML函数
* 必须给定特性的 className = diagnosis_box; 每次点击保证diagnosis_box到达最左侧
* 自己定制部分的样式在 父组件内定义
* _padding 为time_line_box两边的padding 因纳入原生计算 所以统一赋值
* */
const _padding = 24;
const {Sider, Content} = Layout;
let eleDrag = false;
class TimeLine extends PureComponent {
static propTypes = {
timeLineHTML: PropTypes.array.isRequired, // 默认选项
moveCount: PropTypes.number, // 每次点击移动个数
height: PropTypes.number,
mouseSliding: PropTypes.bool // 是否支持鼠标滑动事件
};
static defaultProps = {
moveCount: 3,
height: 65,
mouseSliding: true
};
constructor(props) {
super(props);
this.state = {
left: false,
right: true
};
this.timeLineHTMLArr = [];
this.timeLineCounter = 0;
this.allowClick = true;
this.isResize = false;
this.initClientX = null;
}
componentDidMount() {
let self = this;
// 首次进入加载所有模块的positionleft 作为移动距离判定
if (!this.timeLineHTMLArr.length) {
$('.diagnosis_box').each(function () {
self.timeLineHTMLArr.push($(this).position().left);
});
}
window.onresize = () => {
let target = this;
if (target.resizeFlag) {
clearTimeout(target.resizeFlag);
}
target.resizeFlag = setTimeout(function () {
self.isResize = true;
target.resizeFlag = null;
}, 100);
};
}
// 时间轴滚动事件
timeLineClick = (direction) => {
const {moveCount} = this.props;
let self = this;
// 移动过程中禁止点击
if (!this.allowClick) return;
// 因使用scrollLeft 未达到最大值时scrollLeft已经达到最大 临界判断
let lastPositionLeft = $('.diagnosis_box').eq(this.timeLineHTMLArr.length - 1).position().left;
let lastWidth = $('.diagnosis_box').eq(this.timeLineHTMLArr.length - 1).width();
let timeLinBoxWidth = $('#time_line_box').width() + _padding;
if (direction === 'left' && $('.diagnosis_box').eq(0).position().left >= _padding) {
return;
}
else if (direction === 'right') {
if (Math.floor(lastPositionLeft + lastWidth) <= timeLinBoxWidth) {
return
}
}
this.allowClick = false;
// 因拖拽屏幕会导致scrollLeft 有所变动 所这里重新定位下 this.timeLineCounter
let currentPosition = $('#time_line_box').scrollLeft() + _padding;
if (this.isResize || (direction === 'left'
&& Math.floor(lastPositionLeft + lastWidth) <= timeLinBoxWidth)) {
this.timeLineHTMLArr.some((item, index) => {
if (index < this.timeLineHTMLArr.length - 1
&& item <= currentPosition
&& currentPosition <= this.timeLineHTMLArr[index + 1]
) {
this.timeLineCounter = this.isResize ? index + 1 : index
}
});
}
let count = 0;
// 根据操作移动timeline
if (direction === 'left') {
if (this.timeLineCounter >= 0) {
this.timeLineCounter = this.timeLineCounter - moveCount >= 0
? this.timeLineCounter - moveCount
: 0;
let leftCount = Math.floor((currentPosition - self.timeLineHTMLArr[self.timeLineCounter]) / 5);
let displacementTime = 200 / leftCount;
function moveLeft(count) {
setTimeout(() => {
self.isResize = false;
let counts = count + 1;
let _left = currentPosition - _padding - counts * 5;
$('#time_line_box').scrollLeft(_left);
if (leftCount > count) {
moveLeft(counts);
}
else {
self.allowClick = true;
let newLastPositionLeft = $('.diagnosis_box').eq(self.timeLineHTMLArr.length - 1).position().left;
self.setState({
left: self.timeLineCounter === 0 ? false : true,
right: Math.floor(newLastPositionLeft + lastWidth) <= timeLinBoxWidth ? false : true
});
}
}, displacementTime)
}
moveLeft(count);
}
}
else if (direction === 'right') {
if (this.timeLineCounter <= this.timeLineHTMLArr.length - 1) {
this.timeLineCounter = this.timeLineCounter + moveCount < this.timeLineHTMLArr.length - 1
? this.timeLineCounter + moveCount
: this.timeLineHTMLArr.length - 1;
let leftCount = Math.floor((self.timeLineHTMLArr[self.timeLineCounter] - currentPosition) / 5);
let displacementTime = 200 / leftCount;
function moveRight(count) {
setTimeout(() => {
self.isResize = false;
let counts = count + 1;
let _left = currentPosition - _padding + counts * 5;
$('#time_line_box').scrollLeft(_left);
if (leftCount > count) {
moveRight(counts);
}
else {
self.allowClick = true;
let newLastPositionLeft = $('.diagnosis_box').eq(self.timeLineHTMLArr.length - 1).position().left;
self.setState({
left: self.timeLineCounter === 0 ? false : true,
right: Math.floor(newLastPositionLeft + lastWidth) <= timeLinBoxWidth ? false : true
});
}
}, displacementTime)
}
moveRight(count);
}
}
};
// 拖拽鼠标使用时间轴滑动
onMouseDown = (e) => {
e.stopPropagation();
e.preventDefault();
eleDrag = true;
this.initClientX = e.clientX;
};
onMouseMoveCapture = (e) => {
e.stopPropagation();
e.preventDefault();
if (!this.allowClick) return;
if (eleDrag) {
let currentPosition = $('#time_line_box').scrollLeft();
if (this.initClientX > e.clientX) { // 向右拖动鼠标
let lastPositionLeft = $('.diagnosis_box').eq(this.timeLineHTMLArr.length - 1).position().left;
let lastWidth = $('.diagnosis_box').eq(this.timeLineHTMLArr.length - 1).width();
let timeLinBoxWidth = $('#time_line_box').width() + _padding;
if (Math.floor(lastPositionLeft + lastWidth) <= timeLinBoxWidth) {
return
}
let displacement = this.initClientX - e.clientX;
$('#time_line_box').scrollLeft(currentPosition + displacement);
}
else if (this.initClientX < e.clientX) {// 向左拖动鼠标
if ($('.diagnosis_box').eq(0).position().left >= _padding) {
return;
}
let displacement = currentPosition - (e.clientX - this.initClientX);
$('#time_line_box').scrollLeft(displacement);
}
this.initClientX = e.clientX;
}
};
noDragging = (e) => {
e.stopPropagation();
e.preventDefault();
eleDrag = false;
this.initClientX = null;
};
render() {
const {
props: {
timeLineHTML,
height,
mouseSliding
},
state: {
left,
right,
}
} = this;
return (
<div
className={styles.TimeLine}
style={{
height,
lineHeight: `${height}px`
}}>
<Layout>
<Sider
width={20}
style={{
position: 'relative',
background: 'rgba(0, 0, 0, .25)',
textAlign: 'center',
cursor: left ? 'pointer' : 'not-allowed'
}}>
<span
style={{
position: 'absolute',
left: '-1px',
width: '100%',
height: '100%',
textAlign: 'center',
display: 'inline-block',
cursor: left ? 'pointer' : 'not-allowed'
}}
onMouseEnter={() => {
if ($('.diagnosis_box').eq(0).position().left >= _padding - 0.01) {
this.setState({left: false});
}
else {
this.setState({left: true});
}
}}
onClick={() => {
this.timeLineClick('left')
}}
/>
<Icon type="left"/>
</Sider>
<Content
style={{
height: '100%',
overflow: 'hidden',
position: 'relative'
}}>
{mouseSliding
? <div
onMouseDown={this.onMouseDown}
onMouseMoveCapture={this.onMouseMoveCapture}
onMouseUp={this.noDragging}
onMouseLeave={this.noDragging}
id='time_line_box'
className={styles.time_line_box}>
{timeLineHTML}
</div>
: <div
id='time_line_box'
className={styles.time_line_box}>
{timeLineHTML}
</div>}
</Content>
<Sider
width={20}
style={{
position: 'relative',
background: 'rgba(0, 0, 0, .25)',
textAlign: 'center',
cursor: right ? 'pointer' : 'not-allowed'
}}>
<span
style={{
position: 'absolute',
left: '-1px',
width: '100%',
height: '100%',
textAlign: 'center',
display: 'inline-block',
cursor: right ? 'pointer' : 'not-allowed'
}}
onMouseEnter={() => {
let lastPositionLeft = $('.diagnosis_box').eq(this.timeLineHTMLArr.length - 1).position().left;
let lastWidth = $('.diagnosis_box').eq(this.timeLineHTMLArr.length - 1).width();
let timeLinBoxWidth = $('#time_line_box').width() + _padding;
if (Math.floor(lastPositionLeft + lastWidth) <= timeLinBoxWidth) {
this.setState({right: false});
}
else {
this.setState({right: true});
}
}}
onClick={() => {
this.timeLineClick('right')
}}
/>
<Icon type="right"/>
</Sider>
</Layout>
</div>
)
}
}
export default TimeLine;
下面是对应的less:
.TimeLine{
height: 100%;
:global{
.ant-layout.ant-layout-has-sider{
height: 100%;
}
}
.time_line_box{
position: relative;
padding: 0 24px;
height: 100%;
overflow: hidden;
white-space: nowrap;
}
}
下面是技术总结,主要说下这个组件需要注意的地方:
- 鼠标点击事件需要做判断,在唯一过程中应禁止下一次点击事件;
- 其次鼠标拖拽以后,再次点击移动或者屏幕宽度发生变化后在次点击时,需要校正一下偏移位置;
- 注意鼠标不可点击时的临界判断;
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。