BlockItem.jsx源码
/root/workspace/actionview/av-github-source-code/actionview-fe/app/components/gantt/BlockItem.jsx
import React, { PropTypes, Component } from 'react';
import { OverlayTrigger, Popover, Grid, Row, Col, ControlLabel } from 'react-bootstrap';
import _ from 'lodash';
const moment = require('moment');
export default class BlockItem extends Component {
constructor(props) {
super(props);
}
static propTypes = {
cellWidth: PropTypes.number.isRequired,
blockHeight: PropTypes.number.isRequired,
origin: PropTypes.number.isRequired,
foldIssues: PropTypes.array.isRequired,
issue: PropTypes.object.isRequired,
options: PropTypes.object.isRequired,
mode: PropTypes.string.isRequired
}
//shouldComponentUpdate(newProps, newState) {
// if (newProps.cellWidth != this.props.cellWidth
// || newProps.origin != this.props.origin
// || newProps.mode != this.props.mode
// || this.props.foldIssues.indexOf(this.props.issue.id) !== -1
// || newProps.foldIssues.indexOf(this.props.issue.id) !== -1
// || this.props.issue.id == newProps.selectedIssue.id
// || this.props.issue.id == newProps.selectedIssue.parent_id) {
// return true;
// }
// return false;
//}
render() {
const {
cellWidth,
blockHeight,
origin,
foldIssues,
issue,
options:{ states=[] },
mode
} = this.props;
const stateColors = { new : '#ccc', inprogress: '#3db9d3', completed: '#3c9445' };
const popover=(
<Popover id='popover-trigger-hover' style={ { maxWidth: '350px', padding: '15px 0px' } }>
<Grid>
<Row>
<Col sm={ 4 } componentClass={ ControlLabel } style={ { textAlign: 'right' } }>主题</Col>
<Col sm={ 8 }><div style={ { textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap' } }>{ issue.no + '-' + issue.title }</div></Col>
</Row>
<Row>
<Col sm={ 4 } componentClass={ ControlLabel } style={ { textAlign: 'right' } }>开始时间</Col>
<Col sm={ 8 }>{ issue.expect_start_time ? moment.unix(issue.expect_start_time).format('YYYY/MM/DD') : <span style={ { fontStyle: 'italic', color: '#aaa' } }>未指定</span> }</Col>
</Row>
<Row>
<Col sm={ 4 } componentClass={ ControlLabel } style={ { textAlign: 'right' } }>结束时间</Col>
<Col sm={ 8 }>{ issue.expect_complete_time ? moment.unix(issue.expect_complete_time).format('YYYY/MM/DD') : <span style={ { fontStyle: 'italic', color: '#aaa' } }>未指定</span> }</Col>
</Row>
{ mode == 'progress' &&
<Row>
<Col sm={ 4 } componentClass={ ControlLabel } style={ { textAlign: 'right' } }>进度</Col>
<Col sm={ 8 }>{ issue.progress ? issue.progress + '%' : '0%' }</Col>
</Row> }
{ mode == 'status' &&
<Row>
<Col sm={ 4 } componentClass={ ControlLabel } style={ { textAlign: 'right' } }>状态</Col>
<Col sm={ 8 }>{ _.findIndex(states, { id: issue.state }) != -1 ? <span className={ 'state-' + _.find(states, { id: issue.state }).category + '-label' }>{ _.find(states, { id: issue.state }).name }</span>: '-' }</Col>
</Row> }
</Grid>
</Popover>);
const start = moment.unix(issue.expect_start_time || issue.expect_complete_time || issue.created_at).startOf('day').format('X');
const end = moment.unix(issue.expect_complete_time || issue.expect_start_time || issue.created_at).startOf('day').format('X');
const size = (end - start) / 3600 / 24 + 1;
const offset = (start - origin) / 3600 / 24;
const width = size * cellWidth - 3;
let backgroundColor = '#ccc';
if (mode == 'progress') {
if ((!issue.expect_start_time || !issue.expect_complete_time) && (!issue.progress || issue.progress < 100)) {
backgroundColor = '#555';
} else {
backgroundColor = issue.hasSubtasks ? '#65c16f' : '#3db9d3';
}
} else if (mode == 'status') {
const stateInd = _.findIndex(states, { id: issue.state });
if (stateInd !== -1) {
const category = states[stateInd].category;
if ((!issue.expect_start_time || !issue.expect_complete_time) && category !== 'completed') {
backgroundColor = '#555';
} else {
backgroundColor = stateColors[category];
}
}
}
const progressBGColor = issue.hasSubtasks ? '#3c9445' : '#2898b0';
return (
<div className='ganttview-block-container'>
<OverlayTrigger trigger={ [ 'hover', 'focus' ] } rootClose placement='top' overlay={ popover }>
{ issue.hasSubtasks && foldIssues.indexOf(issue.id) === -1 ?
<div className='ganttview-block-parent'
id={ issue.id + '-block' }
data-id={ issue.id }
style={ { width: width + 'px', marginLeft: (offset * cellWidth + 1) + 'px' } }>
<div className='ganttview-block-parent-left'/>
<div className='ganttview-block-parent-right'/>
</div>
:
<div
className={ 'ganttview-block ' + (issue.hasSubtasks ? '' : 'ganttview-block-movable') }
id={ issue.id + '-block' }
data-id={ issue.id }
style={ { width: width + 'px', height: blockHeight + 'px', marginLeft: (offset * cellWidth + 1) + 'px', backgroundColor } }>
{ mode == 'progress' &&
<div
className='ganttview-block-progress'
style={ { height: blockHeight + 'px', width: (width * _.min([ _.max([ issue.progress || 0, 0 ]), 100 ]) / 100) + 'px', backgroundColor: progressBGColor } }/> }
</div> }
</OverlayTrigger>
</div> );
}
}
源码解读
这段代码定义了一个名为 BlockItem
的 React 组件,用于渲染甘特图中的一个任务块(或称为区块)。这个组件负责根据传入的属性动态生成任务块,并根据不同的模式(进度或状态)显示不同的样式和信息。
代码分析
导入模块
import React, { PropTypes, Component } from 'react';
import { OverlayTrigger, Popover, Grid, Row, Col, ControlLabel } from 'react-bootstrap';
import _ from 'lodash';
const moment = require('moment');
-
React
:React 的核心库。 -
PropTypes
:用于定义组件的 prop 类型检查。 -
react-bootstrap
:提供了 Bootstrap 风格的 React 组件,这里使用了OverlayTrigger
,Popover
,Grid
,Row
,Col
,ControlLabel
。 -
lodash
:一个实用工具库,提供了很多常用的函数,如_.findIndex
和_.find
。 -
moment
:一个流行的 JavaScript 时间日期处理库。
定义组件类
export default class BlockItem extends Component {
constructor(props) {
super(props);
}
-
BlockItem
类继承自Component
,表示这是一个 React 组件。 -
constructor
构造函数初始化组件,并调用父类构造函数super(props)
来初始化父类属性。
定义 prop 类型
static propTypes = {
cellWidth: PropTypes.number.isRequired,
blockHeight: PropTypes.number.isRequired,
origin: PropTypes.number.isRequired,
foldIssues: PropTypes.array.isRequired,
issue: PropTypes.object.isRequired,
options: PropTypes.object.isRequired,
mode: PropTypes.string.isRequired
}
propTypes
是一个静态属性,用于定义组件接受的 prop 类型。
-
cellWidth
:每个单元格的宽度。 -
blockHeight
:任务块的高度。 -
origin
:时间起点的 Unix 时间戳。 -
foldIssues
:一个数组,包含需要折叠的任务 ID。 -
issue
:一个对象,包含任务的信息。 -
options
:一个对象,包含配置项。 -
mode
:模式,可以是'progress'
或'status'
。
shouldComponentUpdate 方法(注释掉)
//shouldComponentUpdate(newProps, newState) {
// if (newProps.cellWidth != this.props.cellWidth
// || newProps.origin != this.props.origin
// || newProps.mode != this.props.mode
// || this.props.foldIssues.indexOf(this.props.issue.id) !== -1
// || newProps.foldIssues.indexOf(this.props.issue.id) !== -1
// || this.props.issue.id == newProps.selectedIssue.id
// || this.props.issue.id == newProps.selectedIssue.parent_id) {
// return true;
// }
// return false;
//}
- 这是一个生命周期方法,用于判断组件是否需要重新渲染。
- 如果某些关键属性发生变化,则返回
true
表示需要更新。 - 如果没有变化,则返回
false
表示不需要更新。 - 注意:此方法已被注释掉,意味着默认情况下组件将遵循 React 的默认更新策略。
渲染方法
render() {
const {
cellWidth,
blockHeight,
origin,
foldIssues,
issue,
options:{ states=[] },
mode
} = this.props;
const stateColors = { new : '#ccc', inprogress: '#3db9d3', completed: '#3c9445' };
const popover=(
<Popover id='popover-trigger-hover' style={ { maxWidth: '350px', padding: '15px 0px' } }>
<Grid>
<Row>
<Col sm={ 4 } componentClass={ ControlLabel } style={ { textAlign: 'right' } }>主题</Col>
<Col sm={ 8 }><div style={ { textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap' } }>{ issue.no + '-' + issue.title }</div></Col>
</Row>
<Row>
<Col sm={ 4 } componentClass={ ControlLabel } style={ { textAlign: 'right' } }>开始时间</Col>
<Col sm={ 8 }>{ issue.expect_start_time ? moment.unix(issue.expect_start_time).format('YYYY/MM/DD') : <span style={ { fontStyle: 'italic', color: '#aaa' } }>未指定</span> }</Col>
</Row>
<Row>
<Col sm={ 4 } componentClass={ ControlLabel } style={ { textAlign: 'right' } }>结束时间</Col>
<Col sm={ 8 }>{ issue.expect_complete_time ? moment.unix(issue.expect_complete_time).format('YYYY/MM/DD') : <span style={ { fontStyle: 'italic', color: '#aaa' } }>未指定</span> }</Col>
</Row>
{ mode == 'progress' &&
<Row>
<Col sm={ 4 } componentClass={ ControlLabel } style={ { textAlign: 'right' } }>进度</Col>
<Col sm={ 8 }>{ issue.progress ? issue.progress + '%' : '0%' }</Col>
</Row> }
{ mode == 'status' &&
<Row>
<Col sm={ 4 } componentClass={ ControlLabel } style={ { textAlign: 'right' } }>状态</Col>
<Col sm={ 8 }>{ _.findIndex(states, { id: issue.state }) != -1 ? <span className={ 'state-' + _.find(states, { id: issue.state }).category + '-label' }>{ _.find(states, { id: issue.state }).name }</span>: '-' }</Col>
</Row> }
</Grid>
</Popover>);
const start = moment.unix(issue.expect_start_time || issue.expect_complete_time || issue.created_at).startOf('day').format('X');
const end = moment.unix(issue.expect_complete_time || issue.expect_start_time || issue.created_at).startOf('day').format('X');
const size = (end - start) / 3600 / 24 + 1;
const offset = (start - origin) / 3600 / 24;
const width = size * cellWidth - 3;
let backgroundColor = '#ccc';
if (mode == 'progress') {
if ((!issue.expect_start_time || !issue.expect_complete_time) && (!issue.progress || issue.progress < 100)) {
backgroundColor = '#555';
} else {
backgroundColor = issue.hasSubtasks ? '#65c16f' : '#3db9d3';
}
} else if (mode == 'status') {
const stateInd = _.findIndex(states, { id: issue.state });
if (stateInd !== -1) {
const category = states[stateInd].category;
if ((!issue.expect_start_time || !issue.expect_complete_time) && category !== 'completed') {
backgroundColor = '#555';
} else {
backgroundColor = stateColors[category];
}
}
}
const progressBGColor = issue.hasSubtasks ? '#3c9445' : '#2898b0';
return (
<div className='ganttview-block-container'>
<OverlayTrigger trigger={ [ 'hover', 'focus' ] } rootClose placement='top' overlay={ popover }>
{ issue.hasSubtasks && foldIssues.indexOf(issue.id) === -1 ?
<div className='ganttview-block-parent'
id={ issue.id + '-block' }
data-id={ issue.id }
style={ { width: width + 'px', marginLeft: (offset * cellWidth + 1) + 'px' } }>
<div className='ganttview-block-parent-left'/>
<div className='ganttview-block-parent-right'/>
</div>
:
<div
className={ 'ganttview-block ' + (issue.hasSubtasks ? '' : 'ganttview-block-movable') }
id={ issue.id + '-block' }
data-id={ issue.id }
style={ { width: width + 'px', height: blockHeight + 'px', marginLeft: (offset * cellWidth + 1) + 'px', backgroundColor } }>
{ mode == 'progress' &&
<div
className='ganttview-block-progress'
style={ { height: blockHeight + 'px', width: (width * _.min([ _.max([ issue.progress || 0, 0 ]), 100 ]) / 100) + 'px', backgroundColor: progressBGColor } }/> }
</div> }
</OverlayTrigger>
</div> );
}
}
render
方法返回组件的 JSX 结构。
- 解构赋值
const { ... } = this.props
来提取传入的 props。 - 定义状态颜色
stateColors
。 - 创建一个
Popover
用于显示任务的详细信息。 - 使用
moment
计算任务的开始时间和结束时间,并转换为 Unix 时间戳。 - 计算任务块的大小
size
和偏移量offset
。 - 计算任务块的宽度
width
。 - 根据模式
mode
设置背景颜色backgroundColor
。 - 根据模式
mode
设置进度背景颜色progressBGColor
。 - 渲染任务块:
- 如果任务有子任务并且不在折叠列表中,则渲染一个带有左右箭头的父任务块。
- 否则,渲染一个普通任务块,并根据模式显示进度条。
- 使用
OverlayTrigger
使得当鼠标悬停或聚焦时显示Popover
。
总结
这个 BlockItem
组件负责渲染甘特图中的一个任务块,并根据不同的模式(进度或状态)显示不同的样式和信息。组件还提供了一个 Popover
弹出框,用于展示任务的详细信息。通过这种方式,组件能够灵活地适应不同的数据和布局需求,为用户提供清晰的任务进度或状态视图,并且提供了交互性,增强了用户体验。