Skip to content

Commit

Permalink
feat(layout): ProHelp support navigationSwitch
Browse files Browse the repository at this point in the history
  • Loading branch information
chenshuai2144 committed Apr 25, 2023
1 parent b445d0b commit 2406457
Show file tree
Hide file tree
Showing 10 changed files with 783 additions and 672 deletions.
58 changes: 58 additions & 0 deletions packages/layout/src/components/Help/AsyncContentPanel.tsx
Original file line number Diff line number Diff line change
@@ -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<any>['children'][number];
onInit?: (ref: HTMLDivElement) => void;
}> = ({ item, onInit }) => {
const { onLoadContext } = useContext(ProHelpProvide); // 获取上下文中的 onLoadContext
const [loading, setLoading] = useState(false); // 加载状态
const [content, setContent] = useState<ProHelpDataSourceChildren<any>[]>(); // 内容数据

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 (
<div
key={item.key}
style={{
display: 'flex',
justifyContent: 'center',
width: '100%',
boxSizing: 'border-box',
padding: 24,
}}
>
<Spin />
</div>
);
}

// 加载完成后,渲染内容面板
return (
<RenderContentPanel
onInit={(ref) => {
onInit?.(ref);
}}
dataSourceChildren={content!}
/>
);
};
12 changes: 11 additions & 1 deletion packages/layout/src/components/Help/HelpProvide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,25 @@ type ProHelpDataSourceContentType = {
children: string;
} & AnchorHTMLAttributes<HTMLAnchorElement>;
/**
* inlineLink 链接类型的数据源子项内容
* 行内链接类型的数据源子项内容
*/
inlineLink: {
children: string;
} & AnchorHTMLAttributes<HTMLAnchorElement>;

/**
* navigation 类型链接,或切换菜单
*/
navigationSwitch: {
selectKey: string;
children: string;
};

/**
* text 文本类型的数据源子项内容。
*/
text: string;

/**
* image 图片类型的数据源子项内容。
*/
Expand Down
150 changes: 150 additions & 0 deletions packages/layout/src/components/Help/ProHelpContentPanel.tsx
Original file line number Diff line number Diff line change
@@ -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<any>;

onScroll?: (key?: string) => void;
};

/**
* 控制具体的帮助文档显示组件
* selectedKey 来展示对应的内容。它会根据不同的item.valueType值来展示不同的内容,包括标题、图片、超链接等。
* @param ProHelpContentPanelProps
* @returns
*/
export const ProHelpContentPanel: React.FC<ProHelpContentPanelProps> = ({
className,
parentItem,
selectedKey,
onScroll,
}) => {
const { dataSource } = useContext(ProHelpProvide);

// 记录每个面板的滚动高度
const scrollHeightMap = useRef<Map<React.Key, HTMLDivElement>>(new Map());

const divRef = useRef<HTMLDivElement>(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<any>['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<any>['children'][number]) => {
if (item?.asyncLoad) {
return (
<div className={className} id={item.title}>
<AsyncContentPanel
key={item?.key}
item={item!}
onInit={(ref) => {
if (!scrollHeightMap.current) return;
scrollHeightMap.current.set(item.key, ref);
}}
/>
</div>
);
}

return (
<div className={className} id={item.title}>
<RenderContentPanel
onInit={(ref) => {
if (!scrollHeightMap.current) return;
scrollHeightMap.current.set(item.key, ref);
}}
dataSourceChildren={item?.children || []}
/>
</div>
);
};

if (parentItem && parentItem.infiniteScrollFull) {
return (
<div
ref={divRef}
className={`${className}-infinite-scroll`}
style={{
overflow: 'auto',
}}
>
{parentItem.children?.map((item) => {
return <React.Fragment key={item.key}>{renderItem(item)}</React.Fragment>;
}) || null}
</div>
);
}
return renderItem(dataSourceMap.get(selectedKey!)!);
};
39 changes: 39 additions & 0 deletions packages/layout/src/components/Help/ProHelpDrawer.tsx
Original file line number Diff line number Diff line change
@@ -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<ProHelpPanelProps, 'onClose'>;

/**
* 渲染一个抽屉,其中显示了一个 ProHelpPanel。
* @param drawerProps 要传递给 Drawer 组件的属性。
* @param props 要传递给 ProHelpPanel 组件的属性。
*/
export const ProHelpDrawer: React.FC<ProHelpDrawerProps> = ({ drawerProps, ...props }) => {
const [drawerOpen, setDrawerOpen] = useMergedState<boolean>(false, {
value: drawerProps.open,
onChange: drawerProps.afterOpenChange,
});
return (
<Drawer
width={720}
closeIcon={null}
headerStyle={{ display: 'none' }}
bodyStyle={{ padding: 0 }}
maskClosable
{...drawerProps}
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
afterOpenChange={(open) => {
setDrawerOpen(open);
}}
>
<ProHelpPanel {...props} onClose={() => setDrawerOpen(false)} bordered={false} />
</Drawer>
);
};
40 changes: 40 additions & 0 deletions packages/layout/src/components/Help/ProHelpModal.tsx
Original file line number Diff line number Diff line change
@@ -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<ProHelpPanelProps, 'onClose'>;

/**
* 渲染一个模态对话框,其中显示了一个 ProHelpPanel。
* @param modalProps 要传递给 Modal 组件的属性。
* @param props 要传递给 ProHelpPanel 组件的属性。
*/
export const ProHelpModal: React.FC<ProHelpModalProps> = ({ modalProps, ...props }) => {
const [modalOpen, setModalOpen] = useMergedState<boolean>(false, {
value: modalProps?.open,
onChange: modalProps?.afterClose,
});
return (
<Modal
onCancel={() => {
setModalOpen(false);
}}
bodyStyle={{
margin: -24,
}}
centered
closable={false}
footer={null}
width={720}
open={modalOpen}
maskClosable
{...modalProps}
>
<ProHelpPanel height={648} {...props} onClose={() => setModalOpen(false)} />
</Modal>
);
};
Loading

0 comments on commit 2406457

Please sign in to comment.