Skip to content

Commit

Permalink
✨ feat: 添加 BackToBottom 按钮的各种配置透出 (#69)
Browse files Browse the repository at this point in the history
* ✨ feat: add backtobottom config
  • Loading branch information
ONLY-yours authored Jan 29, 2024
1 parent d89867d commit 7edb085
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 88 deletions.
193 changes: 110 additions & 83 deletions src/BackBottom/index.tsx
Original file line number Diff line number Diff line change
@@ -1,112 +1,139 @@
import { Button, type BackTopProps } from 'antd';
import { ListEnd } from 'lucide-react';
import {
MouseEventHandler,
memo,
useEffect,
useMemo,
useRef,
useState,
type CSSProperties,
} from 'react';
import { MouseEventHandler, useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';

import Icon from '@/Icon';
import { useStyles } from './style';

export interface BackBottomProps {
className?: string;
/**
* @description
* 点击的回调
*/
onClick?: BackTopProps['onClick'];
style?: CSSProperties;
target: React.RefObject<HTMLDivElement>;
text?: string;
visibilityHeight?: BackTopProps['visibilityHeight'];
/**
* @description 自定义渲染 dom
* @param defaultDom
* @param scrollToBottom
* @param BackBottomConfig
* @returns React.ReactNode
*/
render?: (
defaultDom: React.ReactNode,
scrollToBottom: MouseEventHandler<HTMLDivElement>,
BackBottomConfig: BackBottomProps,
) => React.ReactNode;
/**
* @description
* 是否一直显示
*/
alwaysShow?: boolean;
}

const BackBottom = memo<BackBottomProps>(
({ visibilityHeight = 240, target, onClick, style, className, text }) => {
const [visible, setVisible] = useState<boolean>(false);
const { styles, cx } = useStyles(visible);
const ref = useRef<HTMLAnchorElement | HTMLButtonElement>(null);
const BackBottom = (props: BackBottomProps) => {
const {
visibilityHeight = 240,
target,
onClick,
style,
className,
text,
render,
alwaysShow = false,
} = props || {};
const [visible, setVisible] = useState<boolean>(alwaysShow);
const { styles, cx } = useStyles(visible);
const ref = useRef<HTMLAnchorElement | HTMLButtonElement>(null);

const [isWindowAvailable, setIsWindowAvailable] = useState(false);
const [isWindowAvailable, setIsWindowAvailable] = useState(false);

useEffect(() => {
// 检查window对象是否已经可用
if (typeof window !== 'undefined') {
setIsWindowAvailable(true);
}
}, []);
useEffect(() => {
// 检查window对象是否已经可用
if (typeof window !== 'undefined') {
setIsWindowAvailable(true);
}
}, []);

const current = useMemo(() => {
if (target.current && target.current.scrollHeight > target.current.clientHeight) {
return target.current;
}
return document.body;
}, [isWindowAvailable]);
const current = useMemo(() => {
if (target.current && target.current.scrollHeight > target.current.clientHeight) {
return target.current;
}
return document.body;
}, [isWindowAvailable]);

const scrollHeight = current?.scrollHeight || 0;
const clientHeight = current?.clientHeight || 0;
const [scroll, setScroll] = useState({ top: 0, left: 0 });
const scrollHeight = current?.scrollHeight || 0;
const clientHeight = current?.clientHeight || 0;
const [scroll, setScroll] = useState({ top: 0, left: 0 });

const timeRef = useRef<number | null>(null);
const timeRef = useRef<number | null>(null);

useEffect(() => {
if (typeof window === 'undefined') return;
if (typeof current === 'undefined') return;
const scroll = () => {
timeRef.current = window.setTimeout(() => {
useEffect(() => {
if (typeof window === 'undefined') return;
if (typeof current === 'undefined') return;
const scroll = () => {
timeRef.current = window.setTimeout(() => {
if (!alwaysShow) {
setVisible(current?.scrollTop + clientHeight + visibilityHeight < scrollHeight);
setScroll({
top: current?.scrollTop,
left: current?.scrollLeft,
});
}, 60);
};
current?.addEventListener?.('scroll', scroll, {
passive: true,
});
return () => {
if (timeRef.current) {
clearTimeout(timeRef.current);
}
current?.removeEventListener?.('scroll', scroll);
};
}, [current]);

useEffect(() => {
if (scroll?.top) {
setVisible(scroll?.top + clientHeight + visibilityHeight < scrollHeight);
setScroll({
top: current?.scrollTop,
left: current?.scrollLeft,
});
}, 60);
};
current?.addEventListener?.('scroll', scroll, {
passive: true,
});
return () => {
if (timeRef.current) {
clearTimeout(timeRef.current);
}
}, [scrollHeight, scroll, visibilityHeight, current]);
current?.removeEventListener?.('scroll', scroll);
};
}, [current]);

const scrollToBottom: MouseEventHandler<HTMLDivElement> = (e) => {
useEffect(() => {
if (scroll?.top && !alwaysShow) {
setVisible(scroll?.top + clientHeight + visibilityHeight < scrollHeight);
}
}, [scrollHeight, scroll, visibilityHeight, current]);

const scrollToBottom: MouseEventHandler<HTMLDivElement> = (e) => {
(target as any)?.current?.scrollTo({ behavior: 'smooth', left: 0, top: scrollHeight });
onClick?.(e);
};

/**
* @description
* 为了解决在使用了 ProChatProvider 的情况下,BackBottom 无法正常工作的问题
*/
useEffect(() => {
setTimeout(() => {
(target as any)?.current?.scrollTo({ behavior: 'smooth', left: 0, top: scrollHeight });
onClick?.(e);
};
}, 16);
}, []);

const defauleDom = (
<Button
className={cx(styles, className)}
icon={<Icon icon={ListEnd} />}
onClick={scrollToBottom}
ref={ref}
size={'small'}
style={{ bottom: 18, position: 'absolute', right: 16, ...style }}
>
{text || 'Back to bottom'}
</Button>
);

if (render) return render(defauleDom, scrollToBottom, props);

/**
* @description
* 为了解决在使用了 ProChatProvider 的情况下,BackBottom 无法正常工作的问题
*/
useEffect(() => {
setTimeout(() => {
(target as any)?.current?.scrollTo({ behavior: 'smooth', left: 0, top: scrollHeight });
}, 16);
}, []);

return (
<Button
className={cx(styles, className)}
icon={<Icon icon={ListEnd} />}
onClick={scrollToBottom}
ref={ref}
size={'small'}
style={{ bottom: 16, position: 'absolute', right: 16, ...style }}
>
{text || 'Back to bottom'}
</Button>
);
},
);
return defauleDom;
};

export default BackBottom;
22 changes: 20 additions & 2 deletions src/ProChat/container/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@ interface ConversationProps extends ProChatProps<any> {
}

const App = memo<ConversationProps>(
({ chatInput, className, style, showTitle, chatRef, itemShouldUpdate, chatItemRenderConfig }) => {
({
chatInput,
className,
style,
showTitle,
chatRef,
itemShouldUpdate,
chatItemRenderConfig,
backtoBottomConfig,
}) => {
const ref = useRef<HTMLDivElement>(null);
const areaHtml = useRef<HTMLDivElement>(null);
const { styles, cx } = useStyles();
Expand Down Expand Up @@ -86,7 +95,16 @@ const App = memo<ConversationProps>(
/>
<ChatScrollAnchor />
</div>
{isRender ? <BackBottom target={ref} text={'返回底部'} /> : null}
{isRender ? (
<BackBottom
style={{
bottom: 138,
}}
target={ref}
text={'返回底部'}
{...backtoBottomConfig}
/>
) : null}
</>
<div ref={areaHtml}>{chatInput ?? <ChatInputArea />}</div>
</Flexbox>
Expand Down
5 changes: 5 additions & 0 deletions src/ProChat/container/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CSSProperties, ReactNode } from 'react';
import App from './App';

import { DevtoolsOptions } from 'zustand/middleware';
import { BackBottomProps } from '../../BackBottom';
import { ChatProps } from '../store';
import { ProChatProvider } from './Provider';
import { ProChatChatReference } from './StoreUpdater';
Expand All @@ -16,6 +17,7 @@ export interface ProChatProps<T extends Record<string, any>> extends ChatProps<T
className?: string;
chatRef?: ProChatChatReference;
appStyle?: CSSProperties;
backtoBottomConfig?: Omit<BackBottomProps, 'target'>;
}

export function ProChat<T extends Record<string, any> = Record<string, any>>({
Expand All @@ -25,6 +27,7 @@ export function ProChat<T extends Record<string, any> = Record<string, any>>({
style,
className,
chatItemRenderConfig,
backtoBottomConfig,
appStyle,
...props
}: ProChatProps<T>) {
Expand All @@ -34,6 +37,7 @@ export function ProChat<T extends Record<string, any> = Record<string, any>>({
style={{
height: '100%',
width: '100%',
position: 'relative',
...appStyle,
}}
className={className}
Expand All @@ -44,6 +48,7 @@ export function ProChat<T extends Record<string, any> = Record<string, any>>({
chatRef={props.chatRef}
showTitle={showTitle}
style={style}
backtoBottomConfig={backtoBottomConfig}
className={className}
/>
</Container>
Expand Down
41 changes: 41 additions & 0 deletions src/ProChat/demos/backtoBottomConfig.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* compact: true
*/
import { ProChat } from '@ant-design/pro-chat';
import { useTheme } from 'antd-style';

import { Button } from 'antd';
import { example } from '../mocks/fullFeature';

export default () => {
const theme = useTheme();
return (
<>
<div style={{ background: theme.colorBgLayout, height: '800px' }}>
<ProChat
displayMode={'docs'}
style={{ height: '100%' }}
chats={example.chats}
config={example.config}
backtoBottomConfig={{
render: (_, scrollToBottom) => {
return (
<Button
type="primary"
onClick={scrollToBottom}
style={{
alignSelf: 'flex-end',
width: '200px',
marginRight: '18px',
}}
>
Scroll To Bottom
</Button>
);
},
}}
/>
</div>
</>
);
};
5 changes: 3 additions & 2 deletions src/ProChat/demos/doc-mode.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/**
* iframe: 800
* compact: true
*/
import { ProChat } from '@ant-design/pro-chat';
import { useTheme } from 'antd-style';
Expand All @@ -10,10 +11,10 @@ import { MockResponse } from '../mocks/streamResponse';
export default () => {
const theme = useTheme();
return (
<div style={{ background: theme.colorBgLayout, height: 800 }}>
<div style={{ background: theme.colorBgLayout, height: '100vh' }}>
<ProChat
displayMode={'docs'}
style={{ height: '800px' }}
style={{ height: '100%' }}
request={async (messages) => {
const mockedData: string = `这是一段模拟的流式字符串数据。本次会话传入了${messages.length}条消息`;

Expand Down
1 change: 0 additions & 1 deletion src/ProChat/demos/draggable.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/**
* iframe: 500
* title: 作为侧边栏使用
*/
import { MockResponse } from '@/ProChat/mocks/streamResponse';
import { ProChat } from '@ant-design/pro-chat';
Expand Down
Loading

0 comments on commit 7edb085

Please sign in to comment.