HocWaterFallBox.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import React, { useEffect, useReducer, useRef } from 'react'
  2. // import { useModel } from 'umi'
  3. interface State {
  4. childrenDivs: Element[],//实例
  5. children?: JSX.Element[],//jsxElement
  6. configs?: { w: number, h: number, t: number, l: number }[],//配置
  7. size?: number,//屏幕宽度
  8. newColumn?: number//本次展示列数
  9. }
  10. interface Action {
  11. type: 'addRef' | 'children' | 'configs' | 'size' | 'newColumn',
  12. params?: any,
  13. }
  14. function reducer(state: State, action: Action) {
  15. let { type, params } = action
  16. switch (type) {
  17. case 'addRef':
  18. let arr = state.childrenDivs
  19. arr[params.index] = params.ref//替换指定位置的实例
  20. arr = arr?.filter((item: Element) => { return item })//清空空实例
  21. return { ...state, childrenDivs: arr }
  22. case 'children':
  23. return { ...state, children: params.children }
  24. case 'configs':
  25. if (JSON.stringify(params.configs) === JSON.stringify(state.configs)) {//传入的配置和老配置一样不更新
  26. return { ...state }
  27. }
  28. return { ...state, configs: params.configs }
  29. case 'newColumn':
  30. return { ...state, newColumn: params.newColumn }
  31. case 'size':
  32. return { ...state, size: params.size }
  33. default:
  34. return { ...state }
  35. }
  36. }
  37. /**
  38. *
  39. **高阶函数处理瀑布流传入JSX.ELEMENT
  40. * @example
  41. *<HocWaterFallBox>
  42. * <div>//必要的div包裹,缺少会报错
  43. * {
  44. * getData?.data?.items?.map((item: any, index: number) => {
  45. * return <div key={item.id} className={action.id === item.id ?{...}:{...}}>//必要的div包裹,缺少会报错,不要在此div设置统一className,此处className只在配置选中需求时JS控制添加,当盒子存在className不会做样式更新处理更新注意!
  46. * <Img src='...' /> //要渲染的盒子模型
  47. * </div>
  48. * })
  49. * }
  50. * </div>
  51. *</HocWaterFallBox>
  52. * @param {number} column 一行展示多少列,column和childrenWidth二选一,设置了column自动计算子元素的宽度
  53. * @param {number} childrenWidth 默认子元素宽度300,可自行设置支持number,设置了子元素宽列就以宽度计算
  54. * @param {number} width 容器宽默认100%,可自行设置支持number
  55. * @param {number} margin 子元素左右间隔默认20px,可自行设置支持number,底部间隔默认为0
  56. * @param { JSX.Element} props JSX.Element
  57. * @return { JSX.Element} JSX.Element
  58. */
  59. function HocWaterFallBox(props: { children: JSX.Element, childrenWidth?: number, width?: number, margin?: number, column?: number }) {
  60. const { childrenWidth = 300, width, margin = 20, column } = props
  61. const [state, dispatch] = useReducer(reducer, { childrenDivs: [], children: null, configs: [] })
  62. // let { collapsed } = useModel('@@initialState', model => ({ collapsed: model?.initialState?.collapsed }))
  63. let div: { current: HTMLDivElement | null } = useRef(null)
  64. let { childrenDivs, configs, children, size, newColumn } = state
  65. //监听浏览器宽度变化
  66. useEffect(() => {
  67. window.onresize = function () {
  68. dispatch({ type: 'size', params: { size: document.body.scrollWidth } })
  69. }
  70. return () => {
  71. window.onresize = null
  72. }
  73. }, [])
  74. //计算配置
  75. useEffect(() => {
  76. let children = props?.children?.props?.children
  77. let bodyWidth: number = 0//父容器的宽
  78. let newColumn: number = column ? column : 0 //列
  79. let allW: number = 0//统一宽
  80. let childConfg: { w: number, h: number, t: number, l: number }[] = []//每个children的样式配置
  81. let allH: number[] = []//全部列的高
  82. let time: any = null//定时器
  83. function set() {
  84. if (div?.current) {//计算容器宽和列
  85. bodyWidth = width ? width : div?.current?.clientWidth//容器宽有自定义就使用自定义没有就计算当前容器的宽
  86. if (newColumn) {//假如自定义了列数执行
  87. allW = (bodyWidth - (newColumn - 1) * margin) / newColumn//计算所有children的宽
  88. } else {//否则走自定义children宽逻辑
  89. if (bodyWidth <= childrenWidth) {//假如容器宽小于等于渲染元素的宽退出操作避免报错
  90. return
  91. }
  92. newColumn = Math.floor((bodyWidth - childrenWidth) / (childrenWidth + margin)) + 1//计算列
  93. }
  94. }
  95. childrenDivs.forEach((child: Element, index: number) => {//循环拿到的实例
  96. let w = allW ? allW : child?.clientWidth || 0//children的宽,自定义列就走计算的宽,没有就计算实例的宽,初始为0
  97. let h = child?.scrollHeight || 0//children的高,从实例计算,初始为0
  98. let t = 0//children的top值初始为0
  99. let l = 0//children的left值初始为0
  100. if (index < newColumn) {//假如下标小于列的值为第一行逻辑
  101. allH.push(h)//添加第一行所有children的高
  102. l = index === 0 ? 0 : (allW ? allW : childrenWidth) * index + margin * index//计算第一行每个children的left值
  103. } else {//否则不是第一行走以下逻辑
  104. let minH: number = Math.min(...allH)//取当前最小高度的children的值
  105. let eq = allH.indexOf(minH)//取当前最小高度children的位置
  106. allH[eq] = allH[eq] + h + margin//更新列的高
  107. t = minH ? minH + margin : 0//设置当前children的top
  108. l = eq === 0 ? 0 : (allW ? allW : childrenWidth) * eq + margin * eq//设置当前children的left值
  109. }
  110. childConfg.push({ w, h, t, l })//向配置中添加本次计算
  111. })
  112. dispatch({ type: 'configs', params: { configs: childConfg } })//存放本次计算的所有children配置
  113. dispatch({ type: 'newColumn', params: { newColumn: newColumn } })//存放本次计算的列值
  114. }
  115. if (childrenDivs?.length === children?.length) {//假如获取到实例的个数和本次传入的jsxElement个数相等表示已经全部取得实例可以进行计算
  116. time = setTimeout(() => { //设置定时器,避免渲染过慢导致获取相应的数据不正确
  117. set()
  118. }, 200)
  119. }
  120. return () => {//每次卸载清除定时器以免内存泄漏
  121. time = null
  122. clearTimeout(time)
  123. }
  124. }, [childrenDivs, div, childrenWidth, width, column, margin, size])//此处依赖值变化需要更新计算
  125. /**处理结果 */
  126. useEffect(() => {
  127. let childrens: JSX.Element[] = props?.children?.props?.children || []
  128. let allH: number[] = []//全部列的高
  129. let newChild: { [name: string]: string }[] = []
  130. let oldChild: { [name: string]: string }[] = []
  131. childrens?.map((child: any) => {
  132. newChild.push({ key: child?.key, style: child?.props?.style })
  133. })
  134. state?.children?.props?.children?.map((child: any) => {
  135. oldChild.push({ key: child?.key.replace(/(\/.*)|(")/ig, ''), style: child?.props?.style })
  136. })
  137. if (JSON.stringify(newChild) === JSON.stringify(oldChild)) {
  138. return
  139. }
  140. if (!props?.children?.props?.children) {//当没拿到全部数据不处理
  141. return
  142. }
  143. let configss = configs
  144. if (configss.every((item: { w: number, h: number, t: number, l: number }) => item.l === 0) && configss.length > 0) {//配置没有处理完不处理
  145. return
  146. }
  147. configss?.forEach((conf: { h: number }, index: number) => {//计算全部列的高
  148. allH[index % newColumn] = allH.length === newColumn ? allH[index % newColumn] + conf.h + margin : conf.h + margin
  149. })
  150. let children = React.cloneElement(//创建新的JSX.ELEMENT
  151. props.children,
  152. {
  153. children: React.Children.map(childrens, (child: JSX.Element, index: number) => {
  154. return React.cloneElement(
  155. child,
  156. {
  157. style: { width: configss[index]?.w || childrenWidth, position: 'absolute', left: configss[index]?.l, top: configss[index]?.t, },
  158. ref: (ref: HTMLDivElement) => { dispatch({ type: 'addRef', params: { index, ref } }) },
  159. key: JSON.stringify(child.key)
  160. },
  161. )
  162. }),
  163. ref: div,
  164. style: { width: width || '100%', position: 'relative', height: allH.length > 0 ? Math.max(...allH) : '100%' },
  165. },
  166. )
  167. dispatch({ type: 'children', params: { children } })
  168. }, [props.children, configs, newColumn])
  169. return children
  170. }
  171. export default HocWaterFallBox