/**
 * 时间线模块
 */
export class Timeline {
    private static _instance: Timeline | null = null;
    private _loading: boolean = false;
    private _hasMore: boolean = true;
    private _pageIndex: number = 1;
    private _pageSize: number = 10;
    private _account: string = 'pengqian';
    private _scrollHandler: ((this: Window, ev: Event) => void) | null = null;
    private _isInitialized: boolean = false;
    private _timelineContainer: HTMLElement | null = null;
    private _loadingElement: HTMLElement | null = null;
    private _endElement: HTMLElement | null = null;

    constructor() {
        if (Timeline._instance) {
            Timeline._instance.destroy();
        }

        this._loading = false;
        this._hasMore = true;
        this._pageIndex = 1;
        this._pageSize = 10;
        this._account = 'pengqian';
        this._scrollHandler = null;
        this._isInitialized = false;

        Timeline._instance = this;
        this.init();
    }

    public init(): void {
        const timelineContainer = document.querySelector<HTMLElement>('.timeline');

        if (!timelineContainer) {
            return;
        }

        this._pageIndex = parseInt(timelineContainer.dataset.pageIndex as string) || 1;
        this._pageSize = parseInt(timelineContainer.dataset.pageSize as string) || 10;
        this._account = timelineContainer.dataset.account || 'pengqian';
        this._hasMore = timelineContainer.dataset.hasMore === 'true';

        this._timelineContainer = timelineContainer;
        this._loadingElement = document.querySelector<HTMLElement>('.timeline__loading');
        this._endElement = document.querySelector<HTMLElement>('.timeline__end');

        this.initItemsAnimation();
        this.setupScrollListener();

        if (this._loadingElement) {
            this._loadingElement.style.display = this._hasMore ? 'flex' : 'none';
        }

        this._isInitialized = true;
    }

    public destroy(): void {
        if (this._scrollHandler) {
            window.removeEventListener('scroll', this._scrollHandler);
            this._scrollHandler = null;
        }
        this._isInitialized = false;

        if (Timeline._instance === this) {
            Timeline._instance = null;
        }
    }

    public initItemsAnimation(): void {
        const timelineItems = document.querySelectorAll<HTMLElement>('.timeline__item');

        if (timelineItems.length === 0) {
            return;
        }

        const observerOptions = {
            root: null,
            rootMargin: '0px',
            threshold: 0.1,
        };

        const observer = new IntersectionObserver((entries) => {
            entries.forEach((entry) => {
                if (entry.isIntersecting) {
                    entry.target.classList.add('is-visible');
                    observer.unobserve(entry.target);
                }
            });
        }, observerOptions);

        timelineItems.forEach((item) => {
            observer.observe(item);
        });
    }

    public setupScrollListener(): void {
        let ticking = false;

        const checkScroll = () => {
            const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
            const windowHeight = window.innerHeight;
            const documentHeight = document.documentElement.scrollHeight;

            const threshold = 500;
            const distanceToBottom = documentHeight - (scrollTop + windowHeight);

            if (distanceToBottom < threshold && this._hasMore && !this._loading) {
                this.loadMore();
            }

            ticking = false;
        };

        this._scrollHandler = () => {
            if (!ticking) {
                window.requestAnimationFrame(checkScroll);
                ticking = true;
            }
        };

        window.addEventListener('scroll', this._scrollHandler, { passive: true });
    }

    public async loadMore(): Promise<void> {
        if (this._loading || !this._hasMore) {
            return;
        }

        this._loading = true;

        if (this._loadingElement) {
            this._loadingElement.style.display = 'flex';
        }

        try {
            const nextPage = this._pageIndex + 1;
            const url = `/Timeline/LoadMore?account=${encodeURIComponent(this._account)}&pageIndex=${nextPage}&pageSize=${this._pageSize}`;

            const response = await fetch(url);

            if (!response.ok) {
                throw new Error('Network response was not ok');
            }

            const html = await response.text();

            if (html.trim() === '') {
                this._hasMore = false;
                this.showEndMessage();
                return;
            }

            const tempDiv = document.createElement('div');
            tempDiv.innerHTML = html;

            const newItems = tempDiv.querySelectorAll<HTMLElement>('.timeline__item');

            if (newItems.length === 0) {
                this._hasMore = false;
                this.showEndMessage();
                return;
            }

            newItems.forEach((item) => {
                if (this._timelineContainer) {
                    this._timelineContainer.appendChild(item);
                }
            });

            this.initItemsAnimation();
            this._pageIndex = nextPage;

            if (newItems.length < this._pageSize) {
                this._hasMore = false;
                this.showEndMessage();
            }
        } catch (error) {
            console.error('加载时间轴失败:', error);
        } finally {
            this._loading = false;
            if (this._loadingElement) {
                this._loadingElement.style.display = 'none';
            }
        }
    }

    public showEndMessage(): void {
        if (this._loadingElement) {
            this._loadingElement.style.display = 'none';
        }
        if (this._endElement) {
            this._endElement.style.display = 'flex';
        }
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这是一个时间线模块的 TypeScript 类,用于管理时间线页面的交互功能。以下是代码的详细解释:

类结构和设计模式

这个类采用了单例模式,确保整个应用中只有一个 Timeline 实例:

private static _instance: Timeline | null = null;

主要功能

1. 初始化功能 (init())

  • 查找页面中的时间线容器元素
  • 从 HTML 数据属性中读取配置(页码、页面大小、账户名等)
  • 初始化动画效果和滚动监听器

2. 滚动加载更多 (loadMore())

  • 实现无限滚动功能
  • 当用户滚动到距离页面底部 500px 时自动加载更多内容
  • 通过 AJAX 请求获取新的时间线数据
  • 动态添加新内容到页面中

3. 动画效果 (initItemsAnimation())

  • 使用 Intersection Observer API 实现滚动动画
  • 当时间线项目进入视窗时添加 is-visible 类名
  • 提供流畅的视觉体验

4. 滚动优化 (setupScrollListener())

  • 使用 requestAnimationFrame 进行滚动事件节流
  • 避免频繁的滚动事件处理,提升性能
  • 使用 passive: true 选项优化滚动性能

核心属性

private _loading: boolean = false;        // 加载状态
private _hasMore: boolean = true;         // 是否还有更多数据
private _pageIndex: number = 1;           // 当前页码
private _pageSize: number = 10;           // 每页数量
private _account: string = 'pengqian';    // 用户账户

生命周期管理

  • 构造函数: 销毁旧实例,初始化新实例
  • destroy(): 清理事件监听器,重置状态
  • 确保内存不泄漏和事件监听器的正确管理

用户体验优化

  1. 加载状态显示: 显示/隐藏加载指示器
  2. 结束提示: 当没有更多数据时显示结束消息
  3. 错误处理: 网络请求失败时的错误处理
  4. 性能优化: 滚动节流和被动事件监听

这个类非常适合用于博客、社交媒体或任何需要时间线展示的网站,提供了完整的无限滚动和动画效果功能。

评论加载中...