ForcePilot/web/src/utils/scrollController.js
2026-01-15 06:01:34 +08:00

179 lines
3.8 KiB
JavaScript

import { nextTick } from 'vue'
/**
* 滚动控制工具类
*/
export class ScrollController {
constructor(containerSelector = '.chat', options = {}) {
this.containerSelector = containerSelector
this.options = {
threshold: 100,
scrollDelay: 100,
retryDelays: [50, 150],
...options
}
this.scrollTimer = null
this.isUserScrolling = false
this.shouldAutoScroll = true
this.isProgrammaticScroll = false
// Bind the context of 'this' for the event handler
this.handleScroll = this.handleScroll.bind(this)
}
/**
* 获取滚动容器
* @returns {Element|null}
*/
getContainer() {
return document.querySelector(this.containerSelector)
}
/**
* 检查是否在底部
* @returns {boolean}
*/
isAtBottom() {
const container = this.getContainer()
if (!container) return false
const { threshold } = this.options
return container.scrollHeight - container.scrollTop - container.clientHeight <= threshold
}
/**
* 处理滚动事件
*/
handleScroll() {
if (this.scrollTimer) {
clearTimeout(this.scrollTimer)
}
// 如果是程序性滚动,忽略此次事件
if (this.isProgrammaticScroll) {
this.isProgrammaticScroll = false
return
}
// 标记用户正在滚动
this.isUserScrolling = true
// 检查是否在底部
this.shouldAutoScroll = this.isAtBottom()
// 滚动结束后一段时间重置用户滚动状态
this.scrollTimer = setTimeout(() => {
this.isUserScrolling = false
}, this.options.scrollDelay)
}
/**
* 智能滚动到底部
* @param {boolean} force - 是否强制滚动
*/
async scrollToBottom(force = false) {
await nextTick()
// 只有在应该自动滚动时才执行(除非强制)
if (!force && !this.shouldAutoScroll) return
const container = this.getContainer()
if (!container) return
// 标记为程序性滚动
this.isProgrammaticScroll = true
const scrollOptions = {
top: container.scrollHeight,
behavior: 'smooth'
}
// 立即滚动
container.scrollTo(scrollOptions)
// 多次重试确保滚动成功
this.options.retryDelays.forEach((delay, index) => {
setTimeout(() => {
if (force || this.shouldAutoScroll) {
this.isProgrammaticScroll = true
const behavior = index === this.options.retryDelays.length - 1 ? 'auto' : 'smooth'
container.scrollTo({
top: container.scrollHeight,
behavior
})
}
}, delay)
})
}
async scrollToBottomStaticForce() {
const container = this.getContainer()
if (!container) return
// 标记为程序性滚动
this.isProgrammaticScroll = true
const scrollOptions = {
top: container.scrollHeight,
behavior: 'auto'
}
container.scrollTo(scrollOptions)
}
/**
* 启用自动滚动
*/
enableAutoScroll() {
this.shouldAutoScroll = true
}
/**
* 禁用自动滚动
*/
disableAutoScroll() {
this.shouldAutoScroll = false
}
/**
* 获取滚动状态
*/
getScrollState() {
return {
isUserScrolling: this.isUserScrolling,
shouldAutoScroll: this.shouldAutoScroll,
isAtBottom: this.isAtBottom()
}
}
/**
* 清理定时器
*/
cleanup() {
if (this.scrollTimer) {
clearTimeout(this.scrollTimer)
this.scrollTimer = null
}
}
/**
* 重置滚动状态
*/
reset() {
this.cleanup()
this.isUserScrolling = false
this.shouldAutoScroll = true
this.isProgrammaticScroll = false
}
}
/**
* 创建默认的滚动控制器实例
*/
export const createScrollController = (containerSelector, options) => {
return new ScrollController(containerSelector, options)
}
export default ScrollController