import React, { useEffect, useReducer, useRef } from 'react' // import { useModel } from 'umi' interface State { childrenDivs: Element[],//实例 children?: JSX.Element[],//jsxElement configs?: { w: number, h: number, t: number, l: number }[],//配置 size?: number,//屏幕宽度 newColumn?: number//本次展示列数 } interface Action { type: 'addRef' | 'children' | 'configs' | 'size' | 'newColumn', params?: any, } function reducer(state: State, action: Action) { let { type, params } = action switch (type) { case 'addRef': let arr = state.childrenDivs arr[params.index] = params.ref//替换指定位置的实例 arr = arr?.filter((item: Element) => { return item })//清空空实例 return { ...state, childrenDivs: arr } case 'children': return { ...state, children: params.children } case 'configs': if (JSON.stringify(params.configs) === JSON.stringify(state.configs)) {//传入的配置和老配置一样不更新 return { ...state } } return { ...state, configs: params.configs } case 'newColumn': return { ...state, newColumn: params.newColumn } case 'size': return { ...state, size: params.size } default: return { ...state } } } /** * **高阶函数处理瀑布流传入JSX.ELEMENT * @example * *
//必要的div包裹,缺少会报错 * { * getData?.data?.items?.map((item: any, index: number) => { * return
//必要的div包裹,缺少会报错,不要在此div设置统一className,此处className只在配置选中需求时JS控制添加,当盒子存在className不会做样式更新处理更新注意! * //要渲染的盒子模型 *
* }) * } *
*
* @param {number} column 一行展示多少列,column和childrenWidth二选一,设置了column自动计算子元素的宽度 * @param {number} childrenWidth 默认子元素宽度300,可自行设置支持number,设置了子元素宽列就以宽度计算 * @param {number} width 容器宽默认100%,可自行设置支持number * @param {number} margin 子元素左右间隔默认20px,可自行设置支持number,底部间隔默认为0 * @param { JSX.Element} props JSX.Element * @return { JSX.Element} JSX.Element */ function HocWaterFallBox(props: { children: JSX.Element, childrenWidth?: number, width?: number, margin?: number, column?: number }) { const { childrenWidth = 300, width, margin = 20, column } = props const [state, dispatch] = useReducer(reducer, { childrenDivs: [], children: null, configs: [] }) // let { collapsed } = useModel('@@initialState', model => ({ collapsed: model?.initialState?.collapsed })) let div: { current: HTMLDivElement | null } = useRef(null) let { childrenDivs, configs, children, size, newColumn } = state //监听浏览器宽度变化 useEffect(() => { window.onresize = function () { dispatch({ type: 'size', params: { size: document.body.scrollWidth } }) } return () => { window.onresize = null } }, []) //计算配置 useEffect(() => { let children = props?.children?.props?.children let bodyWidth: number = 0//父容器的宽 let newColumn: number = column ? column : 0 //列 let allW: number = 0//统一宽 let childConfg: { w: number, h: number, t: number, l: number }[] = []//每个children的样式配置 let allH: number[] = []//全部列的高 let time: any = null//定时器 function set() { if (div?.current) {//计算容器宽和列 bodyWidth = width ? width : div?.current?.clientWidth//容器宽有自定义就使用自定义没有就计算当前容器的宽 if (newColumn) {//假如自定义了列数执行 allW = (bodyWidth - (newColumn - 1) * margin) / newColumn//计算所有children的宽 } else {//否则走自定义children宽逻辑 if (bodyWidth <= childrenWidth) {//假如容器宽小于等于渲染元素的宽退出操作避免报错 return } newColumn = Math.floor((bodyWidth - childrenWidth) / (childrenWidth + margin)) + 1//计算列 } } childrenDivs.forEach((child: Element, index: number) => {//循环拿到的实例 let w = allW ? allW : child?.clientWidth || 0//children的宽,自定义列就走计算的宽,没有就计算实例的宽,初始为0 let h = child?.scrollHeight || 0//children的高,从实例计算,初始为0 let t = 0//children的top值初始为0 let l = 0//children的left值初始为0 if (index < newColumn) {//假如下标小于列的值为第一行逻辑 allH.push(h)//添加第一行所有children的高 l = index === 0 ? 0 : (allW ? allW : childrenWidth) * index + margin * index//计算第一行每个children的left值 } else {//否则不是第一行走以下逻辑 let minH: number = Math.min(...allH)//取当前最小高度的children的值 let eq = allH.indexOf(minH)//取当前最小高度children的位置 allH[eq] = allH[eq] + h + margin//更新列的高 t = minH ? minH + margin : 0//设置当前children的top l = eq === 0 ? 0 : (allW ? allW : childrenWidth) * eq + margin * eq//设置当前children的left值 } childConfg.push({ w, h, t, l })//向配置中添加本次计算 }) dispatch({ type: 'configs', params: { configs: childConfg } })//存放本次计算的所有children配置 dispatch({ type: 'newColumn', params: { newColumn: newColumn } })//存放本次计算的列值 } if (childrenDivs?.length === children?.length) {//假如获取到实例的个数和本次传入的jsxElement个数相等表示已经全部取得实例可以进行计算 time = setTimeout(() => { //设置定时器,避免渲染过慢导致获取相应的数据不正确 set() }, 200) } return () => {//每次卸载清除定时器以免内存泄漏 time = null clearTimeout(time) } }, [childrenDivs, div, childrenWidth, width, column, margin, size])//此处依赖值变化需要更新计算 /**处理结果 */ useEffect(() => { let childrens: JSX.Element[] = props?.children?.props?.children || [] let allH: number[] = []//全部列的高 let newChild: { [name: string]: string }[] = [] let oldChild: { [name: string]: string }[] = [] childrens?.map((child: any) => { newChild.push({ key: child?.key, style: child?.props?.style }) }) state?.children?.props?.children?.map((child: any) => { oldChild.push({ key: child?.key.replace(/(\/.*)|(")/ig, ''), style: child?.props?.style }) }) if (JSON.stringify(newChild) === JSON.stringify(oldChild)) { return } if (!props?.children?.props?.children) {//当没拿到全部数据不处理 return } let configss = configs if (configss.every((item: { w: number, h: number, t: number, l: number }) => item.l === 0) && configss.length > 0) {//配置没有处理完不处理 return } configss?.forEach((conf: { h: number }, index: number) => {//计算全部列的高 allH[index % newColumn] = allH.length === newColumn ? allH[index % newColumn] + conf.h + margin : conf.h + margin }) let children = React.cloneElement(//创建新的JSX.ELEMENT props.children, { children: React.Children.map(childrens, (child: JSX.Element, index: number) => { return React.cloneElement( child, { style: { width: configss[index]?.w || childrenWidth, position: 'absolute', left: configss[index]?.l, top: configss[index]?.t, }, ref: (ref: HTMLDivElement) => { dispatch({ type: 'addRef', params: { index, ref } }) }, key: JSON.stringify(child.key) }, ) }), ref: div, style: { width: width || '100%', position: 'relative', height: allH.length > 0 ? Math.max(...allH) : '100%' }, }, ) dispatch({ type: 'children', params: { children } }) }, [props.children, configs, newColumn]) return children } export default HocWaterFallBox