You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							469 lines
						
					
					
						
							11 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							469 lines
						
					
					
						
							11 KiB
						
					
					
				
								import React, {
							 | 
						|
								  useEffect, useState, useRef, useMemo, forwardRef,
							 | 
						|
								} from 'react';
							 | 
						|
								
							 | 
						|
								import PropTypes from 'prop-types';
							 | 
						|
								
							 | 
						|
								import classnames from 'classnames';
							 | 
						|
								
							 | 
						|
								import { deepMerge } from '@jiaminghi/charts/lib/util/index';
							 | 
						|
								
							 | 
						|
								import { deepClone } from '@jiaminghi/c-render/lib/plugin/util';
							 | 
						|
								
							 | 
						|
								import { useAutoResize, co } from '@jiaminghi/data-view-react';
							 | 
						|
								
							 | 
						|
								import './style.less';
							 | 
						|
								
							 | 
						|
								const defaultConfig = {
							 | 
						|
								  /**
							 | 
						|
								   * @description Board header
							 | 
						|
								   * @type {Array<String>}
							 | 
						|
								   * @default header = []
							 | 
						|
								   * @example header = ['column1', 'column2', 'column3']
							 | 
						|
								   */
							 | 
						|
								  header: [],
							 | 
						|
								  /**
							 | 
						|
								   * @description Board data
							 | 
						|
								   * @type {Array<Array>}
							 | 
						|
								   * @default data = []
							 | 
						|
								   */
							 | 
						|
								  data: [],
							 | 
						|
								  /**
							 | 
						|
								   * @description Row num
							 | 
						|
								   * @type {Number}
							 | 
						|
								   * @default rowNum = 5
							 | 
						|
								   */
							 | 
						|
								  rowNum: 5,
							 | 
						|
								  /**
							 | 
						|
								   * @description Header background color
							 | 
						|
								   * @type {String}
							 | 
						|
								   * @default headerBGC = '#00BAFF'
							 | 
						|
								   */
							 | 
						|
								  headerBGC: '#00BAFF',
							 | 
						|
								  /**
							 | 
						|
								   * @description Odd row background color
							 | 
						|
								   * @type {String}
							 | 
						|
								   * @default oddRowBGC = '#003B51'
							 | 
						|
								   */
							 | 
						|
								  oddRowBGC: '#003B51',
							 | 
						|
								  /**
							 | 
						|
								   * @description Even row background color
							 | 
						|
								   * @type {String}
							 | 
						|
								   * @default evenRowBGC = '#003B51'
							 | 
						|
								   */
							 | 
						|
								  evenRowBGC: '#0A2732',
							 | 
						|
								  /**
							 | 
						|
								   * @description Scroll wait time
							 | 
						|
								   * @type {Number}
							 | 
						|
								   * @default waitTime = 2000
							 | 
						|
								   */
							 | 
						|
								  waitTime: 2000,
							 | 
						|
								  /**
							 | 
						|
								   * @description Header height
							 | 
						|
								   * @type {Number}
							 | 
						|
								   * @default headerHeight = 35
							 | 
						|
								   */
							 | 
						|
								  headerHeight: 35,
							 | 
						|
								  /**
							 | 
						|
								   * @description Column width
							 | 
						|
								   * @type {Array<Number>}
							 | 
						|
								   * @default columnWidth = []
							 | 
						|
								   */
							 | 
						|
								  columnWidth: [],
							 | 
						|
								  /**
							 | 
						|
								   * @description Column align
							 | 
						|
								   * @type {Array<String>}
							 | 
						|
								   * @default align = []
							 | 
						|
								   * @example align = ['left', 'center', 'right']
							 | 
						|
								   */
							 | 
						|
								  align: [],
							 | 
						|
								  /**
							 | 
						|
								   * @description Show index
							 | 
						|
								   * @type {Boolean}
							 | 
						|
								   * @default index = false
							 | 
						|
								   */
							 | 
						|
								  index: false,
							 | 
						|
								  /**
							 | 
						|
								   * @description index Header
							 | 
						|
								   * @type {String}
							 | 
						|
								   * @default indexHeader = '#'
							 | 
						|
								   */
							 | 
						|
								  indexHeader: '#',
							 | 
						|
								  /**
							 | 
						|
								   * @description Carousel type
							 | 
						|
								   * @type {String}
							 | 
						|
								   * @default carousel = 'single'
							 | 
						|
								   * @example carousel = 'single' | 'page'
							 | 
						|
								   */
							 | 
						|
								  carousel: 'single',
							 | 
						|
								  /**
							 | 
						|
								   * @description Pause scroll when mouse hovered
							 | 
						|
								   * @type {Boolean}
							 | 
						|
								   * @default hoverPause = true
							 | 
						|
								   * @example hoverPause = true | false
							 | 
						|
								   */
							 | 
						|
								  hoverPause: true,
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								function calcHeaderData({ header, index, indexHeader }) {
							 | 
						|
								  if (!header.length) {
							 | 
						|
								    return [];
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  header = [...header];
							 | 
						|
								
							 | 
						|
								  if (index) header.unshift(indexHeader);
							 | 
						|
								
							 | 
						|
								  return header;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								function calcRows({
							 | 
						|
								  data, index, headerBGC, rowNum,
							 | 
						|
								}) {
							 | 
						|
								  if (index) {
							 | 
						|
								    data = data.map((row, i) => {
							 | 
						|
								      row = [...row];
							 | 
						|
								
							 | 
						|
								      const indexTag = `<span class="index" style="background-color: ${headerBGC};">${i
							 | 
						|
								        + 1}</span>`;
							 | 
						|
								
							 | 
						|
								      row.unshift(indexTag);
							 | 
						|
								
							 | 
						|
								      return row;
							 | 
						|
								    });
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  data = data.map((ceils, i) => ({ ceils, rowIndex: i }));
							 | 
						|
								
							 | 
						|
								  const rowLength = data.length;
							 | 
						|
								
							 | 
						|
								  if (rowLength > rowNum && rowLength < 2 * rowNum) {
							 | 
						|
								    data = [...data, ...data];
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  return data.map((d, i) => ({ ...d, scroll: i }));
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								function calcAligns(mergedConfig, header) {
							 | 
						|
								  const columnNum = header.length;
							 | 
						|
								
							 | 
						|
								  const aligns = new Array(columnNum).fill('left');
							 | 
						|
								
							 | 
						|
								  const { align } = mergedConfig;
							 | 
						|
								
							 | 
						|
								  return deepMerge(aligns, align);
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								const ScrollBoard = forwardRef(({
							 | 
						|
								  onClick, config = {}, className, style, onMouseOver,
							 | 
						|
								}, ref) => {
							 | 
						|
								  const { width, height, domRef } = useAutoResize(ref);
							 | 
						|
								
							 | 
						|
								  const [state, setState] = useState({
							 | 
						|
								    mergedConfig: null,
							 | 
						|
								
							 | 
						|
								    header: [],
							 | 
						|
								
							 | 
						|
								    rows: [],
							 | 
						|
								
							 | 
						|
								    rowsShow: [],
							 | 
						|
								
							 | 
						|
								    widths: [],
							 | 
						|
								
							 | 
						|
								    heights: [],
							 | 
						|
								
							 | 
						|
								    aligns: [],
							 | 
						|
								  });
							 | 
						|
								
							 | 
						|
								  const {
							 | 
						|
								    mergedConfig, header, rows, widths, heights, aligns, rowsShow,
							 | 
						|
								  } = state;
							 | 
						|
								
							 | 
						|
								  const stateRef = useRef({
							 | 
						|
								    ...state,
							 | 
						|
								    rowsData: [],
							 | 
						|
								    avgHeight: 0,
							 | 
						|
								    animationIndex: 0,
							 | 
						|
								  });
							 | 
						|
								
							 | 
						|
								  Object.assign(stateRef.current, state);
							 | 
						|
								
							 | 
						|
								  function onResize() {
							 | 
						|
								    if (!mergedConfig) return;
							 | 
						|
								
							 | 
						|
								    const widths = calcWidths(mergedConfig, stateRef.current.rowsData);
							 | 
						|
								
							 | 
						|
								    const heights = calcHeights(mergedConfig, header);
							 | 
						|
								
							 | 
						|
								    const data = { widths, heights };
							 | 
						|
								
							 | 
						|
								    Object.assign(stateRef.current, data);
							 | 
						|
								    setState((state) => ({ ...state, ...data }));
							 | 
						|
								  }
							 | 
						|
								  const [init, setInit] = useState(true);
							 | 
						|
								
							 | 
						|
								  function calcData() {
							 | 
						|
								    // const mergedConfig = deepMerge(
							 | 
						|
								    //   deepClone(defaultConfig, true),
							 | 
						|
								    //   config || {},
							 | 
						|
								    // );
							 | 
						|
								    const mergedConfig = {
							 | 
						|
								      ...defaultConfig,
							 | 
						|
								      ...config,
							 | 
						|
								    };
							 | 
						|
								
							 | 
						|
								    const header = calcHeaderData(mergedConfig);
							 | 
						|
								
							 | 
						|
								    const rows = calcRows(mergedConfig);
							 | 
						|
								
							 | 
						|
								    const widths = calcWidths(mergedConfig, stateRef.current.rowsData);
							 | 
						|
								
							 | 
						|
								    const heights = calcHeights(mergedConfig, header);
							 | 
						|
								
							 | 
						|
								    const aligns = calcAligns(mergedConfig, header);
							 | 
						|
								
							 | 
						|
								    const data = {
							 | 
						|
								      mergedConfig,
							 | 
						|
								      header,
							 | 
						|
								      rows,
							 | 
						|
								      widths,
							 | 
						|
								      aligns,
							 | 
						|
								      heights: init ? heights : state.heights.concat(heights),
							 | 
						|
								      rowsShow: init ? rows : state.rowsShow,
							 | 
						|
								    };
							 | 
						|
								    setInit(false);
							 | 
						|
								    Object.assign(stateRef.current, data, {
							 | 
						|
								      rowsData: rows,
							 | 
						|
								      animationIndex: stateRef.current.animationIndex,
							 | 
						|
								    });
							 | 
						|
								
							 | 
						|
								    setState((state) => ({ ...state, ...data }));
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  function calcWidths({ columnWidth, header }, rowsData) {
							 | 
						|
								    const usedWidth = columnWidth.reduce((all, w) => all + w, 0);
							 | 
						|
								
							 | 
						|
								    let columnNum = 0;
							 | 
						|
								    if (rowsData[0]) {
							 | 
						|
								      columnNum = rowsData[0].ceils.length;
							 | 
						|
								    } else if (header.length) {
							 | 
						|
								      columnNum = header.length;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    const avgWidth = (width - usedWidth) / (columnNum - columnWidth.length);
							 | 
						|
								
							 | 
						|
								    const widths = new Array(columnNum).fill(avgWidth);
							 | 
						|
								
							 | 
						|
								    return deepMerge(widths, columnWidth);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  function calcHeights({ headerHeight, rowNum, data }, header) {
							 | 
						|
								    let allHeight = height;
							 | 
						|
								
							 | 
						|
								    if (header.length) allHeight -= headerHeight;
							 | 
						|
								
							 | 
						|
								    const avgHeight = allHeight / rowNum;
							 | 
						|
								
							 | 
						|
								    Object.assign(stateRef.current, { avgHeight });
							 | 
						|
								
							 | 
						|
								    return new Array(data.length).fill(avgHeight);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  function* animation(start = false) {
							 | 
						|
								    let {
							 | 
						|
								      avgHeight,
							 | 
						|
								      animationIndex,
							 | 
						|
								      mergedConfig: { waitTime, carousel, rowNum },
							 | 
						|
								      rowsData,
							 | 
						|
								    } = stateRef.current;
							 | 
						|
								
							 | 
						|
								    const rowLength = rowsData.length;
							 | 
						|
								
							 | 
						|
								    if (start) yield new Promise((resolve) => setTimeout(resolve, waitTime));
							 | 
						|
								
							 | 
						|
								    const animationNum = carousel === 'single' ? 1 : rowNum;
							 | 
						|
								
							 | 
						|
								    let rows = rowsData.slice(animationIndex);
							 | 
						|
								    rows.push(...rowsData.slice(0, animationIndex));
							 | 
						|
								    rows = rows.slice(0, carousel === 'page' ? rowNum * 2 : rowNum + 1);
							 | 
						|
								
							 | 
						|
								    const heights = new Array(rowLength).fill(avgHeight);
							 | 
						|
								    setState((state) => ({
							 | 
						|
								      ...state, rows, heights, rowsShow: rows,
							 | 
						|
								    }));
							 | 
						|
								
							 | 
						|
								    yield new Promise((resolve) => setTimeout(resolve, 300));
							 | 
						|
								
							 | 
						|
								    animationIndex += animationNum;
							 | 
						|
								
							 | 
						|
								    const back = animationIndex - rowLength;
							 | 
						|
								    if (back >= 0) animationIndex = back;
							 | 
						|
								
							 | 
						|
								    const newHeights = [...heights];
							 | 
						|
								    newHeights.splice(0, animationNum, ...new Array(animationNum).fill(0));
							 | 
						|
								
							 | 
						|
								    Object.assign(stateRef.current, { animationIndex });
							 | 
						|
								    setState((state) => ({ ...state, heights: newHeights }));
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  function emitEvent(handle, ri, ci, row, ceil) {
							 | 
						|
								    const { ceils, rowIndex } = row;
							 | 
						|
								
							 | 
						|
								    handle && handle({
							 | 
						|
								      row: ceils, ceil, rowIndex, columnIndex: ci,
							 | 
						|
								    });
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  function handleHover(enter, ri, ci, row, ceil) {
							 | 
						|
								    if (enter) emitEvent(onMouseOver, ri, ci, row, ceil);
							 | 
						|
								
							 | 
						|
								    if (!mergedConfig.hoverPause) return;
							 | 
						|
								
							 | 
						|
								    const { pause, resume } = task.current;
							 | 
						|
								
							 | 
						|
								    enter && pause && resume ? pause() : resume && resume();
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  // updateRows(rows, animationIndex) {
							 | 
						|
								  //   const { mergedConfig, animationHandler, animation } = this
							 | 
						|
								  //   this.mergedConfig = {
							 | 
						|
								  //     ...mergedConfig,
							 | 
						|
								  //     data: [...rows]
							 | 
						|
								  //   }
							 | 
						|
								  //   this.needCalc = true
							 | 
						|
								  //   if (typeof animationIndex === 'number') this.animationIndex = animationIndex
							 | 
						|
								  //   if (!animationHandler) animation(true)
							 | 
						|
								  // }
							 | 
						|
								
							 | 
						|
								  const getBackgroundColor = (rowIndex) => mergedConfig[rowIndex % 2 === 0 ? 'evenRowBGC' : 'oddRowBGC'];
							 | 
						|
								
							 | 
						|
								  const task = useRef({});
							 | 
						|
								
							 | 
						|
								  useEffect(() => {
							 | 
						|
								    calcData();
							 | 
						|
								
							 | 
						|
								    let start = true;
							 | 
						|
								
							 | 
						|
								    function* loop() {
							 | 
						|
								      while (true) {
							 | 
						|
								        yield* animation(start);
							 | 
						|
								
							 | 
						|
								        start = false;
							 | 
						|
								
							 | 
						|
								        const { waitTime } = stateRef.current.mergedConfig;
							 | 
						|
								
							 | 
						|
								        yield new Promise((resolve) => setTimeout(resolve, waitTime - 300));
							 | 
						|
								      }
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    const {
							 | 
						|
								      mergedConfig: { rowNum },
							 | 
						|
								      rows: rowsData,
							 | 
						|
								    } = stateRef.current;
							 | 
						|
								
							 | 
						|
								    const rowLength = rowsData.length;
							 | 
						|
								
							 | 
						|
								    if (rowNum >= rowLength) {
							 | 
						|
								      setState((prestate) => ({
							 | 
						|
								        ...prestate, rowsShow: state.rows,
							 | 
						|
								      }));
							 | 
						|
								      return;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    task.current = co(loop);
							 | 
						|
								
							 | 
						|
								    return task.current.end;
							 | 
						|
								  }, [config, domRef.current]);
							 | 
						|
								
							 | 
						|
								  useEffect(onResize, [width, height, domRef.current]);
							 | 
						|
								
							 | 
						|
								  const classNames = useMemo(() => classnames('dv-scroll-board', className), [
							 | 
						|
								    className,
							 | 
						|
								  ]);
							 | 
						|
								
							 | 
						|
								  return (
							 | 
						|
								    <div className={classNames} style={style} ref={domRef}>
							 | 
						|
								      {!!header.length && !!mergedConfig && (
							 | 
						|
								        <div
							 | 
						|
								          className="header"
							 | 
						|
								          style={{ backgroundColor: `${mergedConfig.headerBGC}` }}
							 | 
						|
								        >
							 | 
						|
								          {header.map((headerItem, i) => (
							 | 
						|
								            <div
							 | 
						|
								              className="header-item"
							 | 
						|
								              key={`${headerItem}-${i}`}
							 | 
						|
								              style={{
							 | 
						|
								                height: `${mergedConfig.headerHeight}px`,
							 | 
						|
								                lineHeight: `${mergedConfig.headerHeight}px`,
							 | 
						|
								                width: `${widths[i]}px`,
							 | 
						|
								              }}
							 | 
						|
								              align={aligns[i]}
							 | 
						|
								              dangerouslySetInnerHTML={{ __html: headerItem }}
							 | 
						|
								            />
							 | 
						|
								          ))}
							 | 
						|
								        </div>
							 | 
						|
								      )}
							 | 
						|
								
							 | 
						|
								      {!!mergedConfig && (
							 | 
						|
								        <div
							 | 
						|
								          className="rows"
							 | 
						|
								          style={{
							 | 
						|
								            height: `${height
							 | 
						|
								              - (header.length ? mergedConfig.headerHeight : 0)}px`,
							 | 
						|
								          }}
							 | 
						|
								        >
							 | 
						|
								          {rowsShow.map((row, ri) => (
							 | 
						|
								            <div
							 | 
						|
								              className="row-item"
							 | 
						|
								              key={`${row.toString()}-${row.scroll}`}
							 | 
						|
								              style={{
							 | 
						|
								                height: `${heights[ri]}px`,
							 | 
						|
								                lineHeight: `${heights[ri]}px`,
							 | 
						|
								                backgroundColor: `${getBackgroundColor(row.rowIndex)}`,
							 | 
						|
								              }}
							 | 
						|
								            >
							 | 
						|
								              {row.ceils.map((ceil, ci) => {
							 | 
						|
								                if (typeof (ceil) === 'string') {
							 | 
						|
								                  return (
							 | 
						|
								                    <div
							 | 
						|
								                      className="ceil"
							 | 
						|
								                      key={`${ceil}-${ri}-${ci}`}
							 | 
						|
								                      style={{ width: `${widths[ci]}px` }}
							 | 
						|
								                      align={aligns[ci]}
							 | 
						|
								                      dangerouslySetInnerHTML={{ __html: ceil }}
							 | 
						|
								                      onClick={() => emitEvent(onClick, ri, ci, row, ceil)}
							 | 
						|
								                      onMouseEnter={() => handleHover(true, ri, ci, row, ceil)}
							 | 
						|
								                      onMouseLeave={() => handleHover(false)}
							 | 
						|
								                    />
							 | 
						|
								                  );
							 | 
						|
								                }
							 | 
						|
								                return (
							 | 
						|
								                  <div
							 | 
						|
								                    className="ceil"
							 | 
						|
								                    style={{ width: `${widths[ci]}px` }}
							 | 
						|
								                    align={aligns[ci]}
							 | 
						|
								                    key={`${ri}-${ci}`}
							 | 
						|
								                    onMouseEnter={() => handleHover(true, ri, ci, row, ceil)}
							 | 
						|
								                    onMouseLeave={() => handleHover(false)}
							 | 
						|
								                  >
							 | 
						|
								                    {ceil}
							 | 
						|
								                  </div>
							 | 
						|
								                );
							 | 
						|
								              })}
							 | 
						|
								            </div>
							 | 
						|
								          ))}
							 | 
						|
								        </div>
							 | 
						|
								      )}
							 | 
						|
								    </div>
							 | 
						|
								  );
							 | 
						|
								});
							 | 
						|
								
							 | 
						|
								ScrollBoard.propTypes = {
							 | 
						|
								  config: PropTypes.object,
							 | 
						|
								  onClick: PropTypes.func,
							 | 
						|
								  onMouseOver: PropTypes.func,
							 | 
						|
								  className: PropTypes.string,
							 | 
						|
								  style: PropTypes.object,
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								export default ScrollBoard;
							 | 
						|
								
							 |