feat: 流式数据中自动滚动控制实现

master
LCJ-MinYa 10 months ago
parent c3385af9bc
commit 84afbb6d75

@ -0,0 +1,124 @@
import { ref, onMounted, onUnmounted } from 'vue';
/**
@example
1. hooks, useAutoScroll()canAutoScroll, contentRef, contentScrollFN
import { useAutoScroll } from '@/hooks/useAutoScroll';
const { canAutoScroll, contentRef, contentScrollFN } = useAutoScroll();
2. domcontentRef(contentRef)scrollcontentScrollFN()
<div
class="wordbox"
ref="contentRef"
@scroll="contentScrollFN"
>
</div>
3. canAutoScrolltrue
###
dom
.wordbox {
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: none;
word-break: break-all;
}
.wordbox::-webkit-scrollbar {
display: none;
}
*/
export function useAutoScroll() {
let prescrollHeight = 0;
let touchStartPointY = 0;
const canAutoScroll = ref(false);
const contentRef = ref(null);
const contentScrollFN = () => {
if (canAutoScroll.value === true) {
return;
}
// +10 这里是一个缓冲区,只要快到底部就触发自动滚动
if (contentRef.value.scrollTop + contentRef.value.clientHeight + 10 >= contentRef.value.scrollHeight) {
console.log('设置开始自动滚动');
canAutoScroll.value = true;
}
};
const observeScrollFN = () => {
const observe = new MutationObserver((mutations) => {
if (!contentRef.value || !canAutoScroll.value) {
return;
}
mutations.forEach((mutation) => {
if (mutation.type !== 'childList') {
return;
}
if (prescrollHeight != contentRef.value.scrollHeight) {
contentRef.value.scrollTo({
top: contentRef.value.scrollHeight,
behavior: 'smooth',
});
prescrollHeight = contentRef.value.scrollHeight;
}
});
});
const config = {
childList: true,
subtree: true,
};
observe.observe(contentRef.value, config);
};
const touchstartFN = (event) => {
touchStartPointY = event.targetTouches[0].pageY;
};
const touchmoveFN = (event) => {
if (event.targetTouches.length > 1) {
return;
}
// event.targetTouches[0].pageY - touchStartPointY > 0 即为内容往上滑动
// 5这里也是缓冲区
if (canAutoScroll.value === true && event.targetTouches[0].pageY - touchStartPointY > 5) {
canAutoScroll.value = false;
}
};
const touchendFN = () => {
touchStartPointY = 0;
};
const wheelFN = (event) => {
/** 修复mac下whell触发后惯性继续触发问题 */
if (event.deltaY < 0) {
// console.log('向上滚动');
canAutoScroll.value = false;
} else if (event.deltaY > 0) {
// console.log('向下滚动');
}
};
onMounted(() => {
observeScrollFN();
window.addEventListener('touchstart', touchstartFN);
window.addEventListener('touchmove', touchmoveFN);
window.addEventListener('touchend', touchendFN);
window.addEventListener('wheel', wheelFN);
});
onUnmounted(() => {
window.removeEventListener('touchstart', touchstartFN);
window.removeEventListener('touchmove', touchmoveFN);
window.removeEventListener('touchend', touchendFN);
window.removeEventListener('wheel', wheelFN);
});
return {
canAutoScroll,
contentRef,
contentScrollFN,
};
}

@ -15,40 +15,37 @@
</div>
<div
class="wordbox"
ref="containerRef"
ref="contentRef"
@scroll="contentScrollFN"
>
<span
<div
id="content"
v-html="content"
></span>
></div>
</div>
</base-container>
</template>
<script setup>
import { ref, nextTick } from 'vue';
import { ref } from 'vue';
import { fetchEventSource } from '@microsoft/fetch-event-source';
import { useAutoScroll } from '@/hooks/useAutoScroll';
const { canAutoScroll, contentRef, contentScrollFN } = useAutoScroll();
const content = ref('');
const controller = ref(null);
const updateContent = (event) => {
console.log(event);
console.log('sse收到消息');
content.value += event.data + '<br/>';
console.log(content.value, 'content内容');
content.value += event.data;
if (event.data === 'End') {
/**
* 注释部分为前端手动关闭sse连接的实现
* 这里不受冻关闭原因是@microsoft/fetch-event-source这个框架可以正确检测后段服务端是否关闭了连接所以不需要手动关闭
* 这里不手动关闭原因是@microsoft/fetch-event-source这个框架可以正确检测后段服务端是否关闭了连接所以不需要手动关闭
* 浏览器原生new EventSource()方法无法检测后端服务端是否关闭连接所以需要手动关闭(每次后段关闭都会进onerror事件)
* 具体请看另外一个demo sseNative.vue
*/
// handleClose();
}
nextTick(() => {
scrollToBottom();
});
};
const handleError = (event) => {
@ -58,19 +55,15 @@ const handleError = (event) => {
const handleClose = () => {
console.log('sse关闭');
// controller.value.abort();
};
const containerRef = ref(null);
const scrollToBottom = () => {
containerRef.value.scrollTop = containerRef.value.scrollHeight;
controller.value.abort();
};
const handleSend = () => {
canAutoScroll.value = true;
controller.value = new AbortController();
const signal = controller.value.signal;
/** @microsoft/fetch-event-source该框架支持POST方法并且支持传参 */
fetchEventSource('http://localhost:3000/events', {
fetchEventSource('http://192.168.197.120:3000/events', {
method: 'POST',
body: JSON.stringify({
question: '你好,我是小雅,请问有什么可以帮助您?',
@ -87,11 +80,19 @@ const handleSend = () => {
<style lang="scss" scoped>
.wordbox {
width: 100%;
height: 100%;
overflow: auto;
height: 200px;
position: relative;
overflow-y: auto;
overflow-x: hidden;
padding: 20px;
border: 1px solid #ccc;
margin: 20px 0;
border-radius: 5px;
scrollbar-width: none;
word-break: break-all;
}
.wordbox::-webkit-scrollbar {
display: none; /* 隐藏滚动条 */
}
</style>

Loading…
Cancel
Save