handan-screen/src/page/Component/ScrollBoard/index.tsx
2024-09-09 15:22:48 +08:00

441 lines
11 KiB
TypeScript

import React, { useEffect, useState, useRef, useMemo, forwardRef } from "react";
import useAutoResize from "./use/autoResize";
import { deepMerge } from "./util";
import { deepClone } from "./util/utils";
import { co } from "./util";
import classnames from "classnames";
import "./index.css";
import { current } from "@reduxjs/toolkit";
interface ScrollBoardProps {
config?: object;
onClick?: () => void;
onMouseOver?: () => void;
className?: string;
style?: object;
}
interface TaskType {
end: () => void;
pause: () => void;
resume: () => void;
}
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 }: any) {
if (!header.length) {
return [];
}
header = [...header];
if (index) header.unshift(indexHeader);
return header;
}
function calcRows({ data, index, headerBGC, rowNum }: any) {
if (index) {
data = data.map((row: any, i: any) => {
row = [...row];
const indexTag = `<span class="index" style="background-color: ${headerBGC};">${
i + 1
}</span>`;
row.unshift(indexTag);
return row;
});
}
data = data.map((ceils: any, i: any) => ({ ceils, rowIndex: i }));
const rowLength = data.length;
if (rowLength > rowNum && rowLength < 2 * rowNum) {
data = [...data, ...data];
}
return data.map((d: any, i: any) => ({ ...d, scroll: i }));
}
function calcAligns(mergedConfig: any, header: any) {
const columnNum = header.length;
let aligns = new Array(columnNum).fill("left");
const { align } = mergedConfig;
return deepMerge(aligns, align);
}
const ScrollBoard = forwardRef(
(
{ config, onClick, onMouseOver, className, style }: ScrollBoardProps,
ref
) => {
const { width, height, domRef } = useAutoResize(ref);
const [state, setState] = useState({
mergedConfig: {
align: [],
carousel: "",
columnWidth: [],
data: [],
evenRowBGC: "",
header: [],
headerBGC: "",
headerHeight: 35,
hoverPause: true,
index: false,
oddRowBGC: "",
indexHeader: "#",
rowNum: 5,
waitTime: 2000,
},
header: [],
rows: [],
widths: [],
heights: [],
aligns: [],
});
const { mergedConfig, header, rows, widths, heights, aligns } = 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: any = { widths, heights };
Object.assign(stateRef.current, data);
setState((state) => ({ ...state, ...data }));
}
function calcData() {
const mergedConfig = deepMerge(
// deepClone(defaultConfig, true),
deepClone(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: any = {
mergedConfig,
header,
rows,
widths,
aligns,
heights,
};
Object.assign(stateRef.current, data, {
rowsData: rows,
animationIndex: 0,
});
setState((state) => ({ ...state, ...data }));
}
function calcWidths({ columnWidth, header }: any, rowsData: any) {
const usedWidth = columnWidth.reduce((all: any, w: any) => 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 }: any, header: any) {
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
): Generator<Promise<void>, void, unknown> {
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: any = rowsData.slice(animationIndex);
rows.push(...rowsData.slice(0, animationIndex));
rows = rows.slice(0, carousel === "page" ? rowNum * 2 : rowNum + 1);
const heights: any = new Array(rowLength).fill(avgHeight);
setState((state) => ({ ...state, rows, heights }));
yield new Promise((resolve) => setTimeout(resolve, 300));
animationIndex += animationNum;
const back = animationIndex - rowLength;
if (back >= 0) animationIndex = back;
const newHeights: any = [...heights];
newHeights.splice(0, animationNum, ...new Array(animationNum).fill(0));
Object.assign(stateRef.current, { animationIndex });
setState((state) => ({ ...state, heights: newHeights }));
}
function emitEvent(handle: any, ri: any, ci: any, row: any, ceil: any) {
const { ceils, rowIndex } = row;
handle && handle({ row: ceils, ceil, rowIndex, columnIndex: ci });
}
function handleHover(
enter: any,
ri?: any,
ci?: any,
row?: any,
ceil?: any
) {
if (enter) emitEvent(onMouseOver, ri, ci, row, ceil);
if (!mergedConfig.hoverPause) return;
if (task.current) {
const { pause, resume } = task.current;
enter
? (function () {
if (pause) pause();
})()
: (function () {
if (resume) resume();
})();
}
}
const getBackgroundColor = (rowIndex: any) =>
mergedConfig[rowIndex % 2 === 0 ? "evenRowBGC" : "oddRowBGC"];
const task = useRef<TaskType>(null);
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) return;
// @ts-ignore
task.current = co(loop);
if (task.current) {
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`,
}}
// @ts-ignore
align={aligns[i]}
dangerouslySetInnerHTML={{ __html: headerItem }}
/>
))}
</div>
)}
{!!mergedConfig && (
<div
className="rows"
style={{
height: `${
height - (header.length ? mergedConfig.headerHeight : 0)
}px`,
}}
>
{rows.map((row: any, 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: any, ci: any) => (
<div
className="ceil"
key={`${ceil}-${ri}-${ci}`}
style={{ width: `${widths[ci]}px` }}
// @ts-ignore
align={aligns[ci]}
dangerouslySetInnerHTML={{ __html: ceil }}
onClick={() => emitEvent(onClick, ri, ci, row, ceil)}
onMouseEnter={() => handleHover(true, ri, ci, row, ceil)}
onMouseLeave={() => handleHover(false)}
/>
))}
</div>
))}
</div>
)}
</div>
);
}
);
export default ScrollBoard;