React实现座位排布组件

最近在开发一个影院系统的后台管理系统,该后台可以设置一个影厅的布局。

后台使用的是react框架,一位大神学长在几天之内就把这个控件研究出来了,并进行了较为严密的封装,佩服不已,虽然不是我写的,但着实有必要学习和记录一下。

以下是全部代码:

const MAX_COLUMN = 50;
const DEFAULT_COLUMN = 25;

const MAX_ROW = 50;
const DEFAULT_ROW = 12;

const SEAT_WIDTH = 15; // 每一个小座位的宽度
const SEAT_MARGIN = 2; // 每一个小座位的边距

const SEAT_BOUNDS_MARGIN_LEFT = 10; // seat_bounds_margin_left

// 头部信息(提示信息)

// 根据传入的座位的state属性,展示该座位图片的颜色(已选:灰色,可选:白色,故障:红色,无座位:白色空白)
const getSeatImg = seat => {
  switch (seat.state) {
    case SEAT_STATE.IDLE:
      return WHITE_SEAT;
    case SEAT_STATE.SELECTED:
      return GREEN_SEAT;
    case SEAT_STATE.LOCKED:
      return RED_SEAT;
    default:
      return '';
  }
};

// 格式化返回座位信息,例如:seat_1_2
const getSeatKey = seat => {
  return `seat_${seat.xAxis}_${seat.yAxis}`;
};

// 每一个小座位的样式
const seatStyle = {
  width: `${SEAT_WIDTH}px`,
  height: `${SEAT_WIDTH}px`,
  margin: `${SEAT_MARGIN}px`,
  cursor: 'pointer',
};

// 坐标轴样式
const axisStyle = {
  width: `${SEAT_WIDTH + SEAT_MARGIN * 2}px`,
  height: `${SEAT_WIDTH + SEAT_MARGIN * 2}px`,
  backgroundColor: 'grey',
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'center',
};

// 设置座位样式,需要传入onClick方法
const SeatIcon = ({ seat, onClick, editMode }) => {
  if (seat.state === SEAT_STATE.NULL) {
    return (
      <div
        style={{
          ...seatStyle,
          backgroundColor: editMode ? '#dcdcdc' : '#ffffff',
        }}
        onClick={onClick}
      />
    );
  }
  return <img " onClick={onClick} />;
};

class EditCinemaSeats extends React.Component {
  constructor(props) {
    super(props);
    // colum: 35,row:15
    const { column, row, seats } = props;
    console.log(`constructor, seats,`, seats);
    this.handlerSeatColumnRowChanged({ column, row, seats }, true);
  }

  componentWillReceiveProps(nextProps) {
    if (
      (isEmpty(this.props.seats) && !isEmpty(nextProps.seats)) ||
      this.props.row !== nextProps.row ||
      this.props.column !== nextProps.column
    ) {
      this.handlerSeatColumnRowChanged(nextProps);
    }
  }

  notifySeatsChange = () => {
    const { row, column, seats } = this.state;
    // 从整个布局中筛选除可选的座位,拼接成列表到selectedSeats中
    const selectedSeats = chain(seats)
      .filter(it => it.state === SEAT_STATE.IDLE)
      .value();
    this.props.onSeatsChange({
      row,
      column,
      selectedSeats,
    });
  };

  // 单击控制该座位可选还是不存在(针对影厅排布局的时候使用)
  handlerSeatClick = seat => {
    const { seats } = this.state;
    seats.forEach(item => {
      if (getSeatKey(item) === getSeatKey(seat)) {
        if (item.state !== SEAT_STATE.IDLE) {
          item.state = SEAT_STATE.IDLE;
        } else {
          item.state = SEAT_STATE.NULL;
        }
      }
    });
    this.setState({ seats }, this.notifySeatsChange);
  };

  handlerSeatColumnRowChanged = ({ row, column, seats }, initial) => {
    console.log("+++++++++++");
    console.log(seats);
    const seatMap = reduce(
      seats,
      (obj, seat) => {
        obj[getSeatKey(seat)] = seat;
        return obj;
      },
      {}
    );
    const newList = createSeats(0, column, 0, row);
    newList.forEach(it => {
      const pre = seatMap[getSeatKey(it)];
      if (!isNil(pre) && pre.state === SEAT_STATE.IDLE) {
        it.state = SEAT_STATE.IDLE;
      }
    });
    if (initial) {
      this.state = {
        row,
        column,
        seats: newList,
      };
      this.notifySeatsChange();
    } else {
      this.setState(
        {
          row,
          column,
          seats: newList,
        },
        this.notifySeatsChange
      );
    }
  };

