fix: 滚动条优化

This commit is contained in:
Gsh
2026-02-07 16:42:08 +08:00
parent 13d6fc228a
commit e6d5c272aa

View File

@@ -2,7 +2,7 @@
<script setup lang="ts">
import type { AnyObject } from 'typescript-api-pro';
import type { BubbleListInstance } from 'vue-element-plus-x/types/BubbleList';
import { Loading } from '@element-plus/icons-vue';
import { Loading, Bottom } from '@element-plus/icons-vue';
import { ElIcon, ElMessage } from 'element-plus';
import { useHookFetch } from 'hook-fetch/vue';
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
@@ -34,6 +34,12 @@ const bubbleItems = ref<MessageItemType[]>([]);
const bubbleListRef = ref<BubbleListInstance | null>(null);
const isSending = ref(false);
// 智能滚动相关状态
const isUserAtBottom = ref(true);
const scrollContainer = ref<HTMLElement | null>(null);
const SCROLL_THRESHOLD = 150; // 距离底部多少像素视为"在底部"
let scrollUpdateTimer: number | null = null;
// 删除模式相关状态
const isDeleteMode = ref(false);
const selectedMessageIds = ref<(number | string)[]>([]);
@@ -129,7 +135,57 @@ watch(
);
function scrollToBottom() {
setTimeout(() => bubbleListRef.value?.scrollToBottom(), 350);
setTimeout(() => {
if (bubbleListRef.value) {
bubbleListRef.value.scrollToBottom();
checkIfAtBottom();
}
}, 350);
}
/**
* 检测用户是否在底部(智能滚动核心)
*/
function checkIfAtBottom() {
const container = scrollContainer.value;
if (!container) {
isUserAtBottom.value = true;
return;
}
const { scrollTop, scrollHeight, clientHeight } = container;
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
isUserAtBottom.value = distanceFromBottom < SCROLL_THRESHOLD;
}
/**
* 智能滚动到底部(仅在用户已在底部时滚动)
*/
function smartScrollToBottom(force = false) {
if (!scrollContainer.value) return;
// 强制滚动或用户已在底部时才滚动
if (force || isUserAtBottom.value) {
const targetScroll = scrollContainer.value.scrollHeight - scrollContainer.value.clientHeight;
scrollContainer.value.scrollTo({
top: targetScroll,
behavior: 'smooth'
});
isUserAtBottom.value = true;
}
}
/**
* 处理滚动事件,检测用户位置
*/
function handleScroll() {
// 使用节流优化性能
if (scrollUpdateTimer !== null) {
clearTimeout(scrollUpdateTimer);
}
scrollUpdateTimer = window.setTimeout(() => {
checkIfAtBottom();
}, 100);
}
/**
@@ -177,6 +233,8 @@ function handleDataChunk(chunk: AnyObject) {
latest.loading = true;
latest.thinlCollapse = true;
latest.reasoning_content += parsed.reasoning_content;
// 流式更新时智能滚动
nextTick(() => smartScrollToBottom());
}
// 处理普通内容
@@ -192,10 +250,13 @@ function handleDataChunk(chunk: AnyObject) {
latest.loading = true;
latest.thinlCollapse = true;
latest.reasoning_content += parsed.content.replace('<think>', '').replace('</think>', '');
nextTick(() => smartScrollToBottom());
} else {
latest.thinkingStatus = 'end';
latest.loading = false;
latest.content += parsed.content;
// 流式更新时智能滚动
nextTick(() => smartScrollToBottom());
}
}
} catch (err) {
@@ -532,6 +593,10 @@ onMounted(() => {
onUnmounted(() => {
document.removeEventListener('paste', handlePaste);
// 清理滚动定时器
if (scrollUpdateTimer !== null) {
clearTimeout(scrollUpdateTimer);
}
});
</script>
@@ -551,12 +616,13 @@ onUnmounted(() => {
/>
<!-- 消息列表 -->
<BubbleList
ref="bubbleListRef"
:list="bubbleItems"
max-height="calc(100vh - 240px)"
:class="{ 'delete-mode': isDeleteMode }"
>
<div ref="scrollContainer" class="chat-with-id__messages-wrapper" @scroll="handleScroll">
<BubbleList
ref="bubbleListRef"
:list="bubbleItems"
max-height="calc(100vh - 240px)"
:class="{ 'delete-mode': isDeleteMode }"
>
<template #content="{ item }">
<MessageItem
:item="item"
@@ -578,6 +644,18 @@ onUnmounted(() => {
</template>
</BubbleList>
<!-- 滚动到底部按钮 -->
<Transition name="scroll-btn-fade">
<button
v-show="!isUserAtBottom"
class="chat-with-id__scroll-bottom-btn"
@click="smartScrollToBottom(true)"
>
<ElIcon><Bottom /></ElIcon>
</button>
</Transition>
</div>
<!-- 发送器 -->
<Sender
ref="senderRef"
@@ -689,6 +767,84 @@ onUnmounted(() => {
color: var(--el-color-primary);
animation: rotating 2s linear infinite;
}
// ==================== 消息包装器样式 ====================
&__messages-wrapper {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
// 平滑滚动
scroll-behavior: smooth;
// Webkit 浏览器滚动条样式 (Chrome, Safari, Edge)
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(144, 147, 153, 0.3);
border-radius: 3px;
transition: background 0.2s ease;
&:hover {
background: rgba(144, 147, 153, 0.5);
}
}
// Firefox 滚动条样式
scrollbar-width: thin;
scrollbar-color: rgba(144, 147, 153, 0.3) transparent;
}
// ==================== 滚动到底部按钮 ====================
&__scroll-bottom-btn {
position: absolute;
bottom: 20px;
right: 20px;
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--el-text-color-secondary);
font-size: 18px;
transition: all 0.3s ease;
z-index: 10;
&:hover {
background: var(--el-color-primary);
border-color: var(--el-color-primary);
color: #fff;
transform: scale(1.05);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
&:active {
transform: scale(0.95);
}
@media (max-width: 768px) {
width: 36px;
height: 36px;
bottom: 16px;
right: 16px;
font-size: 16px;
}
}
}
// 气泡列表基础样式覆盖
@@ -752,4 +908,16 @@ onUnmounted(() => {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
// ==================== 滚动按钮过渡动画 ====================
.scroll-btn-fade-enter-active,
.scroll-btn-fade-leave-active {
transition: all 0.3s ease;
}
.scroll-btn-fade-enter-from,
.scroll-btn-fade-leave-to {
opacity: 0;
transform: translateY(10px);
}
</style>