BlockItem.jsx源码

/root/workspace/actionview/av-github-source-code/actionview-fe/app/components/gantt/BlockItem.jsx

actionview react前端 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 弹出框,用于展示任务的详细信息。通过这种方式,组件能够灵活地适应不同的数据和布局需求,为用户提供清晰的任务进度或状态视图,并且提供了交互性,增强了用户体验。