定制化开发——时间轴组件
一、需求
1、开发一个展示轨迹数据用的时间轴组件;
2、后端传入是一个 list,数据格式如下:
const dataRalation = [
{
entityID : "111", // 主体id
accompanyID : "222", // 伴随id
entityTime : "2019-08-05 22:26:34", 时间
accompanyTime : "2019-08-05 22:33:31", // 伴随时间
entitySpace : "未知之地" , // 地点
accompanySpace : "未知之地" , // 伴随地点
entiyImg : "no_photo.png" , // 图片
accompanyImg : "no_photo.png" , // 伴随图片
},
......
]
3、点击日期,其对应的下列数据可以收起
成果如下图:
&
二、思路
1、根据时间将传入的 list 数据转化为map。
2、提取日期作为map的键,剩下数据仍以list的形式作为map键值对的值。
3、遍历map,根据map的数据来生成组件
三、代码
3.1、第一版代码
这是我当时写的第一版代码,其中有个bug。就是测试的时候发现一点击日期所有的展示数据都收起来了。导致的原因是控制展示的 displaytype是遍历生成的。故,组件中的的this.state.displaytype,指代了所有小组件的displaytype。当触发clickTitle方法时,所有的displaytype全部改变,没有实现第三点需求。
import * as React from 'react';
import {format} from 'date-fns';
import PerfectScrollbar from 'perfect-scrollbar';
import {Text} from '../../common/text';
import { IconFont } from '../../common/iconFont';
export interface IProps{
// 关系对象数据
data : any,
// 左边加载的图片
leftImg : string,
// 右边加载的图片
rightImg : string,
// 颜色数组
color : string[],
// 主体对象类型
objectType : string,
// 关联对象类型
relationType : string
}
// IListType[]
export interface IListType {
objectaddress : string
objectId : string
objectTime : string
unionaddress : string
unionId : string
unionTime : string
entiyImg : string
accompanyImg : string
}
export class TimeShaft extends React.PureComponent<IProps,any>{
private dom: React.RefObject<HTMLDivElement> ;
private scroller: PerfectScrollbar | undefined ;
constructor(props:IProps){
super(props);
this.dom = React.createRef();
this.state = {
displayType : 'flex' ,
objectType : this.props.objectType ,
relationType : this.props.relationType
}
}
public componentWillReceiveProps(nextProps:IProps){
this.setState({
objectType : nextProps.objectType,
relationType: nextProps.relationType,
});
}
// 组件渲染之后设置滚动条样式
public componentDidMount() {
const wrapper = this.dom.current;
if (wrapper) {
this.scroller = new PerfectScrollbar(wrapper, {
wheelPropagation: false,
wheelSpeed: 2,
});
this.updateScroller();
}
}
// 组件更新后设置滚动条样式
public componentDidUpdate() {
this.updateScroller();
}
public updateScroller() {
this.scroller!.update();
}
public render() {
const html : any = [] ;
const {displayType,objectType,relationType} = this.state;
const {data,leftImg,rightImg,color} = this.props;
// 定义一个map对象
const map : Map<string,Array<[]>> = new Map<string,Array<[]>>();
// 将data组装成map
data.forEach((item:any, i:number) => {
// 取时间的年月日 dateTitle,作为map的键
const dateTitle = format(item.entityTime, 'YYYY-MM-DD');
// 如果map的键值对中没有 dateTitle,则set一个并创建一个空数组存储值
if(!map.has(dateTitle)){
map.set(dateTitle,[]);
}
// 如果map中已有该键值对,往数组中push遍历的对象item
const list = map.get(dateTitle);
if(list){
list.push(item);
}
});
// 先生成htmlbody
map.forEach((list, value) => {
const datalist : IListType[] = []
// htmlbody要写在foreach循环里面,如果写全局变量最后会把全部结果push进去
const htmlbody : any = [] ;
list.forEach((item:any, num :number) => {
datalist.push({
objectaddress : item.entitySpace,
objectId : item.entityID ,
objectTime : item.entityTime,
unionaddress : item.accompanySpace,
unionId : item.accompanyID,
unionTime : item.accompanyTime,
entiyImg : item.entiyImg ,
accompanyImg : item.accompanyImg ,
})
htmlbody.push(
<div style={{display:displayType, width:'100%'}}>
{/* 左 */}
<div className="shaft-htmlbody-main" style={{ justifyContent:'flex-end'}}>
<div className="shaft-htmlbody-dif" style={{ marginRight:'10px' }}>
<div className="shaft-htmlbody-border" style={{ border:'1px solid' + color[0] }}>
<div className="shaft-htmlbody-smallTitle" style={{backgroundColor:color[0]}}>
<img className="shaft-htmlbody-img" src={leftImg} alt=""/>
<div className="shaft-htmlbody-font">
{ objectType === '01'
? <Text widths="120px" data={datalist[num].objectId.slice(0,7)}/>
: <Text widths="120px" data={datalist[num].objectId}/> }
</div>
</div>
<div className="shaft-htmlbody-dataImg">
<img style={{ width:'100%' }} src={datalist[num].entiyImg}/>
</div>
<div className="shaft-htmlbody-time">
<div style={{width:'40%'}}>过卡时间:</div>
<Text widths="60%" data={format(datalist[num].objectTime, 'HH:mm:ss')}/>
</div>
<div className="shaft-htmlbody-space">
<div style={{width:"40%"}}>卡口地点:</div>
<Text widths="60%" data={datalist[num].objectaddress}/>
</div>
</div>
</div>
</div>
{/* 中间icon */}
<div style={{width: 2, display:'flex', flexDirection:'column'}}>
<div style={{width: 2, backgroundColor: '#e6e6e6',height:30}}>{}</div>
<div style={{width: 2, height:32}}>
<IconFont extraCommonProps={ {style:{fontSize:30,color:'#b3b3b3',marginLeft:-15}} } type="iconshijian2" />
</div>
<div style={{width: 2, backgroundColor: '#e6e6e6', flex:'1'}}>{}</div>
</div>
{/* 右 */}
<div className="shaft-htmlbody-main" style={{ justifyContent:'flex-start'}}>
<div className="shaft-htmlbody-dif" style={{marginLeft:'10px'}}>
<div className="shaft-htmlbody-border" style={{ border:'1px solid'+color[1] }}>
<div className="shaft-htmlbody-smallTitle" style={{ backgroundColor:color[1] }}>
<img className="shaft-htmlbody-img" src={rightImg} alt=""/>
<div className="shaft-htmlbody-font">
{ relationType === '01'
? <Text widths="120px" data={datalist[num].unionId.slice(0,7)}/>
: <Text widths="120px" data={datalist[num].unionId}/> }
</div>
</div>
<div className="shaft-htmlbody-dataImg">
<img style={{ width:'100%' }} src={datalist[num].accompanyImg}/>
</div>
<div className="shaft-htmlbody-time">
<div style={{width:"40%"}}>过卡时间:</div>
<Text widths="60%" data={format(datalist[num].unionTime, 'HH:mm:ss')}/>
</div>
<div className="shaft-htmlbody-space">
<div style={{width:'40%'}}>卡口地点:</div>
<Text widths="60%" data={datalist[num].unionaddress}/>
</div>
</div>
</div>
</div>
</div>
)
})
// 按value值循环生成htmlbody后,再生成html模块
html.push(
<div style={{width:'100%'}}>
<div style={{width:'100%', display:'flex', justifyContent:'center'}}>
<div className="timeline-head" onClick={this.clickTitle(value)}>{value}</div>
</div>
{htmlbody}
<div style={{height:'5px'}}>{}</div>
</div>
)
})
return(
<div style={{flex:1,width:'100%',position:'relative',padding:'15px'}} ref={this.dom}>
{html}
</div>
)
}
private clickTitle = (value:any) => {
return () => {
if (this.state.displayType === 'flex') {
this.setState({
displayType : 'none'
})
}else{
this.setState({
displayType : 'flex'
})
}
}
}
}
3.2、改进
改进思路:
当时思考了一下,认为有三种改进方法:
1、将时间title和展示数据的小模块提成一个小组件,这样小组件中的this.state.displaytype则可以分别代表对应日期数据的展示状态。
2、在生成map的时候可以加一个标记元素,当这个标记元素为ture则展示数据,为false不展示。 private clickTitle = (value:any) => {}方法根 据value来改变不同日期的标记元素。
3、dom操作,先获取到当前点击的div,再获到当前div的下级div,(即为该日期对应的展示数据)将其设为隐藏
改进说明:
我是按思路1改的。因为这种模式比较契合数据驱动ui的设计思路。另外两种方式,我觉得理论可行但未予以实践。如所述有错欢迎大家积极留言指正,如有什么疑问也欢迎各位同学留言讨论,我会尽量帮助你们弄明白。
改进后代码:
timelinebody.tsx为提取出来的小组件,timeline时间轴组件遍历map的时候生成timelinebody组件。这样每天展示数据的displaytype都是遍历生成的,可以达到点击日期,收起其对应的日期的数据的目的
时间轴:timeLine.tsx
import * as React from 'react';
import {format} from 'date-fns';
import PerfectScrollbar from 'perfect-scrollbar';
import { TimeLineBody } from './timeLineBody';
export interface IProps{
data : any,
leftImg : string,
rightImg : string,
color : string[],
iconUrl : string,
objectType : string,
relationType : string
}
export interface IListType {
objectaddress : string
objectId : string
objectTime : string
unionaddress : string
unionId : string
unionTime : string
entiyImg : string
accompanyImg : string
}
export class TimeShaft extends React.PureComponent<IProps,any>{
private dom: React.RefObject<HTMLDivElement> ;
private scroller: PerfectScrollbar | undefined ;
constructor(props:IProps){
super(props);
this.dom = React.createRef();
this.state = {
displayType : 'flex' ,
objectType : this.props.objectType ,
relationType : this.props.relationType
}
}
public componentWillReceiveProps(nextProps:IProps){
this.setState({
relationId : nextProps.objectType,
relationType: nextProps.relationType,
});
}
// 组件渲染之后设置滚动条样式
public componentDidMount() {
const wrapper = this.dom.current;
if (wrapper) {
this.scroller = new PerfectScrollbar(wrapper, {
wheelPropagation: false,
wheelSpeed: 2,
});
this.updateScroller();
}
}
// 组件更新后设置滚动条样式
public componentDidUpdate() {
this.updateScroller();
}
public updateScroller() {
this.scroller!.update();
}
public render() {
const { objectType, relationType } = this.state;
const { data, leftImg, rightImg, color } = this.props;
const body : any = [];
// 定义一个map对象
const map : Map<string,Array<[]>> = new Map<string,Array<[]>>();
// 将data组装成map
data.forEach((item:any, i:number) => {
// 取时间的年月日 dateTitle,作为map的键
const dateTitle = format(item.entityTime, 'YYYY-MM-DD');
// 如果map的键值对中没有 dateTitle,则set一个并创建一个空数组存储值
if(!map.has(dateTitle)){
map.set(dateTitle,[]);
}
// 如果map中已有该键值对,往数组中push遍历的对象item
const list = map.get(dateTitle);
if(list){
list.push(item);
}
});
map.forEach((list, value) => {
body.push(
<TimeLineBody
leftImg = {leftImg}
rightImg = {rightImg}
color = {color}
objectType = {objectType}
relationType = {relationType}
list = {list}
value = {value}
/>
)
})
return(
<div style={{flex:1,width:'100%',position:'relative'}} ref={this.dom}>
{body}
</div>
)
}
}
提取出来的小组件:timeLineBody.tsx
import * as React from 'react';
import {format} from 'date-fns';
import PerfectScrollbar from 'perfect-scrollbar';
import {Text} from '../../common/text';
import { IconFont } from '../../common/iconFont';
export interface IProps{
leftImg : string,
rightImg : string,
color : string[],
objectType : string,
relationType : string,
list : any,
value : any,
}
export interface IListType {
objectaddress : string
objectId : string
objectTime : string
unionaddress : string
unionId : string
unionTime : string
entiyImg : string
accompanyImg : string
}
export class TimeLineBody extends React.PureComponent<IProps,any>{
constructor(props:IProps){
super(props);
this.state = {
displayType : 'flex' ,
objectType : this.props.objectType ,
relationType : this.props.relationType
}
}
public componentWillReceiveProps(nextProps:IProps){
this.setState({
objectType : nextProps.objectType,
relationType: nextProps.relationType,
});
}
public render() {
const { displayType,objectType,relationType } = this.state;
const { leftImg,rightImg,color,list,value } = this.props;
const html : any = [] ;
const datalist : IListType[] = [] ;
// htmlbody要写在foreach循环里面,如果写全局变量最后会把全部结果push进去
const htmlbody : any = [] ;
list.forEach((item:any, num :number) => {
datalist.push({
objectaddress : item.entitySpace,
objectId : item.entityID ,
objectTime : item.entityTime,
unionaddress : item.accompanySpace,
unionId : item.accompanyID,
unionTime : item.accompanyTime,
entiyImg : item.entiyImg ,
accompanyImg : item.accompanyImg ,
})
htmlbody.push(
<div style={{display:displayType, width:'100%'}}>
{/* 左 */}
<div className="shaft-htmlbody-main" style={{ justifyContent:'flex-end'}}>
<div className="shaft-htmlbody-dif" style={{ marginRight:'10px' }}>
<div className="shaft-htmlbody-border" style={{ border:'1px solid' + color[0] }}>
<div className="shaft-htmlbody-smallTitle" style={{backgroundColor:color[0]}}>
<img className="shaft-htmlbody-img" src={leftImg} alt=""/>
<div className="shaft-htmlbody-font">
{ objectType === '01'
? <Text widths="120px" data={datalist[num].objectId.slice(0,7)}/>
: <Text widths="120px" data={datalist[num].objectId}/> }
</div>
</div>
<div className="shaft-htmlbody-dataImg">
<img style={{ width:'100%' }} src={datalist[num].entiyImg}/>
</div>
<div className="shaft-htmlbody-time">
<div style={{width:'40%'}}>过卡时间:</div>
<Text widths="60%" data={format(datalist[num].objectTime, 'HH:mm:ss')}/>
</div>
<div className="shaft-htmlbody-space">
<div style={{width:"40%"}}>卡口地点:</div>
<Text widths="60%" data={datalist[num].objectaddress}/>
</div>
</div>
</div>
</div>
{/* 中间icon */}
<div style={{width: 2, display:'flex', flexDirection:'column'}}>
<div style={{width: 2, backgroundColor: '#e6e6e6',height:30}}>{}</div>
<div style={{width: 2, height:32}}>
<IconFont extraCommonProps={ {style:{fontSize:30,color:'#b3b3b3',marginLeft:-15}} } type="iconshijian2" />
</div>
<div style={{width: 2, backgroundColor: '#e6e6e6', flex:'1'}}>{}</div>
</div>
{/* 右 */}
<div className="shaft-htmlbody-main" style={{ justifyContent:'flex-start'}}>
<div className="shaft-htmlbody-dif" style={{marginLeft:'10px'}}>
<div className="shaft-htmlbody-border" style={{ border:'1px solid'+color[1] }}>
<div className="shaft-htmlbody-smallTitle" style={{ backgroundColor:color[1] }}>
<img className="shaft-htmlbody-img" src={rightImg} alt=""/>
<div className="shaft-htmlbody-font">
{ relationType === '01'
? <Text widths="120px" data={datalist[num].unionId.slice(0,7)}/>
: <Text widths="120px" data={datalist[num].unionId}/> }
</div>
</div>
<div className="shaft-htmlbody-dataImg">
<img style={{ width:'100%' }} src={datalist[num].accompanyImg}/>
</div>
<div className="shaft-htmlbody-time">
<div style={{width:"40%"}}>过卡时间:</div>
<Text widths="60%" data={format(datalist[num].unionTime, 'HH:mm:ss')}/>
</div>
<div className="shaft-htmlbody-space">
<div style={{width:'40%'}}>卡口地点:</div>
<Text widths="60%" data={datalist[num].unionaddress}/>
</div>
</div>
</div>
</div>
</div>
)
})
// 按value值循环生成htmlbody后,再生成html模块
html.push(
<div style={{width:'100%'}}>
<div style={{width:'100%', display:'flex', justifyContent:'center'}}>
<div className="timeline-head" onClick={this.clickTitle(value)}>{value}</div>
</div>
{htmlbody}
<div style={{height:'5px'}}>{}</div>
</div>
)
return(
<div style={{flex:1,width:'100%',position:'relative',padding:'15px'}} >
{html}
</div>
)
}
private clickTitle = (value:any) => {
return () => {
if (this.state.displayType === 'flex') {
this.setState({
displayType : 'none'
})
}else{
this.setState({
displayType : 'flex'
})
}
}
}
}