  renderEditHeader = () => {
    const { column, row, editMode } = this.props;

    return (
      <div className={styles.cinema_seats_header_1}>
        <img src={WHITE_SEAT} alt="" />
        <div 5px' }}>已选</div>
        <InputNumber
          15px' }}
          min={1}
          disabled={!editMode}
          max={MAX_COLUMN}
          defaultValue={column}
          formatter={value => `y:${value}`}
          parser={value => value.replace('y:', '')}
          value={this.state.column}
          onChange={value => {
            this.handlerSeatColumnRowChanged({
              ...this.state,
              column: value,
            });
          }}
        />
        <InputNumber
          15px' }}
          min={1}
          disabled={!editMode}
          max={MAX_ROW}
          defaultValue={row}
          formatter={value => `x:${value}`}
          parser={value => value.replace('x:', '')}
          value={this.state.row}
          onChange={value => {
            this.handlerSeatColumnRowChanged({
              ...this.state,
              row: value,
            });
          }}
        />
      </div>
    );
  };

  renderXAxis = () => {
    const { column } = this.state;
    const children = [];
    for (let i = 0; i < column; i++) {
      children.push(
        <div key={`xAxis_${i}`} style={axisStyle}>
          <span #ffffff', fontSize: '8px' }}>{i + 1}</span>
        </div>
      );
    }
    return (
      <div
        style={{
          display: 'flex',
          flexDirection: 'row',
          marginLeft: SEAT_BOUNDS_MARGIN_LEFT + SEAT_WIDTH,
          marginTop: '10px',
        }}
      >
        {children}
      </div>
    );
  };

  renderYAxis = () => {
    const { row } = this.state;
    const children = [];
    for (let i = 0; i < row; i++) {
      children.push(
        <div key={`yAxis_${i}`} style={axisStyle}>
          <span #ffffff', fontSize: '8px' }}>{i + 1}</span>
        </div>
      );
    }
    return (
      <div
        style={{
          display: 'flex',
          flexDirection: 'column',
          marginTop: '10px',
        }}
      >
        {children}
      </div>
    );
  };

  // 显示整个排座的列表
  renderSeatsBounds = () => {
    const { editMode } = this.props;
    const { row, column } = this.state;
    const boxStyle = {
      width: `${(SEAT_WIDTH + SEAT_MARGIN * 2) * column}px`,
      height: `${(SEAT_WIDTH + SEAT_MARGIN * 2) * row}px`,
      marginTop: '10px',
    };
    const { seats } = this.state;
    return (
      <div style={boxStyle} className={styles.cinema_seats_bounds}>
        {seats.map(item => (
          <SeatIcon
            key={getSeatKey(item)}
            seat={item}
            editMode={editMode}
            onClick={() => {
              if (editMode) {
                this.handlerSeatClick(item);
              }
            }}
          />
        ))}
      </div>
    );
  };

  render() {
    return (
      <div className={styles.cinema_seats_container}>
        {this.renderEditHeader()}
        {this.renderXAxis()}
        <div
          style={{
            display: 'flex',
            flexDirection: 'row',
          }}
        >
          {this.renderYAxis()}
          <div
            className={styles.cinema_seats_container_box}
            style={{
              marginLeft: SEAT_BOUNDS_MARGIN_LEFT,
            }}
          >
            {this.renderSeatsBounds()}
          </div>
        </div>
      </div>
    );
  }
}

EditCinemaSeats.propTypes = {
  seats: PropTypes.array,
  column: PropTypes.number,
  row: PropTypes.number,
  onSeatsChange: PropTypes.func,
  editMode: PropTypes.bool,
};

EditCinemaSeats.defaultProps = {
  seats: createDefaultSeat(), // 初始值为中间一块为可选状态的区域
  row: DEFAULT_ROW,
  column: DEFAULT_COLUMN,
  onSeatsChange: noop,
  editMode: true,
};

// 创建默认座位
function createDefaultSeat() {
  const seats = createSeats(1, 24, 0, 11);
  seats.forEach(it => {
    it.state = SEAT_STATE.IDLE;
  });
  // const res = chain(seats).filter((item) => item.yAxis !== 3).value()
  return seats;
}

// 先设置矩形座位区域,初始值都为null,即:不存在座位
function createSeats(fromX, toX, fromY, toY) {
  const seats = [];
  for (let y = fromY; y < toY; y++) {
    for (let x = fromX; x < toX; x++) {
      seats.push({
        xAxis: x,
        yAxis: y,
        state: SEAT_STATE.NULL,
      });
    }
  }
  return seats;
}

const SEAT_STATE = {
  NULL: 0, // 空,没位置
  IDLE: 1, //可选
  SELECTED: 2, // 已选
  LOCKED: 3, //坏了
};

export default EditCinemaSeats;

除了图标资源以外,基本上所有的代码都在这了,注意代码中用到了lodash库中的一些方法,比如chain和reduce等,由于是大神编写的,所以在理解上我都花了很大功夫,不过这样的代码才有助于成长,特此记录一下。