diff --git a/packages/layout/src/components/Help/AsyncContentPanel.tsx b/packages/layout/src/components/Help/AsyncContentPanel.tsx new file mode 100644 index 000000000000..a73d2e279f9b --- /dev/null +++ b/packages/layout/src/components/Help/AsyncContentPanel.tsx @@ -0,0 +1,58 @@ +import { Spin } from 'antd'; +import { useContext, useState, useEffect } from 'react'; +import { ProHelpDataSource, ProHelpProvide, ProHelpDataSourceChildren } from './HelpProvide'; +import { RenderContentPanel } from './RenderContentPanel'; + +/** + * 异步加载内容的面板组件 + * @param item 指向当前面板的 ProHelpDataSource + */ +export const AsyncContentPanel: React.FC<{ + item: ProHelpDataSource['children'][number]; + onInit?: (ref: HTMLDivElement) => void; +}> = ({ item, onInit }) => { + const { onLoadContext } = useContext(ProHelpProvide); // 获取上下文中的 onLoadContext + const [loading, setLoading] = useState(false); // 加载状态 + const [content, setContent] = useState[]>(); // 内容数据 + + useEffect(() => { + if (!item.key) return; // 如果没有key则返回 + setLoading(true); // 开始加载 + onLoadContext?.(item.key, item).then((res) => { + // 调用加载方法 + setLoading(false); // 加载完成 + setContent(res); // 设置内容数据 + }); + }, [item.key]); + + // 如果没有key,则返回null + if (!item.key) return null; + + // 如果正在加载并且有key,则显示加载中的状态 + if (loading && item.key) { + return ( +
+ +
+ ); + } + + // 加载完成后,渲染内容面板 + return ( + { + onInit?.(ref); + }} + dataSourceChildren={content!} + /> + ); +}; diff --git a/packages/layout/src/components/Help/HelpProvide.tsx b/packages/layout/src/components/Help/HelpProvide.tsx index 88c8f5c1360f..f9d2630e581c 100644 --- a/packages/layout/src/components/Help/HelpProvide.tsx +++ b/packages/layout/src/components/Help/HelpProvide.tsx @@ -31,15 +31,25 @@ type ProHelpDataSourceContentType = { children: string; } & AnchorHTMLAttributes; /** - * inlineLink 链接类型的数据源子项内容。 + * 行内链接类型的数据源子项内容。 */ inlineLink: { children: string; } & AnchorHTMLAttributes; + + /** + * navigation 类型链接,或切换菜单 + */ + navigationSwitch: { + selectKey: string; + children: string; + }; + /** * text 文本类型的数据源子项内容。 */ text: string; + /** * image 图片类型的数据源子项内容。 */ diff --git a/packages/layout/src/components/Help/ProHelpContentPanel.tsx b/packages/layout/src/components/Help/ProHelpContentPanel.tsx new file mode 100644 index 000000000000..aa2bd83502a9 --- /dev/null +++ b/packages/layout/src/components/Help/ProHelpContentPanel.tsx @@ -0,0 +1,150 @@ +import { useEffect, useRef } from 'react'; +import React, { useContext, useMemo } from 'react'; +import type { ProHelpDataSource } from './HelpProvide'; +import { ProHelpProvide } from './HelpProvide'; +import { useDebounceFn } from '@ant-design/pro-utils'; +import { RenderContentPanel } from './RenderContentPanel'; +import { AsyncContentPanel } from './AsyncContentPanel'; + +export type ProHelpContentPanelProps = { + /** + * 控制当前选中的帮助文档 + */ + selectedKey: React.Key; + className?: string; + parentItem?: ProHelpDataSource; + + onScroll?: (key?: string) => void; +}; + +/** + * 控制具体的帮助文档显示组件 + * selectedKey 来展示对应的内容。它会根据不同的item.valueType值来展示不同的内容,包括标题、图片、超链接等。 + * @param ProHelpContentPanelProps + * @returns + */ +export const ProHelpContentPanel: React.FC = ({ + className, + parentItem, + selectedKey, + onScroll, +}) => { + const { dataSource } = useContext(ProHelpProvide); + + // 记录每个面板的滚动高度 + const scrollHeightMap = useRef>(new Map()); + + const divRef = useRef(null); + + useEffect(() => { + if (!selectedKey || !parentItem?.infiniteScrollFull) return; + const div = scrollHeightMap.current.get(selectedKey); + if (div?.offsetTop && divRef.current) { + if (Math.abs(divRef.current!.scrollTop - div?.offsetTop + 40) > div?.clientHeight) { + divRef.current!.scrollTop = div?.offsetTop - 40; + } + } + }, [selectedKey]); + + /** + * debounce(防抖)处理滚动事件,并根据滚动位置来实现找到当前列表的 key + */ + const onScrollEvent = useDebounceFn(async (e: Event) => { + const dom = e?.target as HTMLDivElement; + // 根据滚动位置来找到当前列表的 key + const list = Array.from(scrollHeightMap.current.entries()).find(([, value]) => { + if (dom?.scrollTop < value.offsetTop) { + return true; + } + return false; + }); + + if (!list) { + return; + } + // 如果获取的 key 和当前 key 不同丢弃掉 + if (list.at(0) !== selectedKey) { + onScroll?.(list.at(0) as string | undefined); + } + }, 200); + + /** + * 当 parentItem 组件中的 infiniteScrollFull 属性变化时 + * 如果该属性为真值,则开始监听滚动事件; + * 如果为假值,则停止监听滚动事件并取消防抖处理。 + * 在监听滚动事件时,可以实现分页(瀑布流)效果。同时,该代码还会根据 selectedKey 的变化来触发跳转 + */ + useEffect(() => { + if (!parentItem?.infiniteScrollFull) return; + onScrollEvent.cancel(); + divRef.current?.addEventListener('scroll', onScrollEvent.run, false); + return () => { + onScrollEvent.cancel(); + divRef.current?.removeEventListener('scroll', onScrollEvent.run, false); + }; + }, [parentItem?.infiniteScrollFull, selectedKey]); + + /** + * 生成一个 Map 能根据 key 找到所有的 index + */ + const dataSourceMap = useMemo(() => { + const map = new Map< + React.Key, + ProHelpDataSource['children'][number] & { + parentKey?: React.Key; + } + >(); + dataSource.forEach((page) => { + page.children.forEach((item) => { + map.set(item.key || item.title, { ...item, parentKey: page.key }); + }); + }); + return map; + }, [dataSource]); + + const renderItem = (item: ProHelpDataSource['children'][number]) => { + if (item?.asyncLoad) { + return ( +
+ { + if (!scrollHeightMap.current) return; + scrollHeightMap.current.set(item.key, ref); + }} + /> +
+ ); + } + + return ( +
+ { + if (!scrollHeightMap.current) return; + scrollHeightMap.current.set(item.key, ref); + }} + dataSourceChildren={item?.children || []} + /> +
+ ); + }; + + if (parentItem && parentItem.infiniteScrollFull) { + return ( +
+ {parentItem.children?.map((item) => { + return {renderItem(item)}; + }) || null} +
+ ); + } + return renderItem(dataSourceMap.get(selectedKey!)!); +}; diff --git a/packages/layout/src/components/Help/ProHelpDrawer.tsx b/packages/layout/src/components/Help/ProHelpDrawer.tsx new file mode 100644 index 000000000000..851045f5953a --- /dev/null +++ b/packages/layout/src/components/Help/ProHelpDrawer.tsx @@ -0,0 +1,39 @@ +import { Drawer, DrawerProps } from 'antd'; +import useMergedState from 'rc-util/es/hooks/useMergedState'; +import { ProHelpPanelProps, ProHelpPanel } from './ProHelpPanel'; + +export type ProHelpDrawerProps = { + /** + * Ant Design Drawer 组件的 Props,可以传递一些选项,如位置、大小、关闭方式等等。 + */ + drawerProps: DrawerProps; +} & Omit; + +/** + * 渲染一个抽屉,其中显示了一个 ProHelpPanel。 + * @param drawerProps 要传递给 Drawer 组件的属性。 + * @param props 要传递给 ProHelpPanel 组件的属性。 + */ +export const ProHelpDrawer: React.FC = ({ drawerProps, ...props }) => { + const [drawerOpen, setDrawerOpen] = useMergedState(false, { + value: drawerProps.open, + onChange: drawerProps.afterOpenChange, + }); + return ( + setDrawerOpen(false)} + afterOpenChange={(open) => { + setDrawerOpen(open); + }} + > + setDrawerOpen(false)} bordered={false} /> + + ); +}; diff --git a/packages/layout/src/components/Help/ProHelpModal.tsx b/packages/layout/src/components/Help/ProHelpModal.tsx new file mode 100644 index 000000000000..546bfcdaffd6 --- /dev/null +++ b/packages/layout/src/components/Help/ProHelpModal.tsx @@ -0,0 +1,40 @@ +import { ModalProps, Modal } from 'antd'; +import useMergedState from 'rc-util/es/hooks/useMergedState'; +import { ProHelpPanelProps, ProHelpPanel } from './ProHelpPanel'; +export type ProHelpModalProps = { + /** + * Ant Design Modal 组件的 props,可以传递一些选项,如位置、大小、关闭方式等等。 + */ + modalProps?: ModalProps; +} & Omit; + +/** + * 渲染一个模态对话框,其中显示了一个 ProHelpPanel。 + * @param modalProps 要传递给 Modal 组件的属性。 + * @param props 要传递给 ProHelpPanel 组件的属性。 + */ +export const ProHelpModal: React.FC = ({ modalProps, ...props }) => { + const [modalOpen, setModalOpen] = useMergedState(false, { + value: modalProps?.open, + onChange: modalProps?.afterClose, + }); + return ( + { + setModalOpen(false); + }} + bodyStyle={{ + margin: -24, + }} + centered + closable={false} + footer={null} + width={720} + open={modalOpen} + maskClosable + {...modalProps} + > + setModalOpen(false)} /> + + ); +}; diff --git a/packages/layout/src/components/Help/ProHelpPanel.tsx b/packages/layout/src/components/Help/ProHelpPanel.tsx new file mode 100644 index 000000000000..72bdf9a16ed3 --- /dev/null +++ b/packages/layout/src/components/Help/ProHelpPanel.tsx @@ -0,0 +1,252 @@ +import { ProProvider } from '@ant-design/pro-provider'; +import { Card, ConfigProvider, Menu } from 'antd'; +import useMergedState from 'rc-util/es/hooks/useMergedState'; +import { useContext, useState, useMemo } from 'react'; +import { ProHelpProvide, ProHelpDataSource } from './HelpProvide'; +import { ProHelpContentPanel } from './ProHelpContentPanel'; +import { ProHelpSelect } from './Search'; +import { useStyle } from './style'; +import { CloseOutlined, ProfileOutlined } from '@ant-design/icons'; +import React from 'react'; + +export const SelectKeyProvide = React.createContext<{ + selectedKey: string | undefined; + setSelectedKey: (key: string | undefined) => void; +}>({ + selectedKey: undefined, + setSelectedKey: () => {}, +}); + +export type ProHelpPanelProps = { + /** + * 帮助面板首次打开时的默认选中文档的键名 + */ + defaultSelectedKey?: string; + /** + * 当前选中的帮助文档的键名。如果提供了这个 prop,那么该组件将是一个受控组件,其状态将由父组件管理。如果未提供,那么该组件将是一个非受控组件,其状态将在组件内部管理。 + */ + selectedKey?: string; + /** + * 当选中的文档键名发生变化时调用的回调函数。新的键名将作为参数传递给该函数。 + */ + onSelectedKeyChange?: (key: string | undefined) => void; + /** + *控制左侧面板是否能够打开 + */ + showLeftPanel?: boolean; + /** + * 当左侧面板打开状态发生变化时调用的回调函数。新的打开状态将作为参数传递给该函数。 + */ + onShowLeftPanelChange?: (show: boolean) => void; + /** + * 是否显示边框 + */ + bordered?: boolean; + + /** + * 当帮助面板关闭时调用的回调函数。 + */ + onClose?: () => void; + + /** + * 帮助面板的高度,可以是数字或字符串类型。 + */ + height?: number | string; + + /** + * 帮助面板的页脚 + */ + footer?: React.ReactNode; + + /** + * 在一页内加载所有的 children 内容 + */ + infiniteScrollFull?: boolean; +}; +/** + * ProHelpPanel 组件是一个帮助中心面板组件,具有可折叠的左侧菜单和右侧帮助内容区域。 + * 左侧菜单显示了帮助文档的目录结构,右侧帮助内容区域显示了用户选中的帮助文档内容。 + * 在左侧菜单中,用户可以通过点击目录来选择并显示相应的文档内容。 + * @param param0 + * @returns + */ +export const ProHelpPanel: React.FC = ({ + bordered = true, + onClose, + footer, + height, + ...props +}) => { + const { getPrefixCls } = useContext(ConfigProvider.ConfigContext); + const className = getPrefixCls('pro-help'); + const { wrapSSR } = useStyle(className); + const { dataSource } = useContext(ProHelpProvide); + const [selectedKey, setSelectedKey] = useMergedState(undefined, { + defaultValue: props.defaultSelectedKey, + value: props.selectedKey, + onChange: props.onSelectedKeyChange, + }); + const [openKey, setOpenKey] = useState(''); + const { token } = useContext(ProProvider); + const [showLeftPanel, setShowLeftPanel] = useMergedState(true, { + value: props.showLeftPanel, + onChange: props.onShowLeftPanelChange, + }); + + const dataSourceKeyMap = useMemo(() => { + const map = new Map< + React.Key, + ProHelpDataSource & { + parentKey?: React.Key; + } + >(); + dataSource.forEach((page) => { + map.set(page.key, page); + page.children?.forEach((item) => { + map.set(item.key || item.title, { + parentKey: page.key, + ...item, + } as unknown as ProHelpDataSource); + }); + }); + return map; + }, [dataSource]); + + const parentKey = useMemo( + () => dataSourceKeyMap.get(selectedKey!)?.parentKey as string, + [dataSourceKeyMap, selectedKey], + ); + + return wrapSSR( + + +
+ { + setShowLeftPanel(!showLeftPanel); + }} + /> +
+ { + setSelectedKey(value); + setOpenKey((item as any)?.dataItemKey); + }} + /> + + {onClose ? ( +
+ { + onClose?.(); + }} + /> +
+ ) : null} + + } + > + {showLeftPanel ? ( +
+ + { + setOpenKey(keys.at(-1) || ''); + }} + selectedKeys={selectedKey ? [selectedKey] : []} + onSelect={({ selectedKeys }) => { + setSelectedKey(selectedKeys.at(-1) || ''); + }} + mode="inline" + items={dataSource.map((item) => { + return { + key: item.key, + label: item.title, + children: item.children.map((child) => { + return { + key: child.key, + label: child.title, + }; + }), + }; + })} + /> + +
+ ) : null} +
+ {selectedKey ? ( + setSelectedKey(key)} + /> + ) : null} + {footer ?
{footer}
: null} +
+
+
, + ); +}; diff --git a/packages/layout/src/components/Help/ProHelpPopover.tsx b/packages/layout/src/components/Help/ProHelpPopover.tsx new file mode 100644 index 000000000000..b19a1bfef56b --- /dev/null +++ b/packages/layout/src/components/Help/ProHelpPopover.tsx @@ -0,0 +1,63 @@ +import classNames from 'classnames'; +import type { PopoverProps } from 'antd'; +import { Popover, ConfigProvider } from 'antd'; +import React, { useContext } from 'react'; +import { useStyle } from './style'; +import { ProHelpContentPanel } from './ProHelpContentPanel'; + +export type ProHelpPopoverProps = Omit & { + /** + * 悬浮提示文字的 CSS 类名 + */ + textClassName?: string; + + /** + * Popover 内容的 content 的 CSS 类名 + */ + popoverContextClassName?: string; + + /** + * 悬浮提示文字的 CSS 样式对象 + */ + textStyle?: React.CSSProperties; + + /** + * 当前选中的帮助文档的 key 值 + */ + selectedKey: string; + + /** + * 可选的悬浮提示 Popover 组件的 Props,用于自定义悬浮提示的样式和行为。 + * 该属性可以传递 Ant Design Popover 组件的 props,如位置、大小、触发方式等等 + * @see 注意,content 属性已经被从 PopoverProps 中删除,因为这个属性由 ProHelpPopover 内部控制。 + */ + popoverProps?: PopoverProps; +}; + +/** + * 渲染一个弹出式提示框,其中显示了一个ProHelpContentPanel,展示帮助文案的详情 + * @param popoverProps 要传递给 Drawer 组件的属性。 + * @param props 要传递给 ProHelpPanel 组件的属性。 + */ +export const ProHelpPopover: React.FC = (props) => { + const { getPrefixCls } = useContext(ConfigProvider.ConfigContext); + const className = getPrefixCls('pro-help'); + const { wrapSSR } = useStyle(className); + return wrapSSR( + + + + } + {...props.popoverProps} + > + + {props.children} + + , + ); +}; diff --git a/packages/layout/src/components/Help/RenderContentPanel.tsx b/packages/layout/src/components/Help/RenderContentPanel.tsx new file mode 100644 index 000000000000..85275d2e85f4 --- /dev/null +++ b/packages/layout/src/components/Help/RenderContentPanel.tsx @@ -0,0 +1,151 @@ +import type { ImageProps } from 'antd'; +import { Image, Typography } from 'antd'; +import type { AnchorHTMLAttributes } from 'react'; +import { useEffect, useRef } from 'react'; +import React, { useContext } from 'react'; +import type { ProHelpDataSourceChildren } from './HelpProvide'; +import { ProHelpProvide } from './HelpProvide'; +import { SelectKeyProvide } from './ProHelpPanel'; + +// HTML渲染组件,接收一个字符串形式的html作为props +// 可选接收className作为组件的样式类名 +const HTMLRender: React.FC<{ + children: string; + className?: string; +}> = (props) => { + const ref = useRef(null); + + // 当html发生变化时,将其渲染到ref.current的innerHTML中 + useEffect(() => { + if (ref.current) ref.current.innerHTML = props.children; + }, [props.children]); + // 返回一个div元素作为容器,并传递ref和className作为props + return
; +}; + +const NavigationSwitch: React.FC<{ + children: string; + selectKey: string; +}> = (props) => { + const context = useContext(SelectKeyProvide); + return ( + + { + context.setSelectedKey(props.selectKey); + }} + > + {props.children} + + + ); +}; + +export const RenderContentPanel: React.FC<{ + dataSourceChildren: ProHelpDataSourceChildren[]; + onInit?: (ref: HTMLDivElement) => void; +}> = ({ dataSourceChildren, onInit }) => { + const { valueTypeMap } = useContext(ProHelpProvide); + const divRef = useRef(null); + + useEffect(() => { + onInit?.(divRef.current!); + }, [dataSourceChildren]); + + /** + * itemRender 的定义 + * @param {ProHelpDataSourceChildren} item + * @param {number} index + * @return {*} + */ + const itemRender = (item: ProHelpDataSourceChildren, index: number) => { + // 自定义的渲染,优先级最高 + if (valueTypeMap.has(item.valueType)) { + return ( + + {valueTypeMap.get(item.valueType)?.(item, index)} + + ); + } + if (item.valueType === 'html') { + return ( + + ); + } + + if (item.valueType === 'h1') { + return ( + + {item.children as string} + + ); + } + + if (item.valueType === 'h2') { + return ( + + {item.children as string} + + ); + } + if (item.valueType === 'image') { + return ( +
+ +
+ ); + } + if (item.valueType === 'inlineLink') { + return ( + + )} /> + + ); + } + if (item.valueType === 'link') { + return ( + + ); + } + if (item.valueType === 'navigationSwitch') { + return ( + + ); + } + return {item.children as string}; + }; + + return
{dataSourceChildren?.map(itemRender)}
; +}; diff --git a/packages/layout/src/components/Help/index.tsx b/packages/layout/src/components/Help/index.tsx index 8c12d241c2e6..11d2df6fe1ee 100644 --- a/packages/layout/src/components/Help/index.tsx +++ b/packages/layout/src/components/Help/index.tsx @@ -1,69 +1,18 @@ -import { CloseOutlined, ProfileOutlined } from '@ant-design/icons'; -import { ProProvider } from '@ant-design/pro-provider'; -import classNames from 'classnames'; -import type { ImageProps, PopoverProps, ModalProps, DrawerProps } from 'antd'; -import { Spin } from 'antd'; -import { Popover, Menu, Image, Typography, Card, ConfigProvider, Drawer, Modal } from 'antd'; -import type { AnchorHTMLAttributes } from 'react'; -import { useEffect, useRef } from 'react'; -import React, { useContext, useMemo, useState } from 'react'; -import useMergedState from 'rc-util/lib/hooks/useMergedState'; +import React from 'react'; import type { ProHelpDataSource, ProHelpDataSourceChildren } from './HelpProvide'; import { ProHelpProvide } from './HelpProvide'; -import { useStyle } from './style'; import { ProHelpSelect } from './Search'; -import { useDebounceFn } from '@ant-design/pro-utils'; + +export * from './ProHelpContentPanel'; +export * from './ProHelpDrawer'; +export * from './ProHelpModal'; +export * from './ProHelpPopover'; +export * from './RenderContentPanel'; +export * from './ProHelpPanel'; export type { ProHelpDataSource, ProHelpDataSourceChildren }; export { ProHelpProvide, ProHelpSelect }; -export type ProHelpPanelProps = { - /** - * 帮助面板首次打开时的默认选中文档的键名 - */ - defaultSelectedKey?: string; - /** - * 当前选中的帮助文档的键名。如果提供了这个 prop,那么该组件将是一个受控组件,其状态将由父组件管理。如果未提供,那么该组件将是一个非受控组件,其状态将在组件内部管理。 - */ - selectedKey?: string; - /** - * 当选中的文档键名发生变化时调用的回调函数。新的键名将作为参数传递给该函数。 - */ - onSelectedKeyChange?: (key: string | undefined) => void; - /** - *控制左侧面板是否能够打开 - */ - showLeftPanel?: boolean; - /** - * 当左侧面板打开状态发生变化时调用的回调函数。新的打开状态将作为参数传递给该函数。 - */ - onShowLeftPanelChange?: (show: boolean) => void; - /** - * 是否显示边框 - */ - bordered?: boolean; - - /** - * 当帮助面板关闭时调用的回调函数。 - */ - onClose?: () => void; - - /** - * 帮助面板的高度,可以是数字或字符串类型。 - */ - height?: number | string; - - /** - * 帮助面板的页脚 - */ - footer?: React.ReactNode; - - /** - * 在一页内加载所有的 children 内容 - */ - infiniteScrollFull?: boolean; -}; - export type ProHelpProps = { /** * 帮助文档的数据源,包含一组帮助文档数据,每个数据包含标题和内容等信息。 @@ -108,487 +57,6 @@ export type ProHelpContentPanelProps = { onScroll?: (key?: string) => void; }; -// HTML渲染组件,接收一个字符串形式的html作为props -// 可选接收className作为组件的样式类名 -const HTMLRender: React.FC<{ - children: string; - className?: string; -}> = (props) => { - const ref = useRef(null); - - // 当html发生变化时,将其渲染到ref.current的innerHTML中 - useEffect(() => { - if (ref.current) ref.current.innerHTML = props.children; - }, [props.children]); - // 返回一个div元素作为容器,并传递ref和className作为props - return
; -}; - -const RenderContentPanel: React.FC<{ - dataSourceChildren: ProHelpDataSourceChildren[]; - onInit?: (ref: HTMLDivElement) => void; -}> = ({ dataSourceChildren, onInit }) => { - const { valueTypeMap } = useContext(ProHelpProvide); - const divRef = useRef(null); - - useEffect(() => { - onInit?.(divRef.current!); - }, [dataSourceChildren]); - - /** - * itemRender 的定义 - * @param {ProHelpDataSourceChildren} item - * @param {number} index - * @return {*} - */ - const itemRender = (item: ProHelpDataSourceChildren, index: number) => { - // 自定义的渲染,优先级最高 - if (valueTypeMap.has(item.valueType)) { - return ( - - {valueTypeMap.get(item.valueType)?.(item, index)} - - ); - } - if (item.valueType === 'html') { - return ( - - ); - } - - if (item.valueType === 'h1') { - return ( - - {item.children as string} - - ); - } - - if (item.valueType === 'h2') { - return ( - - {item.children as string} - - ); - } - if (item.valueType === 'image') { - return ( -
- -
- ); - } - if (item.valueType === 'inlineLink') { - return ( - -
)} /> - - ); - } - if (item.valueType === 'link') { - return ( - - ); - } - return {item.children as string}; - }; - - return
{dataSourceChildren?.map(itemRender)}
; -}; - -/** - * 异步加载内容的面板组件 - * @param item 指向当前面板的 ProHelpDataSource - */ -const AsyncContentPanel: React.FC<{ - item: ProHelpDataSource['children'][number]; - onInit?: (ref: HTMLDivElement) => void; -}> = ({ item, onInit }) => { - const { onLoadContext } = useContext(ProHelpProvide); // 获取上下文中的 onLoadContext - const [loading, setLoading] = useState(false); // 加载状态 - const [content, setContent] = useState[]>(); // 内容数据 - - useEffect(() => { - if (!item.key) return; // 如果没有key则返回 - setLoading(true); // 开始加载 - onLoadContext?.(item.key, item).then((res) => { - // 调用加载方法 - setLoading(false); // 加载完成 - setContent(res); // 设置内容数据 - }); - }, [item.key]); - - // 如果没有key,则返回null - if (!item.key) return null; - - // 如果正在加载并且有key,则显示加载中的状态 - if (loading && item.key) { - return ( -
- -
- ); - } - - // 加载完成后,渲染内容面板 - return ( - { - onInit?.(ref); - }} - dataSourceChildren={content!} - /> - ); -}; - -/** - * 控制具体的帮助文档显示组件 - * selectedKey 来展示对应的内容。它会根据不同的item.valueType值来展示不同的内容,包括标题、图片、超链接等。 - * @param ProHelpContentPanelProps - * @returns - */ -export const ProHelpContentPanel: React.FC = ({ - className, - parentItem, - selectedKey, - onScroll, -}) => { - const { dataSource } = useContext(ProHelpProvide); - - // 记录每个面板的滚动高度 - const scrollHeightMap = useRef>(new Map()); - - const divRef = useRef(null); - - useEffect(() => { - if (!selectedKey || !parentItem?.infiniteScrollFull) return; - const div = scrollHeightMap.current.get(selectedKey); - if (div?.offsetTop && divRef.current) { - if (Math.abs(divRef.current!.scrollTop - div?.offsetTop + 40) > div?.clientHeight) { - divRef.current!.scrollTop = div?.offsetTop - 40; - } - } - }, [selectedKey]); - - /** - * debounce(防抖)处理滚动事件,并根据滚动位置来实现找到当前列表的 key - */ - const onScrollEvent = useDebounceFn(async (e: Event) => { - const dom = e?.target as HTMLDivElement; - // 根据滚动位置来找到当前列表的 key - const list = Array.from(scrollHeightMap.current.entries()).find(([, value]) => { - if (dom?.scrollTop < value.offsetTop) { - return true; - } - return false; - }); - - if (!list) { - return; - } - // 如果获取的 key 和当前 key 不同丢弃掉 - if (list.at(0) !== selectedKey) { - onScroll?.(list.at(0) as string | undefined); - } - }, 200); - - /** - * 当 parentItem 组件中的 infiniteScrollFull 属性变化时 - * 如果该属性为真值,则开始监听滚动事件; - * 如果为假值,则停止监听滚动事件并取消防抖处理。 - * 在监听滚动事件时,可以实现分页(瀑布流)效果。同时,该代码还会根据 selectedKey 的变化来触发跳转 - */ - useEffect(() => { - if (!parentItem?.infiniteScrollFull) return; - onScrollEvent.cancel(); - divRef.current?.addEventListener('scroll', onScrollEvent.run, false); - return () => { - onScrollEvent.cancel(); - divRef.current?.removeEventListener('scroll', onScrollEvent.run, false); - }; - }, [parentItem?.infiniteScrollFull, selectedKey]); - - /** - * 生成一个 Map 能根据 key 找到所有的 index - */ - const dataSourceMap = useMemo(() => { - const map = new Map< - React.Key, - ProHelpDataSource['children'][number] & { - parentKey?: React.Key; - } - >(); - dataSource.forEach((page) => { - page.children.forEach((item) => { - map.set(item.key || item.title, { ...item, parentKey: page.key }); - }); - }); - return map; - }, [dataSource]); - - const renderItem = (item: ProHelpDataSource['children'][number]) => { - if (item?.asyncLoad) { - return ( -
- { - if (!scrollHeightMap.current) return; - scrollHeightMap.current.set(item.key, ref); - }} - /> -
- ); - } - - return ( -
- { - if (!scrollHeightMap.current) return; - scrollHeightMap.current.set(item.key, ref); - }} - dataSourceChildren={item?.children || []} - /> -
- ); - }; - - if (parentItem && parentItem.infiniteScrollFull) { - return ( -
- {parentItem.children?.map((item) => { - return {renderItem(item)}; - }) || null} -
- ); - } - return renderItem(dataSourceMap.get(selectedKey!)!); -}; - -/** - * ProHelpPanel 组件是一个帮助中心面板组件,具有可折叠的左侧菜单和右侧帮助内容区域。 - * 左侧菜单显示了帮助文档的目录结构,右侧帮助内容区域显示了用户选中的帮助文档内容。 - * 在左侧菜单中,用户可以通过点击目录来选择并显示相应的文档内容。 - * @param param0 - * @returns - */ -export const ProHelpPanel: React.FC = ({ - bordered = true, - onClose, - footer, - height, - ...props -}) => { - const { getPrefixCls } = useContext(ConfigProvider.ConfigContext); - const className = getPrefixCls('pro-help'); - const { wrapSSR } = useStyle(className); - const { dataSource } = useContext(ProHelpProvide); - const [selectedKey, setSelectedKey] = useMergedState(undefined, { - defaultValue: props.defaultSelectedKey, - value: props.selectedKey, - onChange: props.onSelectedKeyChange, - }); - const [openKey, setOpenKey] = useState(''); - const { token } = useContext(ProProvider); - const [showLeftPanel, setShowLeftPanel] = useMergedState(true, { - value: props.showLeftPanel, - onChange: props.onShowLeftPanelChange, - }); - - const dataSourceKeyMap = useMemo(() => { - const map = new Map< - React.Key, - ProHelpDataSource & { - parentKey?: React.Key; - } - >(); - dataSource.forEach((page) => { - map.set(page.key, page); - page.children?.forEach((item) => { - map.set(item.key || item.title, { - parentKey: page.key, - ...item, - } as unknown as ProHelpDataSource); - }); - }); - return map; - }, [dataSource]); - - const parentKey = useMemo( - () => dataSourceKeyMap.get(selectedKey!)?.parentKey as string, - [dataSourceKeyMap, selectedKey], - ); - - return wrapSSR( - -
- { - setShowLeftPanel(!showLeftPanel); - }} - /> -
- { - setSelectedKey(value); - setOpenKey((item as any)?.dataItemKey); - }} - /> - - {onClose ? ( -
- { - onClose?.(); - }} - /> -
- ) : null} -
- } - > - {showLeftPanel ? ( -
- - { - setOpenKey(keys.at(-1) || ''); - }} - selectedKeys={selectedKey ? [selectedKey] : []} - onSelect={({ selectedKeys }) => { - setSelectedKey(selectedKeys.at(-1) || ''); - }} - mode="inline" - items={dataSource.map((item) => { - return { - key: item.key, - label: item.title, - children: item.children.map((child) => { - return { - key: child.key, - label: child.title, - }; - }), - }; - })} - /> - -
- ) : null} -
- {selectedKey ? ( - setSelectedKey(key)} - /> - ) : null} - {footer ?
{footer}
: null} -
- , - ); -}; - export const ProHelp = ({ dataSource, valueTypeMap = new Map(), @@ -601,134 +69,3 @@ export const ProHelp = ({ ); }; - -export type ProHelpPopoverProps = Omit & { - /** - * 悬浮提示文字的 CSS 类名 - */ - textClassName?: string; - - /** - * Popover 内容的 content 的 CSS 类名 - */ - popoverContextClassName?: string; - - /** - * 悬浮提示文字的 CSS 样式对象 - */ - textStyle?: React.CSSProperties; - - /** - * 当前选中的帮助文档的 key 值 - */ - selectedKey: string; - - /** - * 可选的悬浮提示 Popover 组件的 Props,用于自定义悬浮提示的样式和行为。 - * 该属性可以传递 Ant Design Popover 组件的 props,如位置、大小、触发方式等等 - * @see 注意,content 属性已经被从 PopoverProps 中删除,因为这个属性由 ProHelpPopover 内部控制。 - */ - popoverProps?: PopoverProps; -}; - -/** - * 渲染一个弹出式提示框,其中显示了一个ProHelpContentPanel,展示帮助文案的详情 - * @param popoverProps 要传递给 Drawer 组件的属性。 - * @param props 要传递给 ProHelpPanel 组件的属性。 - */ -export const ProHelpPopover: React.FC = (props) => { - const { getPrefixCls } = useContext(ConfigProvider.ConfigContext); - const className = getPrefixCls('pro-help'); - const { wrapSSR } = useStyle(className); - return wrapSSR( - - -
- } - {...props.popoverProps} - > - - {props.children} - - , - ); -}; - -export type ProHelpDrawerProps = { - /** - * Ant Design Drawer 组件的 Props,可以传递一些选项,如位置、大小、关闭方式等等。 - */ - drawerProps: DrawerProps; -} & Omit; - -/** - * 渲染一个抽屉,其中显示了一个 ProHelpPanel。 - * @param drawerProps 要传递给 Drawer 组件的属性。 - * @param props 要传递给 ProHelpPanel 组件的属性。 - */ -export const ProHelpDrawer: React.FC = ({ drawerProps, ...props }) => { - const [drawerOpen, setDrawerOpen] = useMergedState(false, { - value: drawerProps.open, - onChange: drawerProps.afterOpenChange, - }); - return ( - setDrawerOpen(false)} - afterOpenChange={(open) => { - setDrawerOpen(open); - }} - > - setDrawerOpen(false)} bordered={false} /> - - ); -}; - -export type ProHelpModalProps = { - /** - * Ant Design Modal 组件的 props,可以传递一些选项,如位置、大小、关闭方式等等。 - */ - modalProps?: ModalProps; -} & Omit; - -/** - * 渲染一个模态对话框,其中显示了一个 ProHelpPanel。 - * @param modalProps 要传递给 Modal 组件的属性。 - * @param props 要传递给 ProHelpPanel 组件的属性。 - */ -export const ProHelpModal: React.FC = ({ modalProps, ...props }) => { - const [modalOpen, setModalOpen] = useMergedState(false, { - value: modalProps?.open, - onChange: modalProps?.afterClose, - }); - return ( - { - setModalOpen(false); - }} - bodyStyle={{ - margin: -24, - }} - centered - closable={false} - footer={null} - width={720} - open={modalOpen} - maskClosable - {...modalProps} - > - setModalOpen(false)} /> - - ); -}; diff --git a/packages/layout/src/demos/help.tsx b/packages/layout/src/demos/help.tsx index fe70e6b7d61e..f9897ad4a182 100644 --- a/packages/layout/src/demos/help.tsx +++ b/packages/layout/src/demos/help.tsx @@ -438,6 +438,17 @@ export default () => { children: '一种离线处理数据的方式,用户将需要处理的数据批量上传到系统中,再通过系统进行处理。', }, + { + valueType: 'text', + children: '相关数据请查看:', + }, + { + valueType: 'navigationSwitch', + children: { + selectKey: 'name9', + children: '节点场景', + }, + }, ], }, {