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