Vue3+Element Plus实战:el-scrollbar在AI聊天窗口中的自动滚动优化技巧
Vue3 Element Plus 实战打造丝滑AI聊天窗口的滚动艺术最近在重构一个智能对话应用的前端界面核心挑战之一就是那个聊天消息窗口的滚动体验。当AI进行流式回复文字像溪流般逐字涌现时如果滚动条卡顿、跳动或者跟不上消息增长的速度整个对话的沉浸感会瞬间崩塌。这不仅仅是功能实现更关乎用户体验的“最后一公里”。Element Plus 的el-scrollbar组件是一个强大的基础但将其与 Vue3 的响应式系统、现代浏览器的滚动 API 深度结合才能雕琢出真正流畅、智能的滚动交互。本文将抛开简单的“滚动到底部”代码深入探讨在 AI 聊天场景下如何系统性地优化el-scrollbar实现近乎原生的滚动体验。1. 理解核心场景AI聊天窗口的滚动挑战AI聊天窗口尤其是支持流式输出的场景对滚动控制提出了独特且苛刻的要求。它不同于普通的列表或日志查看器。首先消息的产生是异步且不可预测的。AI的回复可能是一段长文本分多次chunks到达前端每次到达都需要更新DOM并考虑是否滚动。用户也可能在AI回复过程中快速连续发送消息。这种高频率、不定时的DOM更新是滚动逻辑需要应对的首要挑战。其次用户体验期望是“智能跟随”而非“粗暴跳动”。理想的体验是当用户停留在聊天区域底部附近时新消息到来应自动平滑滚动以保持最新内容可见而当用户主动向上滚动查看历史记录时系统应“感知”到用户意图暂停自动滚动避免打断用户的阅读流。这需要滚动逻辑具备状态感知能力。再者性能与平滑度至关重要。频繁的scrollTop赋值、DOM查询以及滚动监听如果处理不当容易导致布局抖动Layout Thrashing或帧率下降在低端设备或消息密集时尤为明显。我们需要利用现代浏览器API和Vue3的响应式特性进行优化。最后Element Plus 的el-scrollbar是一个封装了原生滚动行为的组件。它提供了更一致的跨浏览器样式和部分自定义能力但其底层仍然是原生的滚动容器overflow: auto/scroll。因此我们的优化既需要利用其提供的ref和事件也需要深入其DOM结构进行精准操作。理解这些挑战是我们构建优化方案的基础。接下来我们将从搭建基础环境开始。2. 环境搭建与基础滚动实现我们从一个干净的 Vue3 TypeScript Element Plus 项目开始。确保你已经安装了必要的依赖。# 创建一个新的Vue项目如果你还没有 npm create vuelatest my-chat-app # 进入项目并安装Element Plus cd my-chat-app npm install element-plus npm install element-plus/icons-vue # 可选用于图标在main.ts或main.js中全局引入 Element Plusimport { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue const app createApp(App) app.use(ElementPlus) app.mount(#app)现在我们来构建一个最基础的聊天窗口组件ChatWindow.vue。这个版本实现了最基本的消息列表展示和“滚动到底部”功能。template div classchat-container div classchat-headerAI对话助手/div !-- 使用 el-scrollbar 包裹消息列表 -- el-scrollbar refscrollbarRef height500px classchat-messages-wrapper scrollhandleScroll div classmessages-list div v-for(msg, index) in messages :keyindex :class[message-bubble, msg.role] div classavatar{{ msg.role user ? : }}/div div classcontent{{ msg.content }}/div /div !-- 流式消息的临时显示区域 -- div v-ifstreamingText classmessage-bubble assistant div classavatar/div div classcontent{{ streamingText }}span classcursor▌/span/div /div /div /el-scrollbar div classchat-input-area el-input v-modelinputText placeholder输入您的问题... keyup.entersendMessage / el-button typeprimary clicksendMessage发送/el-button /div /div /template script setup langts import { ref, onMounted, nextTick } from vue import type { ElScrollbar } from element-plus // 消息数据模型 interface ChatMessage { role: user | assistant content: string } const messages refChatMessage[]([ { role: assistant, content: 您好我是AI助手有什么可以帮您 } ]) const inputText ref() const streamingText ref() // 用于模拟流式输出 const scrollbarRef refInstanceTypetypeof ElScrollbar() // 模拟AI流式回复 const simulateStreamingResponse async (question: string) { const mockResponse 这是关于“${question}”的模拟流式回复。流式输出意味着文字会逐段或逐字返回模拟思考过程。 streamingText.value for (let i 0; i mockResponse.length; i) { await new Promise(resolve setTimeout(resolve, 30)) // 模拟网络延迟 streamingText.value mockResponse[i] // 每次更新流式文本后尝试滚动 scrollToBottomIfNeeded() } // 流式结束将内容存入正式消息列表 messages.value.push({ role: assistant, content: streamingText.value }) streamingText.value scrollToBottom() } const sendMessage () { if (!inputText.value.trim()) return const userMsg: ChatMessage { role: user, content: inputText.value } messages.value.push(userMsg) const question inputText.value inputText.value // 滚动到最新用户消息位置 scrollToBottom() // 模拟AI回复 setTimeout(() simulateStreamingResponse(question), 300) } // 基础滚动到底部函数 const scrollToBottom () { nextTick(() { const scrollbar scrollbarRef.value if (!scrollbar) return // 获取 el-scrollbar 内部的滚动容器 const wrap scrollbar.$el.querySelector(.el-scrollbar__wrap) as HTMLElement if (wrap) { wrap.scrollTop wrap.scrollHeight } }) } // 一个简单的“如果需要则滚动”的版本 const scrollToBottomIfNeeded () { // 暂时留空后续章节会完善智能判断逻辑 scrollToBottom() } // 处理滚动事件用于后续智能判断 const handleScroll ({ scrollTop, scrollHeight, clientHeight }: { scrollTop: number; scrollHeight: number; clientHeight: number }) { // 记录滚动状态用于智能判断 console.log(滚动位置: ${scrollTop}, 总高: ${scrollHeight}, 可视高: ${clientHeight}) } onMounted(() { // 组件挂载后滚动到底部 scrollToBottom() }) /script style scoped .chat-container { width: 100%; max-width: 800px; margin: 0 auto; border: 1px solid #e4e7ed; border-radius: 8px; overflow: hidden; } .chat-header { padding: 16px; background-color: #409eff; color: white; font-weight: bold; text-align: center; } .chat-messages-wrapper { padding: 16px; } .messages-list { display: flex; flex-direction: column; gap: 16px; } .message-bubble { display: flex; gap: 12px; max-width: 80%; } .message-bubble.user { align-self: flex-end; flex-direction: row-reverse; } .message-bubble .avatar { flex-shrink: 0; width: 36px; height: 36px; border-radius: 50%; background-color: #f0f2f5; display: flex; align-items: center; justify-content: center; font-size: 18px; } .message-bubble.user .avatar { background-color: #409eff; color: white; } .message-bubble .content { padding: 12px 16px; border-radius: 18px; background-color: #f0f2f5; line-height: 1.5; } .message-bubble.user .content { background-color: #409eff; color: white; border-bottom-right-radius: 4px; } .message-bubble.assistant .content { border-bottom-left-radius: 4px; } .cursor { animation: blink 1s infinite; } keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } .chat-input-area { display: flex; gap: 12px; padding: 16px; border-top: 1px solid #e4e7ed; } /style这个基础版本已经能够工作发送消息或AI流式回复时窗口会自动滚动到底部。但它存在几个明显问题滚动是瞬间跳动的缺乏平滑感无论用户是否在查看历史消息它都会强制滚动干扰用户频繁的scrollTop赋值可能不够高效。接下来我们将逐一解决这些问题。3. 平滑滚动与性能优化策略瞬间跳动的滚动体验在对话应用中显得非常生硬。现代 CSS 和 JavaScript 提供了多种实现平滑滚动的方法我们需要根据场景选择最合适的一种。3.1 使用 CSSscroll-behavior最简单的方法是为滚动容器添加 CSS 属性scroll-behavior: smooth。这会让该容器内所有通过scrollTop或scrollTo触发的滚动都产生平滑动画效果。我们可以在获取到滚动容器后直接设置其样式const scrollToBottomSmoothCSS () { nextTick(() { const wrap scrollbarRef.value?.$el.querySelector(.el-scrollbar__wrap) as HTMLElement if (!wrap) return // 设置平滑滚动行为 wrap.style.scrollBehavior smooth wrap.scrollTop wrap.scrollHeight // 注意这是一个全局样式设置会影响该容器所有后续滚动。 // 如果需要在特定情况下禁用平滑滚动需要动态切换此属性。 }) }注意scroll-behavior: smooth是一个相对较新的 CSS 属性虽然主流现代浏览器都已支持但其动画曲线和时长通常由浏览器控制自定义程度较低。在某些场景下动画可能显得不够“跟手”。3.2 使用Element.scrollTo()API原生的scrollTo()方法提供了更强大的控制能力允许我们指定滚动的行为behavior和具体的滚动位置。const scrollToBottomNativeAPI () { nextTick(() { const wrap scrollbarRef.value?.$el.querySelector(.el-scrollbar__wrap) as HTMLElement if (!wrap) return wrap.scrollTo({ top: wrap.scrollHeight, behavior: smooth // 也可以是 auto 或 instant }) }) }这种方法比直接设置scrollTop配合 CSS 属性更推荐因为它将行为定义与滚动操作封装在一起意图更清晰且部分浏览器对其优化更好。3.3 自定义动画与requestAnimationFrame对于需要极致控制滚动曲线、时长或者需要兼容旧浏览器的场景我们可以使用requestAnimationFrame手动实现动画。这在实现一些特殊滚动效果如弹性滚动、减速滚动时非常有用。下面是一个使用缓动函数实现自定义平滑滚动的工具函数// 工具函数自定义平滑滚动 const smoothScrollTo (element: HTMLElement, targetScrollTop: number, duration: number 300) { const startScrollTop element.scrollTop const distance targetScrollTop - startScrollTop let startTime: number | null null // 缓动函数easeOutCubic const easeOutCubic (t: number): number { return 1 - Math.pow(1 - t, 3) } const animation (currentTime: number) { if (!startTime) startTime currentTime const timeElapsed currentTime - startTime const progress Math.min(timeElapsed / duration, 1) const easeProgress easeOutCubic(progress) element.scrollTop startScrollTop distance * easeProgress if (timeElapsed duration) { requestAnimationFrame(animation) } } requestAnimationFrame(animation) } // 在组件中使用 const scrollToBottomCustom () { nextTick(() { const wrap scrollbarRef.value?.$el.querySelector(.el-scrollbar__wrap) as HTMLElement if (!wrap) return smoothScrollTo(wrap, wrap.scrollHeight, 400) // 400ms 的动画时长 }) }3.4 性能优化避免布局抖动在流式聊天场景中消息列表会频繁更新。如果我们在每次数据更新如streamingText变化后都立即触发滚动计算和 DOM 操作可能会引发性能问题尤其是在低端设备上。布局抖动Layout Thrashing是指浏览器被迫在短时间内多次重新计算布局和样式导致性能急剧下降。例如在同一个任务循环中频繁读取scrollHeight然后又设置scrollTop可能会触发多次重排。优化策略是批处理和延迟计算。Vue3 的nextTick是一个基础工具它确保我们的滚动操作在 DOM 更新周期之后执行。但对于流式输出这种极高频的更新我们还可以做得更好使用防抖Debounce在流式输出过程中我们不需要每收到一个字符就滚动一次。可以设置一个合理的阈值比如每 100ms 最多触发一次滚动检查。import { debounce } from lodash-es // 或自己实现一个简单的防抖函数 const scrollToBottomDebounced debounce(() { const wrap scrollbarRef.value?.$el.querySelector(.el-scrollbar__wrap) as HTMLElement if (wrap) { wrap.scrollTo({ top: wrap.scrollHeight, behavior: smooth }) } }, 100) // 100毫秒内最多执行一次 // 在流式更新函数中调用防抖版本 // simulateStreamingResponse 函数内部 // streamingText.value chunk // scrollToBottomDebounced()使用requestAnimationFrame节流对于需要与渲染帧同步的操作requestAnimationFrame是比setTimeout或防抖函数更好的节流机制它能保证滚动动画与浏览器的重绘同步避免丢帧。let scrollRafId: number | null null const scrollToBottomRaf () { if (scrollRafId) { cancelAnimationFrame(scrollRafId) } scrollRafId requestAnimationFrame(() { const wrap scrollbarRef.value?.$el.querySelector(.el-scrollbar__wrap) as HTMLElement if (wrap) { wrap.scrollTo({ top: wrap.scrollHeight, behavior: smooth }) } scrollRafId null }) }缓存 DOM 引用避免在每次滚动函数中重复查询.el-scrollbar__wrap。可以在组件挂载时或scrollbarRef可用时一次性获取并缓存该 DOM 元素的引用。const scrollWrapRef refHTMLElement | null(null) onMounted(() { // 假设我们通过某种方式获得了 wrap 的引用 // 例如使用 watch 监听 scrollbarRef 的变化 })综合来看对于大多数 AI 聊天场景使用原生的scrollTo({behavior: smooth})并配合适度的防抖是平衡效果与性能的最佳实践。接下来我们要解决更核心的问题如何让滚动变得“智能”。4. 实现智能滚动状态感知与用户意图尊重强制性的自动滚动会破坏用户体验尤其是在用户想要回看之前的内容时。一个优秀的聊天窗口应该能判断用户的意图只在合适的时机自动滚动。4.1 判断“是否应自动滚动”的逻辑核心逻辑是当新消息到来时如果用户当前已经停留在或非常接近聊天区域的底部则自动滚动到底部如果用户手动向上滚动查看了历史消息则暂停自动滚动。如何定义“停留在底部”一个常见的阈值是判断滚动条距离底部的距离是否小于一个特定值例如 50px 或 100px。我们首先需要增强滚动事件处理以实时追踪用户的滚动位置和意图。// 在组件脚本中定义状态 const isUserScrolling ref(false) // 用户是否正在主动滚动通过鼠标滚轮、拖动滚动条 const isNearBottom ref(true) // 当前是否接近底部 const scrollThreshold 100 // 像素阈值距离底部小于此值则认为“接近底部” // 增强的滚动事件处理函数 const handleScroll ({ scrollTop, scrollHeight, clientHeight }: { scrollTop: number; scrollHeight: number; clientHeight: number }) { // 计算距离底部的像素值 const distanceToBottom scrollHeight - scrollTop - clientHeight // 更新是否接近底部的状态 isNearBottom.value distanceToBottom scrollThreshold // 注意这里不能简单地用 isNearBottom 来判断用户意图。 // 因为当新消息增加导致 scrollHeight 变大时即使用户没动distanceToBottom 也会变大。 // 我们需要更精细的状态管理。 } // 监听鼠标滚轮和拖动事件以标记“用户主动滚动” const wrapElement refHTMLElement | null(null) onMounted(() { const wrap scrollbarRef.value?.$el.querySelector(.el-scrollbar__wrap) if (wrap) { wrapElement.value wrap // 监听 wheel 事件 wrap.addEventListener(wheel, () { isUserScrolling.value true // 设置一个定时器一段时间后重置 isUserScrolling表示滚动停止 clearTimeout((window as any).scrollTimer) ;(window as any).scrollTimer setTimeout(() { isUserScrolling.value false }, 1500) // 1.5秒内无新滚动操作则认为滚动停止 }) // 监听 mousedown 在滚动条滑块上可选较复杂 } })但这还不够。用户可能通过键盘PageUp/Down方向键滚动也可能通过触摸屏滑动。更通用的方法是记录上一次滚动发生前的scrollTop值并与当前值比较。如果变化是由程序设置scrollTop引起的我们不应将其视为用户滚动如果变化是来自其他原因用户输入、惯性滚动等则视为用户滚动。然而在实践中精确区分“程序滚动”和“用户滚动”非常困难且容易出错。一个更健壮、更常见的模式是定义一个“自动滚动启用”的状态 (autoScrollEnabled)默认为true。当用户进行任何可能改变scrollTop的交互时wheel, touchmove, keydown 等将autoScrollEnabled设置为false。当用户滚动到非常接近底部时例如距离底部 10px将autoScrollEnabled重新设置为true。这表示用户“追上了”最新消息希望恢复自动跟随。当新消息到来时只有autoScrollEnabled为true才执行自动滚动。4.2 完整的智能滚动实现让我们实现这个更健壮的方案。我们将创建一个组合式函数useSmartScroll来封装这部分逻辑使其更可复用。// composables/useSmartScroll.ts import { ref, onMounted, onUnmounted, nextTick } from vue import type { Ref } from vue export interface UseSmartScrollOptions { scrollThreshold?: number // 触发“接近底部”判断的阈值像素 enableResetThreshold?: number // 重新启用自动滚动的阈值像素应更小 scrollDebounceMs?: number // 滚动事件防抖毫秒数 } export function useSmartScroll( scrollbarRef: Refany, // el-scrollbar 的 ref options: UseSmartScrollOptions {} ) { const { scrollThreshold 100, enableResetThreshold 10, scrollDebounceMs 150 } options const autoScrollEnabled ref(true) const isNearBottom ref(true) let scrollTimeout: number | null null const checkProximityToBottom () { const wrap scrollbarRef.value?.$el?.querySelector(.el-scrollbar__wrap) as HTMLElement if (!wrap) return const { scrollTop, scrollHeight, clientHeight } wrap const distanceToBottom scrollHeight - scrollTop - clientHeight isNearBottom.value distanceToBottom scrollThreshold // 关键逻辑如果用户滚动到了非常接近底部的位置则重新启用自动滚动 if (distanceToBottom enableResetThreshold) { autoScrollEnabled.value true } } const handleUserScrollAction () { // 用户进行了滚动交互暂停自动滚动 autoScrollEnabled.value false // 防抖检查当前位置 if (scrollTimeout) clearTimeout(scrollTimeout) scrollTimeout window.setTimeout(checkProximityToBottom, scrollDebounceMs) } const scrollToBottom (behavior: ScrollBehavior smooth) { nextTick(() { // 只有启用自动滚动时才执行 if (!autoScrollEnabled.value) return const wrap scrollbarRef.value?.$el?.querySelector(.el-scrollbar__wrap) as HTMLElement if (!wrap) return wrap.scrollTo({ top: wrap.scrollHeight, behavior }) // 滚动后我们肯定在底部更新状态 isNearBottom.value true autoScrollEnabled.value true }) } const scrollToBottomIfNeeded (behavior: ScrollBehavior smooth) { // 这个函数是 scrollToBottom 的“智能”版本外部调用这个即可 // 内部会判断 autoScrollEnabled 和 isNearBottom if (autoScrollEnabled.value isNearBottom.value) { scrollToBottom(behavior) } } onMounted(() { const wrap scrollbarRef.value?.$el?.querySelector(.el-scrollbar__wrap) as HTMLElement if (!wrap) return // 监听多种用户滚动交互 const events [wheel, touchmove, keydown] // keydown 可以监听 PageUp/Down等 events.forEach(eventType { wrap.addEventListener(eventType, handleUserScrollAction, { passive: true }) }) // 初始检查一次位置 checkProximityToBottom() }) onUnmounted(() { if (scrollTimeout) clearTimeout(scrollTimeout) }) // 暴露给组件使用的 API return { autoScrollEnabled, isNearBottom, scrollToBottom, scrollToBottomIfNeeded, // 提供一个手动重置自动滚动的方法例如添加一个“跳至最新”按钮 enableAutoScroll: () { autoScrollEnabled.value true scrollToBottom() } } }在组件中使用这个组合式函数script setup langts // ... 其他导入 import { useSmartScroll } from /composables/useSmartScroll // ... 原有的 ref 定义 const scrollbarRef refInstanceTypetypeof ElScrollbar() // 使用智能滚动逻辑 const { autoScrollEnabled, isNearBottom, scrollToBottom, scrollToBottomIfNeeded, enableAutoScroll } useSmartScroll(scrollbarRef, { scrollThreshold: 150, enableResetThreshold: 20 }) // 修改 simulateStreamingResponse 函数使用智能滚动 const simulateStreamingResponse async (question: string) { const mockResponse 这是关于“${question}”的模拟流式回复。 streamingText.value for (let i 0; i mockResponse.length; i) { await new Promise(resolve setTimeout(resolve, 30)) streamingText.value mockResponse[i] // 使用智能判断的滚动函数 scrollToBottomIfNeeded(smooth) // 只在需要时平滑滚动 } messages.value.push({ role: assistant, content: streamingText.value }) streamingText.value // 流式结束后强制滚动到底部因为此时内容已固定 scrollToBottom(smooth) } // 在模板中添加一个“跳至最新”按钮方便用户手动恢复自动跟随 const scrollToLatest () { enableAutoScroll() // 这会启用自动滚动并立即滚动到底部 } /script template !-- ... 其他模板内容 -- el-scrollbar ... scrollcheckProximityToBottom !-- 消息列表 -- /el-scrollbar !-- 在输入区域附近添加一个提示或按钮 -- div v-if!autoScrollEnabled classscroll-notification el-button sizesmall clickscrollToLatest有新消息点击查看/el-button /div !-- ... -- /template通过这套机制我们的聊天窗口具备了基本的“智能”它不会在用户阅读历史时突兀地跳转同时又能确保当用户停留在对话前沿时新消息能平滑地进入视野。状态提示如“有新消息”按钮进一步尊重了用户的选择权。5. 高级技巧与边界情况处理有了基础的智能滚动我们还可以考虑更多提升体验的细节和应对复杂场景。5.1 处理图片、视频等异步加载内容聊天消息中可能包含图片、视频或 iframe这些资源加载完成后会改变消息气泡的高度从而导致scrollHeight发生变化。如果我们在资源加载前就执行了滚动到底部的操作当资源加载完成后用户视图可能就不再处于最底部。解决方案是使用ResizeObserver来监听消息列表容器或其内部特定元素的大小变化。import { onMounted, onUnmounted } from vue const messagesListRef refHTMLElement() // 指向 .messages-list 元素 onMounted(() { const listEl messagesListRef.value if (!listEl) return const resizeObserver new ResizeObserver(() { // 当消息列表尺寸变化如图片加载时检查是否需要滚动 if (autoScrollEnabled.value isNearBottom.value) { scrollToBottom(smooth) } }) resizeObserver.observe(listEl) onUnmounted(() { resizeObserver.disconnect() }) })提示ResizeObserver回调可能被频繁触发建议在其中使用防抖逻辑避免性能问题。5.2 滚动位置恢复与虚拟列表考量在超长聊天历史中直接渲染所有 DOM 节点会导致性能灾难。此时通常会引入虚拟列表技术如使用vue-virtual-scroller或自行实现。在虚拟列表中scrollHeight是估算的DOM 节点是复用的传统的滚动到底部方法可能失效。如果使用虚拟列表你需要确保虚拟列表组件提供正确的滚动到索引或滚动到底部的 API。智能滚动逻辑需要基于虚拟列表暴露的可见区域索引和总数据量来判断是否“接近底部”。可能需要监听虚拟列表的item-resized等事件来处理内容高度变化。这是一个更高级的话题其实现高度依赖于你所选用的虚拟列表库。5.3 平滑滚动到特定消息消息引用有时用户可能点击一个引用链接需要滚动到历史中的某条特定消息。这要求我们能够计算并滚动到目标消息的位置。假设每条消息都有一个唯一的id和对应的 DOM 元素data-message-idtemplate div classmessages-list refmessagesListRef div v-formsg in messages :keymsg.id :data-message-idmsg.id classmessage-bubble !-- 消息内容 -- a clickscrollToMessage(msg.id)#/a /div /div /template script setup langts const scrollToMessage (messageId: string) { nextTick(() { const messagesList messagesListRef.value if (!messagesList) return const targetElement messagesList.querySelector([data-message-id${messageId}]) as HTMLElement if (!targetElement) return const wrap scrollbarRef.value?.$el.querySelector(.el-scrollbar__wrap) as HTMLElement if (!wrap) return // 计算目标元素相对于滚动容器的偏移位置 const targetOffsetTop targetElement.offsetTop const containerOffsetTop messagesList.offsetTop // 如果列表有内边距需要考虑 const scrollToPosition targetOffsetTop - containerOffsetTop - 20 // 减去20px使其上方有些空间 wrap.scrollTo({ top: scrollToPosition, behavior: smooth }) // 滚动到特定消息属于用户主动行为应暂停自动滚动 autoScrollEnabled.value false }) } /script5.4 与el-scrollbar特定属性的结合el-scrollbar组件本身提供了一些有用的属性可以在特定场景下结合使用always属性是否始终显示滚动条。在聊天应用中通常设置为false默认让滚动条在需要时才出现界面更清爽。max-height/height务必为滚动区域设置一个明确的高度或最大高度这是滚动容器能够工作的前提。通常使用max-height配合弹性布局更灵活。native属性设置为true时el-scrollbar将退化为原生滚动条。如果你需要极致性能或对自定义样式要求不高可以考虑使用原生滚动其行为在某些浏览器中可能更一致。一个综合了上述多项优化技巧的聊天窗口其滚动体验将非常接近主流商业应用如 Slack, Discord的水平。它流畅、智能、尊重用户并能优雅地处理各种边界情况。

