|
| 1 | +import React, { useState, useEffect, useRef, useLayoutEffect } from 'react' |
| 2 | +import { Button } from 'antd' |
| 3 | +import styles from './index.module.less' |
| 4 | + |
| 5 | +/** |
| 6 | + * 转让历史组件 |
| 7 | + * @param {Object} props - 组件属性 |
| 8 | + * @param {number} [props.defaultDisplayCount=3] - 默认显示的子项数量 |
| 9 | + */ |
| 10 | +const TransferHistory = ({ defaultDisplayCount = 3 }) => { |
| 11 | + const [expanded, setExpanded] = useState(false) |
| 12 | + const [leftLineHeight, setLeftLineHeight] = useState('100%') |
| 13 | + const leftLineRef = useRef(null) |
| 14 | + const itemRefs = useRef([]) |
| 15 | + const contentWrapperRef = useRef(null) |
| 16 | + |
| 17 | + // 初始化引用数组 |
| 18 | + useEffect(() => { |
| 19 | + itemRefs.current = [] |
| 20 | + }, []) |
| 21 | + |
| 22 | + // 模拟数据 |
| 23 | + const transferData = [ |
| 24 | + { |
| 25 | + name: '转让历史', |
| 26 | + id: '1', |
| 27 | + level: 1, |
| 28 | + children: [ |
| 29 | + { |
| 30 | + name: '山东AI科技股份有限公司', |
| 31 | + id: '1-1', |
| 32 | + level: 2, |
| 33 | + children: [ |
| 34 | + { name: '科技股份有限公司第一公司', value: '2022年8月8号', id: '1-1-1', level: 3 }, |
| 35 | + { name: '科技股份有限公司第二公司', value: '2023年6月6号', id: '1-1-2', level: 3 }, |
| 36 | + { name: '科技股份有限公司第三公司', value: '2024年9月9号', id: '1-1-3', level: 3 }, |
| 37 | + ], |
| 38 | + ratio: '85%', |
| 39 | + }, |
| 40 | + { |
| 41 | + name: '广东发展有限公司', |
| 42 | + id: '1-2', |
| 43 | + level: 2, |
| 44 | + children: [{ name: '广东发展有限公司分公司', value: '2024年5月1号', id: '1-2-1', level: 3 }], |
| 45 | + ratio: '55%', |
| 46 | + }, |
| 47 | + { |
| 48 | + name: '科技发展股份有限公司', |
| 49 | + id: '1-3', |
| 50 | + level: 2, |
| 51 | + children: [ |
| 52 | + { name: '科技股份有限公司一', value: '2022年7月31号', id: '1-3-1', level: 3 }, |
| 53 | + { name: '科技股份有限公司二', value: '2023年8月1号', id: '1-3-2', level: 3 }, |
| 54 | + ], |
| 55 | + ratio: '35%', |
| 56 | + }, |
| 57 | + { |
| 58 | + name: '第四家公司', |
| 59 | + id: '1-4', |
| 60 | + level: 2, |
| 61 | + children: [{ name: '受让方第四分公司', value: '2021年8月20号', id: '1-4-1', level: 3 }], |
| 62 | + ratio: '25%', |
| 63 | + }, |
| 64 | + { |
| 65 | + name: '第五家公司', |
| 66 | + id: '1-5', |
| 67 | + level: 2, |
| 68 | + children: [{ name: '受让方第五分公司', value: '2020年6月6号', id: '1-5-1', level: 3 }], |
| 69 | + ratio: '15%', |
| 70 | + }, |
| 71 | + ], |
| 72 | + }, |
| 73 | + ] |
| 74 | + |
| 75 | + /** |
| 76 | + * 计算左侧线条高度(不包含按钮高度) |
| 77 | + */ |
| 78 | + const calculateLineHeight = () => { |
| 79 | + if (!transferData || !transferData[0] || !transferData[0].children || transferData[0].children.length === 0) { |
| 80 | + setLeftLineHeight('0px') |
| 81 | + return |
| 82 | + } |
| 83 | + |
| 84 | + // 根据展开状态计算显示的子项数量 |
| 85 | + const displayCount = expanded |
| 86 | + ? transferData[0].children.length |
| 87 | + : Math.min(defaultDisplayCount, transferData[0].children.length) |
| 88 | + const lastIndex = displayCount - 1 |
| 89 | + |
| 90 | + // 确保元素已渲染 |
| 91 | + if (!itemRefs.current[0] || !itemRefs.current[lastIndex] || !leftLineRef.current) { |
| 92 | + return |
| 93 | + } |
| 94 | + |
| 95 | + // 获取第一个和最后一个子项的高度 |
| 96 | + const firstItemHeight = itemRefs.current[0].offsetHeight |
| 97 | + const lastItemHeight = itemRefs.current[lastIndex].offsetHeight |
| 98 | + |
| 99 | + // 计算实际内容高度(仅包含子项容器,不包含按钮) |
| 100 | + // 使用getBoundingClientRect获取精确高度 |
| 101 | + const firstItemRect = itemRefs.current[0].getBoundingClientRect() |
| 102 | + const lastItemRect = itemRefs.current[lastIndex].getBoundingClientRect() |
| 103 | + |
| 104 | + // 计算线条高度:从第一个子项的中心到最后一个子项的中心 |
| 105 | + const lineHeight = lastItemRect.top + lastItemRect.height / 2 - (firstItemRect.top + firstItemRect.height / 2) |
| 106 | + |
| 107 | + setLeftLineHeight(`${lineHeight}px`) |
| 108 | + } |
| 109 | + |
| 110 | + /** |
| 111 | + * 切换展开/折叠状态 |
| 112 | + */ |
| 113 | + const toggleExpand = () => { |
| 114 | + setExpanded(!expanded) |
| 115 | + } |
| 116 | + |
| 117 | + // 使用useLayoutEffect确保在DOM更新后立即计算 |
| 118 | + useLayoutEffect(() => { |
| 119 | + calculateLineHeight() |
| 120 | + }, [expanded]) |
| 121 | + |
| 122 | + // 组件挂载后计算线条高度 |
| 123 | + useLayoutEffect(() => { |
| 124 | + calculateLineHeight() |
| 125 | + |
| 126 | + // 添加窗口大小变化时的重新计算 |
| 127 | + const handleResize = () => { |
| 128 | + calculateLineHeight() |
| 129 | + } |
| 130 | + |
| 131 | + window.addEventListener('resize', handleResize) |
| 132 | + return () => { |
| 133 | + window.removeEventListener('resize', handleResize) |
| 134 | + } |
| 135 | + }, [expanded]) |
| 136 | + |
| 137 | + return ( |
| 138 | + <div className={styles.transferHistoryContainer}> |
| 139 | + {transferData.map((item, index) => ( |
| 140 | + <div key={index} className={styles.historyItem}> |
| 141 | + {item.level === 1 && ( |
| 142 | + <div className={styles.levelOne}> |
| 143 | + <div className={styles.levelName}>{item.name}</div> |
| 144 | + </div> |
| 145 | + )} |
| 146 | + |
| 147 | + {/* 主体内容区域 */} |
| 148 | + <div className={styles.contentWrapper} ref={contentWrapperRef}> |
| 149 | + <div className={styles.leftContent}> |
| 150 | + <div className={styles.levelTwo}> |
| 151 | + <div className={styles.verticalConnector} style={{ height: leftLineHeight }}></div> |
| 152 | + <div className={styles.leftLine} ref={leftLineRef}> |
| 153 | + {item.children && item.children.length > 0 && ( |
| 154 | + <> |
| 155 | + {/* 控制显示的子项数量 */} |
| 156 | + {(item.children.length > defaultDisplayCount && !expanded |
| 157 | + ? item.children.slice(0, defaultDisplayCount) |
| 158 | + : item.children |
| 159 | + ).map((cItem, cIndex) => ( |
| 160 | + <div |
| 161 | + key={cIndex} |
| 162 | + className={`${styles.subItemContainer} ${styles.subItemLine}`} |
| 163 | + ref={(el) => (itemRefs.current[cIndex] = el)} |
| 164 | + > |
| 165 | + {/* 第二级菜单 */} |
| 166 | + <div className={styles.subItem}>{cItem.name}</div> |
| 167 | + |
| 168 | + {/* 第三级菜单 */} |
| 169 | + {cItem.children && cItem.children.length > 0 && ( |
| 170 | + <> |
| 171 | + {cItem.children.map((tItem, tIndex) => ( |
| 172 | + <div |
| 173 | + key={tIndex} |
| 174 | + className={ |
| 175 | + tIndex === cItem.children.length - 1 |
| 176 | + ? `${styles.tertiaryItem} ${styles.lastTertiaryItem}` |
| 177 | + : styles.tertiaryItem |
| 178 | + } |
| 179 | + > |
| 180 | + {tIndex < cItem.children.length && ( |
| 181 | + <> |
| 182 | + <div className={styles.transferInfo}> |
| 183 | + <div> |
| 184 | + 转让<span style={{ color: '#EF4A10' }}>20%</span>股权 |
| 185 | + </div> |
| 186 | + <div>受让方:{tItem.name}</div> |
| 187 | + </div> |
| 188 | + <div className={styles.infoConnector}></div> |
| 189 | + <div |
| 190 | + className={ |
| 191 | + cItem.children.length - 2 === tIndex |
| 192 | + ? styles.horizontalConnector |
| 193 | + : `${styles.horizontalConnector} ${styles.lastConnector}` |
| 194 | + } |
| 195 | + ></div> |
| 196 | + <div className={styles.transferYear}>{tItem.value}</div> |
| 197 | + </> |
| 198 | + )} |
| 199 | + </div> |
| 200 | + ))} |
| 201 | + <div className={styles.currentHolding}> |
| 202 | + <div>现持有股权:</div> |
| 203 | + <div style={{ color: '#EF4A10' }}>{cItem.ratio}</div> |
| 204 | + </div> |
| 205 | + </> |
| 206 | + )} |
| 207 | + </div> |
| 208 | + ))} |
| 209 | + </> |
| 210 | + )} |
| 211 | + </div> |
| 212 | + </div> |
| 213 | + |
| 214 | + {item.children && item.children.length > defaultDisplayCount && ( |
| 215 | + <div className={styles.expandButtonContainer}> |
| 216 | + <Button type="link" onClick={toggleExpand} className={styles.expandButton}> |
| 217 | + {expanded ? '收起部分公司' : '展开全部公司'} |
| 218 | + <i className={expanded ? 'anticon anticon-up' : 'anticon anticon-down'}></i> |
| 219 | + </Button> |
| 220 | + </div> |
| 221 | + )} |
| 222 | + </div> |
| 223 | + </div> |
| 224 | + </div> |
| 225 | + ))} |
| 226 | + </div> |
| 227 | + ) |
| 228 | +} |
| 229 | + |
| 230 | +export default TransferHistory |
0 commit comments