mirror of
https://gitee.com/ccnetcore/Yi
synced 2026-03-03 00:00:58 +08:00
fix: 滚动条优化
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user