相关新闻

VideoAgentTrek Screen Filter快速上手:ComfyUI可视化工作流搭建与调用

VideoAgentTrek Screen Filter快速上手:ComfyUI可视化工作流搭建与调用

VideoAgentTrek Screen Filter快速上手:ComfyUI可视化工作流搭建与调用 你是不是也对那些能自动分析视频、识别内容的AI工具感到好奇,但又觉得写代码门槛太高?别担心,今天咱们就来聊聊一个特别适合“动手派”的解决方案。不用敲一…

2026/7/3 19:15:01 阅读更多 →
修复 ComfyUI-3D-Pack 在 Python 3.12 下的 SyntaxWarning 警告

修复 ComfyUI-3D-Pack 在 Python 3.12 下的 SyntaxWarning 警告

🛠️ 修复 ComfyUI-3D-Pack 在 Python 3.12 下的 SyntaxWarning 警告 ComfyUI-3D-Pack 插件仓库 https://github.com/MrForExample/ComfyUI-3D-Pack ComfyUI-3D-Pack 所有依赖及安装教程在文章及文内的引用链接中ComfyUI-3D-Pack Windows 11 安装完全指南ComfyUI-3D…

2026/5/17 7:37:41 阅读更多 →
比迪丽LoRA模型轻量化实践:LoRA+QLoRA混合微调,显存降低40%

