- 废话少说,先上效果

菜单容器
容器主要代码
class App extends Component<appProps, appState> {constructor(props: appProps) {super(props)this.state = {position: { top: 0, left: 0 },menuVisiable: false,menuWidth: 0,menuHeight: 0,menuDatas: [{id: "A000001",value: "apple",children: []},{id: "A000003",value: "AAAAAAAAAAAAAAAAAAAA",children: [{id: "A000031",value: "BBBBBBBBBBBBBBB",children: [{id: "A000311",value: "ddddddddddddd",children: []}]},{id: "A000032",value: "CCCCCCCCCCCCCCCCCCC",children: []}]}, {id: "A000002",value: "banana",children: []}]}}contextMenuHandle = (e: React.MouseEvent<HTMLDivElement>) => {this.setState({ menuVisiable: true, position: { top: top, left: left } })}handleClick = (e: React.MouseEvent<HTMLDivElement>) => {e.preventDefault()this.setState({ menuVisiable: false })}onMenuClick = (data: menu) => {console.log(data)this.setState({ menuVisiable: false })}syncParams = (width: number, height: number) => {this.setState({ menuHeight: height, menuWidth: width })}showMenus = () => {if (!this.state.menuVisiable) {return null;}return <Content containerId='content-container' syncParams={this.syncParams} menuItams={this.state.menuDatas} position={this.state.position} onMenuClick={this.onMenuClick} />}render(): ReactNode {return (<section id="content-container" onClick={this.handleClick} onContextMenu={this.contextMenuHandle}>{this.showMenus()}</section>)}
}export default App
容器样式
#content-container {height: 80vh;width: 90vw;background-color: rgb(255, 255, 255);border-radius: 8px;
}
菜单内容
菜单主要代码
export interface menu {id: stringvalue: stringchildren: menu[]
}export interface position {top: numberleft: number
}interface contentProps {menuItams: menu[]onMenuClick: (data: menu) => voidposition: positionsyncParams: FunctioncontainerId: string
}interface contentState {selfHeight: number
}class Content extends Component<contentProps, contentState> {innerRef = createRef<HTMLDivElement>();observer: ResizeObserver | null = null;state: contentState = {selfHeight: 0}componentDidMount = () => {if (this.innerRef.current) {this.setState({ selfHeight: this.innerRef.current.clientHeight })// 实例化 ResizeObserver,回调函数会在尺寸变化时触发this.observer = new ResizeObserver(([entries]) => {// 获取最新的 contentRect 尺寸(不包含 padding 和 border)const { width, height } = entries.contentRect;this.props.syncParams(width, height)});// 开始监听绑定的 DOM 元素this.observer.observe(this.innerRef.current);}}componentWillUnmount(): void {if (this.observer) {this.observer.disconnect();}}handleClick = (data: menu): void => {this.props.onMenuClick(data)}getTop = () => {return this.props.position.top}getLeft = () => {return this.props.position.left}generateItems = (menuItams: menu[]): React.ReactNode => {let menuList = menuItams.map((el, i) => {let children: React.ReactNodeif (el.children.length > 0) {children = this.generateItems(el.children)}return <MenuItem containerId={this.props.containerId} level={i} originPosition={this.props.position} onChildClick={this.handleClick} key={el.id} item={el} children={children} />})return menuList;}render(): ReactNode {let menuList = this.generateItems(this.props.menuItams);return (<><div ref={this.innerRef} id="content-menu" style={{ left: this.getLeft() + "px", top: this.getTop() + "px" }} onContextMenu={(e) => { e.stopPropagation(); e.preventDefault() }}>{menuList}</div></>)}}interface menuItemProps {item: menuchildren: React.ReactNodelevel: numberbrother: numberoriginPosition: positioncontainerId: stringonChildClick: (data: menu) => void
}interface menuItemState {top: numberleft: numbershowChild: booleanhoverMenu: boolean
}class MenuItem extends Component<menuItemProps, menuItemState> {innerRef = createRef<HTMLDivElement>();constructor(props: menuItemProps) {super(props)this.state = {top: 0,left: 0,showChild: false,hoverMenu: false}}handleClick = (e: React.MouseEvent<HTMLDivElement>) => {e.stopPropagation()e.preventDefault()this.props.onChildClick(this.props.item)}getTop = () => {return this.state.top}getLeft = () => {return this.state.left}handleMouseOver = (e: React.MouseEvent<HTMLDivElement>) => {e.preventDefault()e.stopPropagation()this.setState({ hoverMenu: true })if (this.props.children) {this.setState({ showChild: true })}}handleMouseLeave = (e: React.MouseEvent<HTMLDivElement>) => {e.preventDefault()e.stopPropagation()this.setState({ hoverMenu: false, showChild: false })}render(): ReactNode {return (<><div key={this.props.item.id} className={this.state.hoverMenu ? "hightlight" : ""} ref={this.innerRef} id="menu-item" onClick={this.handleClick} onMouseEnter={this.handleMouseOver} onMouseLeave={() => this.setState({ hoverMenu: false, showChild: false })}><QuestionCircleOutlined /><div id="item-context">{this.props.item.value}</div>{this.props.children ? <RightOutlined /> : null}{this.state.showChild ?<div id="children-menu" style={{ left: this.getLeft() + "px", top: this.getTop() + "px" }}>{this.props.children}</div>: null}</div></>)}
}export default Content
菜单样式
#content-menu {box-shadow: 0 6px 16px 0 rgba(0,0,0,0.08), 0 3px 6px -4px rgba(0,0,0,0.12), 0 9px 28px 8px rgba(0,0,0,0.05);border-radius: 4px;position: relative;width: 200px;background-color: rgb(255, 255, 255);
}#menu-item{display: flex;justify-content: space-between;align-items: center;padding: 3px 10px;height: 26px;#item-context{padding: 0 10px;max-width: 100px;margin-right: auto;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;}
}.hightlight{color: #1677ff; cursor: pointer;
}#children-menu{color: initial; box-shadow: 0 6px 16px 0 rgba(0,0,0,0.08), 0 3px 6px -4px rgba(0,0,0,0.12), 0 9px 28px 8px rgba(0,0,0,0.05);position: absolute;width: 200px;background-color: rgb(255, 255, 255);
}
感谢交流、留言!!!
