功能说明
产品要做一个类似产品对比的功能,但是两者之间并不完全相似,只是某些字段需要进行比对,为了更明显的展示对比的两项,产品要求两者之间画出一条线来表示两者可以进行对比。
我在进行功能拆分之后,我发现本质上其实就是两个点之间的连线,大概实现后效果是这样子的
实现方法
当然可能还有更好的方法,会有更高的性能,此瑾代表为大家提供一个思路而已,同时代码本质只是一个demo,并未进行优化(懒)
我先来说本质,画线的本质其实就是渲染两个div盒子 ,根据要画线的两点之间的位置,计算出两个盒子的高度和宽度,然后分别按照业务情况去渲染他们的border ,再根据两点的offsetTop和offsetLeft 计算出两个盒子分别的top和left ,绝对定位到大盒子中,最后再往渲染的两点画出一个圆圈就大功完成了
在设计这个功能时,应编写出一个组件,只暴露出要渲染两个点的offsetTop和offsetLeft ,其余在内部实现,让功能保持单一原则,最小暴露原则,高内聚。
下面我将用原生Js和React的形式来为大家展示代码将如何进行编写
原生Js
首先我们先捋清楚实现这个效果需要几个步骤,可能整个功能会很难,感觉没有思路,但是可以一点点的拆分功能,最后实现这个功能,这个也是我在工作中的开发思路
通过本质,我们可以知道我们需要一个绝对定位盒子的高度和宽度,通过设置它的top和left,来达到视觉上的画线,所以要编写一个专门用来画线的函数,参数为高度,宽度,top,left和画线的type,最后再插入到父元素中
js
// 仅供参考,可优化
function createLineElement({ width, height, type, left, top }) {
const line = document.createElement('div');
line.style.width = width + 'px';
line.style.height = height + 'px';
line.style.position = 'absolute';
line.style.left = left + 'px';
line.style.top = top + 'px';
if (type === 1) {
line.style.borderBottom = '2px dashed #ff5b4d';
line.style.borderRight = '2px dashed #ff5b4d';
} else if (type === 2) {
line.style.borderTop = '2px dashed #ff5b4d';
line.style.borderLeft = '2px dashed #ff5b4d';
} else if (type === 3) {
line.style.borderTop = '2px dashed #ff5b4d';
} else if (type === 4) {
line.style.borderTop = '2px dashed #ff5b4d';
line.style.borderRight = '2px dashed #ff5b4d';
} else if (type === 5) {
line.style.borderBottom = '2px dashed #ff5b4d';
line.style.borderLeft = '2px dashed #ff5b4d';
}
fatherEL.appendChild(line);
}
但是盒子的高度我们是通过两点的offsetTop和offsetLeft来动态的计算出来的 ,我们需要考虑当两个点的高度,哪边高,哪边低,来进行动态的渲染 ,其次大家也可在这里来编写业务中需要几种类型的画线方式 ,是主要的逻辑处理区
js
//仅供参考,可优化
function renderElement({ leftEl, rightEl, type }) {
const { LeftScroll, LeftScrollTop } = leftEl;
const { rightScroll, rightScrollTop } = rightEl;
const middleWidth = boxEl.clientWidth / 2;
const styleEl1 = {
height: 0,
width: 0,
type: 1,
left: LeftScroll,
top: 0,
};
const styleEl2 = {
height: 0,
width: 0,
type: 3,
left: middleWidth,
top: 0,
};
if (type === 1) {
if (rightScrollTop > LeftScrollTop) {
styleEl1.height = rightScrollTop - LeftScrollTop;
styleEl2.height = rightScrollTop - LeftScrollTop;
styleEl1.top = LeftScrollTop;
styleEl2.top = LeftScrollTop + styleEl2.height;
styleEl1.type = 4;
styleEl2.type = 3;
} else {
styleEl1.height = LeftScrollTop - rightScrollTop;
styleEl2.height = LeftScrollTop - rightScrollTop;
styleEl1.top = LeftScrollTop - styleEl1.height;
styleEl2.top = LeftScrollTop - styleEl2.height;
}
styleEl1.width = middleWidth - LeftScroll;
styleEl2.width = rightScroll - middleWidth;
createCircleElement(LeftScroll, LeftScrollTop, '>');
createCircleElement(rightScroll, rightScrollTop, '>');
} else if (type === 2) {
styleEl1.height = 22;
styleEl1.width = middleWidth - LeftScroll;
styleEl2.width = rightScroll - middleWidth + 5;
styleEl1.top = LeftScrollTop;
styleEl1.type = 5;
if (rightScrollTop > LeftScrollTop) {
styleEl2.height = rightScrollTop - LeftScrollTop - 22;
styleEl2.width = rightScroll - middleWidth + 5;
styleEl2.type = 5;
styleEl2.top = LeftScrollTop + 22;
createCircleElement(LeftScroll, LeftScrollTop, '^');
createCircleElement(rightScroll, rightScrollTop, '>');
} else {
styleEl2.height = LeftScrollTop - rightScrollTop + 22;
styleEl2.type = 2;
styleEl2.top = rightScrollTop;
createCircleElement(LeftScroll, LeftScrollTop, '^');
createCircleElement(rightScroll, rightScrollTop, '>');
}
} else {
styleEl1.height = 22;
styleEl1.width = middleWidth - LeftScroll;
styleEl1.top = LeftScrollTop - 22;
styleEl1.type = 2;
styleEl2.width = rightScroll - middleWidth + 5;
if (rightScrollTop > LeftScrollTop) {
styleEl2.height = rightScrollTop - LeftScrollTop + 22;
styleEl2.type = 5;
styleEl2.top = LeftScrollTop - 22;
createCircleElement(LeftScroll, LeftScrollTop, '^');
createCircleElement(rightScroll, rightScrollTop, '>');
} else {
styleEl1.width = middleWidth - LeftScroll;
styleEl2.height = LeftScrollTop - rightScrollTop - 22;
styleEl2.type = 2;
styleEl2.top = rightScrollTop;
createCircleElement(LeftScroll, LeftScrollTop, '^');
createCircleElement(rightScroll, rightScrollTop, '>');
}
}
createLineElement(styleEl1);
createLineElement(styleEl2);
}
其次就是画圆圈的逻辑,其实逻辑大多数是相似的,这里就不一一赘述了
React代码
其实用React来编写时候,更加的方便,因为他会根据传入的值,动态的去渲染页面逻辑,这时在页面改动,需要动态更新画线点的时候会更加的方便
jsx
import React from 'react';
import { DownOutlined, UpOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons';
function CreateLineElement({ width, height, type, left, top }) {
let style = {
width: width + 'px',
height: height + 'px',
position: 'absolute',
left: left + 'px',
top: top + 'px',
};
if (type === 1) {
style = { ...style, borderBottom: '2px dashed #ff5b4d', borderRight: '2px dashed #ff5b4d' };
} else if (type === 2) {
style = { ...style, borderTop: '2px dashed #ff5b4d', borderLeft: '2px dashed #ff5b4d' };
} else if (type === 3) {
style = { ...style, borderTop: '2px dashed #ff5b4d' };
} else if (type === 4) {
style = { ...style, borderTop: '2px dashed #ff5b4d', borderRight: '2px dashed #ff5b4d' };
} else if (type === 5) {
style = { ...style, borderBottom: '2px dashed #ff5b4d', borderLeft: '2px dashed #ff5b4d' };
}
return <div className='render-line-hrl' style={style}></div>;
}
function CreateCircleElement({ left, top, content }) {
const style = {
top: top - 7.5 + 'px',
left: left - 7.5 + 'px',
fontSize: 12,
color: '#fff',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '15px',
height: '15px',
borderRadius: '50%',
position: 'absolute',
backgroundColor: '#ff5b4d',
zIndex: '999',
};
return (
<div className='render-circle-hrl' style={style}>
{content}
</div>
);
}
function RenderElement({ leftEl, rightEl, type, boxEl }) {
const { LeftScroll, LeftScrollTop } = leftEl;
const { rightScroll, rightScrollTop } = rightEl;
let circleLeftContent = '';
let circleRightContent = '';
const middleWidth = boxEl?.clientWidth / 2;
const styleEl1 = {
height: 0,
width: 0,
type: 1,
left: LeftScroll,
top: 0,
};
const styleEl2 = {
height: 0,
width: 0,
type: 3,
left: middleWidth,
top: 0,
};
if (type === 1) {
if (rightScrollTop > LeftScrollTop) {
styleEl1.height = rightScrollTop - LeftScrollTop;
styleEl2.height = rightScrollTop - LeftScrollTop;
styleEl1.top = LeftScrollTop;
styleEl2.top = LeftScrollTop + styleEl2.height;
styleEl1.type = 4;
styleEl2.type = 3;
} else {
styleEl1.height = LeftScrollTop - rightScrollTop;
styleEl2.height = LeftScrollTop - rightScrollTop;
styleEl1.top = LeftScrollTop - styleEl1.height;
styleEl2.top = LeftScrollTop - styleEl2.height;
}
circleLeftContent = <DownOutlined />;
circleRightContent = <RightOutlined />;
styleEl1.width = middleWidth - LeftScroll;
styleEl2.width = rightScroll - middleWidth;
} else if (type === 2) {
styleEl1.height = 22;
styleEl1.width = middleWidth - LeftScroll;
styleEl2.width = rightScroll - middleWidth + 5;
styleEl1.top = LeftScrollTop;
styleEl1.type = 5;
circleLeftContent = <UpOutlined />;
circleRightContent = <RightOutlined />;
if (rightScrollTop > LeftScrollTop) {
styleEl2.height = rightScrollTop - LeftScrollTop - 22;
styleEl2.width = rightScroll - middleWidth + 5;
styleEl2.type = 5;
styleEl2.top = LeftScrollTop + 22;
} else {
styleEl2.height = LeftScrollTop - rightScrollTop + 22;
styleEl2.type = 2;
styleEl2.top = rightScrollTop;
}
} else {
styleEl1.height = 22;
styleEl1.width = middleWidth - LeftScroll;
styleEl1.top = LeftScrollTop - 22;
styleEl1.type = 2;
styleEl2.width = rightScroll - middleWidth + 5;
circleLeftContent = <UpOutlined />;
circleRightContent = <RightOutlined />;
if (rightScrollTop > LeftScrollTop) {
styleEl2.height = rightScrollTop - LeftScrollTop + 22;
styleEl2.type = 5;
styleEl2.top = LeftScrollTop - 22;
} else {
styleEl1.width = middleWidth - LeftScroll;
styleEl2.height = LeftScrollTop - rightScrollTop - 22;
styleEl2.type = 2;
styleEl2.top = rightScrollTop;
}
}
return (
<>
<CreateLineElement {...styleEl1} />
<CreateCircleElement left={LeftScroll} top={LeftScrollTop} content={circleLeftContent} />
<CreateLineElement {...styleEl2} />
<CreateCircleElement left={rightScroll} top={rightScrollTop} content={circleRightContent} />
</>
);
}
function Render({ renderList, type, boxEl }) {
return renderList.map((item, index) => {
return <RenderElement leftEl={item.leftEl} key={index} rightEl={item.rightEl} type={item.type} boxEl={boxEl} />;
});
}
export default Render;
因为要画的线可能不止一条,所以传入一个数组来渲染,同时使用useState来传入数据,可以达到动态删除旧的元素和渲染新的元素
总结
其实在我实际开发中,画线的技术倒不是一个难点,只是想到动态添加div去渲染有点想不到(有没有可能只有我想不出来)
至于如何取到offsetTop和offsetLeft,有很多办法,我推荐的是两个对比点使用相同的类名,在不同的父节点中取到相同的类名,这样可以提高一下复用性。