比迪丽LoRA模型轻量化实践:LoRA+QLoRA混合微调,显存降低40%

比迪丽LoRA模型轻量化实践:LoRAQLoRA混合微调,显存降低40% 1. 引言:当二次元角色遇上大模型微调 如果你玩过AI绘画,特别是Stable Diffusion这类工具,肯定对LoRA模型不陌生。它就像给AI模型安装了一个“角色插件”&am…

2026/7/3 0:05:12 阅读更多 →

最新新闻

知网查重太贵?2026年免费论文查重渠道汇总+PaperRed隐藏功能曝光

知网查重太贵?2026年免费论文查重渠道汇总+PaperRed隐藏功能曝光

2026年毕业季,知网查重一次要多少钱?答案是:本科论文约100-200元,硕博论文200-400元。而且很多学校只给1-2次免费查重机会,用完之后就得自费。对于预算有限的学生来说,这笔开销不算小。更让人头疼的是&…

2026/7/5 5:43:44 阅读更多 →
电机控制进阶——PID速度环参数整定实战与调优

电机控制进阶——PID速度环参数整定实战与调优

1. PID速度环控制基础概念 第一次接触电机PID控制时,我盯着那三条看似简单的曲线发愣——比例、积分、微分,这三个数学概念怎么就能让电机转速乖乖听话呢?后来在实验室熬了三个通宵才明白,PID控制就像教小朋友骑自行车&#xff1a…

