Skip to content

Commit 7edb085

Browse files
authored
✨ feat: 添加 BackToBottom 按钮的各种配置透出 (#69)
* ✨ feat: add backtobottom config
1 parent d89867d commit 7edb085

7 files changed

Lines changed: 199 additions & 88 deletions

File tree

src/BackBottom/index.tsx

Lines changed: 110 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,139 @@
11
import { Button, type BackTopProps } from 'antd';
22
import { ListEnd } from 'lucide-react';
3-
import {
4-
MouseEventHandler,
5-
memo,
6-
useEffect,
7-
useMemo,
8-
useRef,
9-
useState,
10-
type CSSProperties,
11-
} from 'react';
3+
import { MouseEventHandler, useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
124

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

168
export interface BackBottomProps {
179
className?: string;
10+
/**
11+
* @description
12+
* 点击的回调
13+
*/
1814
onClick?: BackTopProps['onClick'];
1915
style?: CSSProperties;
2016
target: React.RefObject<HTMLDivElement>;
2117
text?: string;
2218
visibilityHeight?: BackTopProps['visibilityHeight'];
19+
/**
20+
* @description 自定义渲染 dom
21+
* @param defaultDom
22+
* @param scrollToBottom
23+
* @param BackBottomConfig
24+
* @returns React.ReactNode
25+
*/
26+
render?: (
27+
defaultDom: React.ReactNode,
28+
scrollToBottom: MouseEventHandler<HTMLDivElement>,
29+
BackBottomConfig: BackBottomProps,
30+
) => React.ReactNode;
31+
/**
32+
* @description
33+
* 是否一直显示
34+
*/
35+
alwaysShow?: boolean;
2336
}
2437

25-
const BackBottom = memo<BackBottomProps>(
26-
({ visibilityHeight = 240, target, onClick, style, className, text }) => {
27-
const [visible, setVisible] = useState<boolean>(false);
28-
const { styles, cx } = useStyles(visible);
29-
const ref = useRef<HTMLAnchorElement | HTMLButtonElement>(null);
38+
const BackBottom = (props: BackBottomProps) => {
39+
const {
40+
visibilityHeight = 240,
41+
target,
42+
onClick,
43+
style,
44+
className,
45+
text,
46+
render,
47+
alwaysShow = false,
48+
} = props || {};
49+
const [visible, setVisible] = useState<boolean>(alwaysShow);
50+
const { styles, cx } = useStyles(visible);
51+
const ref = useRef<HTMLAnchorElement | HTMLButtonElement>(null);
3052

31-
const [isWindowAvailable, setIsWindowAvailable] = useState(false);
53+
const [isWindowAvailable, setIsWindowAvailable] = useState(false);
3254

33-
useEffect(() => {
34-
// 检查window对象是否已经可用
35-
if (typeof window !== 'undefined') {
36-
setIsWindowAvailable(true);
37-
}
38-
}, []);
55+
useEffect(() => {
56+
// 检查window对象是否已经可用
57+
if (typeof window !== 'undefined') {
58+
setIsWindowAvailable(true);
59+
}
60+
}, []);
3961

40-
const current = useMemo(() => {
41-
if (target.current && target.current.scrollHeight > target.current.clientHeight) {
42-
return target.current;
43-
}
44-
return document.body;
45-
}, [isWindowAvailable]);
62+
const current = useMemo(() => {
63+
if (target.current && target.current.scrollHeight > target.current.clientHeight) {
64+
return target.current;
65+
}
66+
return document.body;
67+
}, [isWindowAvailable]);
4668

47-
const scrollHeight = current?.scrollHeight || 0;
48-
const clientHeight = current?.clientHeight || 0;
49-
const [scroll, setScroll] = useState({ top: 0, left: 0 });
69+
const scrollHeight = current?.scrollHeight || 0;
70+
const clientHeight = current?.clientHeight || 0;
71+
const [scroll, setScroll] = useState({ top: 0, left: 0 });
5072

51-
const timeRef = useRef<number | null>(null);
73+
const timeRef = useRef<number | null>(null);
5274

53-
useEffect(() => {
54-
if (typeof window === 'undefined') return;
55-
if (typeof current === 'undefined') return;
56-
const scroll = () => {
57-
timeRef.current = window.setTimeout(() => {
75+
useEffect(() => {
76+
if (typeof window === 'undefined') return;
77+
if (typeof current === 'undefined') return;
78+
const scroll = () => {
79+
timeRef.current = window.setTimeout(() => {
80+
if (!alwaysShow) {
5881
setVisible(current?.scrollTop + clientHeight + visibilityHeight < scrollHeight);
59-
setScroll({
60-
top: current?.scrollTop,
61-
left: current?.scrollLeft,
62-
});
63-
}, 60);
64-
};
65-
current?.addEventListener?.('scroll', scroll, {
66-
passive: true,
67-
});
68-
return () => {
69-
if (timeRef.current) {
70-
clearTimeout(timeRef.current);
7182
}
72-
current?.removeEventListener?.('scroll', scroll);
73-
};
74-
}, [current]);
75-
76-
useEffect(() => {
77-
if (scroll?.top) {
78-
setVisible(scroll?.top + clientHeight + visibilityHeight < scrollHeight);
83+
setScroll({
84+
top: current?.scrollTop,
85+
left: current?.scrollLeft,
86+
});
87+
}, 60);
88+
};
89+
current?.addEventListener?.('scroll', scroll, {
90+
passive: true,
91+
});
92+
return () => {
93+
if (timeRef.current) {
94+
clearTimeout(timeRef.current);
7995
}
80-
}, [scrollHeight, scroll, visibilityHeight, current]);
96+
current?.removeEventListener?.('scroll', scroll);
97+
};
98+
}, [current]);
8199

82-
const scrollToBottom: MouseEventHandler<HTMLDivElement> = (e) => {
100+
useEffect(() => {
101+
if (scroll?.top && !alwaysShow) {
102+
setVisible(scroll?.top + clientHeight + visibilityHeight < scrollHeight);
103+
}
104+
}, [scrollHeight, scroll, visibilityHeight, current]);
105+
106+
const scrollToBottom: MouseEventHandler<HTMLDivElement> = (e) => {
107+
(target as any)?.current?.scrollTo({ behavior: 'smooth', left: 0, top: scrollHeight });
108+
onClick?.(e);
109+
};
110+
111+
/**
112+
* @description
113+
* 为了解决在使用了 ProChatProvider 的情况下,BackBottom 无法正常工作的问题
114+
*/
115+
useEffect(() => {
116+
setTimeout(() => {
83117
(target as any)?.current?.scrollTo({ behavior: 'smooth', left: 0, top: scrollHeight });
84-
onClick?.(e);
85-
};
118+
}, 16);
119+
}, []);
120+
121+
const defauleDom = (
122+
<Button
123+
className={cx(styles, className)}
124+
icon={<Icon icon={ListEnd} />}
125+
onClick={scrollToBottom}
126+
ref={ref}
127+
size={'small'}
128+
style={{ bottom: 18, position: 'absolute', right: 16, ...style }}
129+
>
130+
{text || 'Back to bottom'}
131+
</Button>
132+
);
133+
134+
if (render) return render(defauleDom, scrollToBottom, props);
86135

87-
/**
88-
* @description
89-
* 为了解决在使用了 ProChatProvider 的情况下,BackBottom 无法正常工作的问题
90-
*/
91-
useEffect(() => {
92-
setTimeout(() => {
93-
(target as any)?.current?.scrollTo({ behavior: 'smooth', left: 0, top: scrollHeight });
94-
}, 16);
95-
}, []);
96-
97-
return (
98-
<Button
99-
className={cx(styles, className)}
100-
icon={<Icon icon={ListEnd} />}
101-
onClick={scrollToBottom}
102-
ref={ref}
103-
size={'small'}
104-
style={{ bottom: 16, position: 'absolute', right: 16, ...style }}
105-
>
106-
{text || 'Back to bottom'}
107-
</Button>
108-
);
109-
},
110-
);
136+
return defauleDom;
137+
};
111138

112139
export default BackBottom;

src/ProChat/container/App.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,16 @@ interface ConversationProps extends ProChatProps<any> {
3232
}
3333

3434
const App = memo<ConversationProps>(
35-
({ chatInput, className, style, showTitle, chatRef, itemShouldUpdate, chatItemRenderConfig }) => {
35+
({
36+
chatInput,
37+
className,
38+
style,
39+
showTitle,
40+
chatRef,
41+
itemShouldUpdate,
42+
chatItemRenderConfig,
43+
backtoBottomConfig,
44+
}) => {
3645
const ref = useRef<HTMLDivElement>(null);
3746
const areaHtml = useRef<HTMLDivElement>(null);
3847
const { styles, cx } = useStyles();
@@ -86,7 +95,16 @@ const App = memo<ConversationProps>(
8695
/>
8796
<ChatScrollAnchor />
8897
</div>
89-
{isRender ? <BackBottom target={ref} text={'返回底部'} /> : null}
98+
{isRender ? (
99+
<BackBottom
100+
style={{
101+
bottom: 138,
102+
}}
103+
target={ref}
104+
text={'返回底部'}
105+
{...backtoBottomConfig}
106+
/>
107+
) : null}
90108
</>
91109
<div ref={areaHtml}>{chatInput ?? <ChatInputArea />}</div>
92110
</Flexbox>

src/ProChat/container/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CSSProperties, ReactNode } from 'react';
44
import App from './App';
55

66
import { DevtoolsOptions } from 'zustand/middleware';
7+
import { BackBottomProps } from '../../BackBottom';
78
import { ChatProps } from '../store';
89
import { ProChatProvider } from './Provider';
910
import { ProChatChatReference } from './StoreUpdater';
@@ -16,6 +17,7 @@ export interface ProChatProps<T extends Record<string, any>> extends ChatProps<T
1617
className?: string;
1718
chatRef?: ProChatChatReference;
1819
appStyle?: CSSProperties;
20+
backtoBottomConfig?: Omit<BackBottomProps, 'target'>;
1921
}
2022

2123
export function ProChat<T extends Record<string, any> = Record<string, any>>({
@@ -25,6 +27,7 @@ export function ProChat<T extends Record<string, any> = Record<string, any>>({
2527
style,
2628
className,
2729
chatItemRenderConfig,
30+
backtoBottomConfig,
2831
appStyle,
2932
...props
3033
}: ProChatProps<T>) {
@@ -34,6 +37,7 @@ export function ProChat<T extends Record<string, any> = Record<string, any>>({
3437
style={{
3538
height: '100%',
3639
width: '100%',
40+
position: 'relative',
3741
...appStyle,
3842
}}
3943
className={className}
@@ -44,6 +48,7 @@ export function ProChat<T extends Record<string, any> = Record<string, any>>({
4448
chatRef={props.chatRef}
4549
showTitle={showTitle}
4650
style={style}
51+
backtoBottomConfig={backtoBottomConfig}
4752
className={className}
4853
/>
4954
</Container>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* compact: true
3+
*/
4+
import { ProChat } from '@ant-design/pro-chat';
5+
import { useTheme } from 'antd-style';
6+
7+
import { Button } from 'antd';
8+
import { example } from '../mocks/fullFeature';
9+
10+
export default () => {
11+
const theme = useTheme();
12+
return (
13+
<>
14+
<div style={{ background: theme.colorBgLayout, height: '800px' }}>
15+
<ProChat
16+
displayMode={'docs'}
17+
style={{ height: '100%' }}
18+
chats={example.chats}
19+
config={example.config}
20+
backtoBottomConfig={{
21+
render: (_, scrollToBottom) => {
22+
return (
23+
<Button
24+
type="primary"
25+
onClick={scrollToBottom}
26+
style={{
27+
alignSelf: 'flex-end',
28+
width: '200px',
29+
marginRight: '18px',
30+
}}
31+
>
32+
Scroll To Bottom
33+
</Button>
34+
);
35+
},
36+
}}
37+
/>
38+
</div>
39+
</>
40+
);
41+
};

src/ProChat/demos/doc-mode.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/**
22
* iframe: 800
3+
* compact: true
34
*/
45
import { ProChat } from '@ant-design/pro-chat';
56
import { useTheme } from 'antd-style';
@@ -10,10 +11,10 @@ import { MockResponse } from '../mocks/streamResponse';
1011
export default () => {
1112
const theme = useTheme();
1213
return (
13-
<div style={{ background: theme.colorBgLayout, height: 800 }}>
14+
<div style={{ background: theme.colorBgLayout, height: '100vh' }}>
1415
<ProChat
1516
displayMode={'docs'}
16-
style={{ height: '800px' }}
17+
style={{ height: '100%' }}
1718
request={async (messages) => {
1819
const mockedData: string = `这是一段模拟的流式字符串数据。本次会话传入了${messages.length}条消息`;
1920

src/ProChat/demos/draggable.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/**
22
* iframe: 500
3-
* title: 作为侧边栏使用
43
*/
54
import { MockResponse } from '@/ProChat/mocks/streamResponse';
65
import { ProChat } from '@ant-design/pro-chat';

0 commit comments

Comments
 (0)