2026/7/5 5:41:44 阅读更多 →
Meshroom完整指南:免费开源3D重建软件从入门到精通

Meshroom完整指南:免费开源3D重建软件从入门到精通

Meshroom完整指南:免费开源3D重建软件从入门到精通 【免费下载链接】Meshroom Node-based Visual Programming Toolbox 项目地址: https://gitcode.com/gh_mirrors/me/Meshroom 你是否曾想过,能否将手机拍摄的普通照片变成逼真的3D模型&#xff1…

2026/7/5 5:41:44 阅读更多 →
企业级接口自动化测试框架搭建:基于pytest+requests+Allure+YAML实战

企业级接口自动化测试框架搭建:基于pytest+requests+Allure+YAML实战

1. 项目概述:为什么我们需要一个企业级接口自动化框架? 在当前的软件研发流程中,接口作为前后端、微服务之间通信的基石,其稳定性和正确性直接决定了整个系统的质量。如果你还在用 Postman 手动点来点去,或者写一堆零…

2026/7/5 5:37:43 阅读更多 →
MeshLab终极指南:3D网格处理从入门到精通完整教程

MeshLab终极指南:3D网格处理从入门到精通完整教程

MeshLab终极指南:3D网格处理从入门到精通完整教程 【免费下载链接】meshlab The open source mesh processing system 项目地址: https://gitcode.com/gh_mirrors/me/meshlab 你是否曾经面对杂乱无章的3D扫描数据感到束手无策?或者想要优化模型却…

2026/7/5 5:33:41 阅读更多 →
三步搞定开源DPS统计工具:深度解析《碧蓝幻想:Relink》战斗数据

三步搞定开源DPS统计工具:深度解析《碧蓝幻想:Relink》战斗数据

三步搞定开源DPS统计工具:深度解析《碧蓝幻想:Relink》战斗数据 【免费下载链接】gbfr-logs GBFR Logs lets you track damage statistics with a nice overlay DPS meter for Granblue Fantasy: Relink. 项目地址: https://gitcode.com/gh_mirrors/gb…

2026/7/5 5:33:41 阅读更多 →

日新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/5 0:03:34 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/5 0:03:34 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/5 0:07:38 阅读更多 →

周新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/5 0:03:34 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/5 0:03:34 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/5 0:07:38 阅读更多 →

月新闻