import './types/globals.d.ts';

import { CommentSystem } from './modules/comment/Index.ts';
import { Nav } from './modules/Nav.ts';
import { Home } from './modules/home/Index.ts';
import { ArticleRead } from './modules/ArticleRead.ts';
import { Gallery } from './modules/Gallery.ts';
import { Steam } from './modules/Steam.ts';
import { BackToTop } from './modules/BackToTop.ts';
import { MumbleApp } from './modules/Mumble.ts';
import { MusicPlayer } from './modules/music-player/Index.ts';
import { BookmarkManager } from './modules/Bookmark.ts';
import { CodeExplorer } from './modules/CodeExplorer.ts';
import { Friends } from './modules/Friends.ts';
import { Timeline } from './modules/Timeline.ts';
import { Albums } from './modules/Albums.ts';
import { Archive } from './modules/Archive.ts';
import { Video } from './modules/Video.ts';
import { uiFeedbackService } from './modules/UiFeedbackService.ts';
import { signalrNotification } from './modules/SignalrNotification.ts';
import { CodeArea } from './modules/CodeArea.ts';
import { codeAiLive } from './modules/CodeAiLive.ts';
import { groupChat } from './modules/group-chat/Index.ts';
import { EasterEgg } from './modules/EasterEgg.ts';
import { util } from './modules/util.ts';
import { watermelonGame } from './modules/WatermelonGame.ts';
import LazyLoad, { ILazyLoadInstance } from 'vanilla-lazyload';
import { logger } from './modules/ConsoleLogger.ts';

declare global {
    interface Window {
        help: () => void;
    }
}

/**
 * 应用程序主入口类
 * 负责初始化和管理整个前端应用的生命周期
 */
class App {
    /**
     * 导航组件实例
     */
    private readonly _nav: Nav;

    /**
     * 图片懒加载实例
     */
    private _lazyLoadInstance: ILazyLoadInstance | null = null;

    /**
     * Tooltip 延迟显示计时器(仅用于 App 内 title/time 接管场景)
     */
    private _tooltipDelayTimer: number | null = null;

    /**
     * Tooltip 显示延迟,避免快速划过触发频繁绘制
     */
    private static readonly TOOLTIP_SHOW_DELAY_MS = 100;

    public constructor() {
        this._nav = new Nav();
        this._lazyLoadInstance = null;
        this._tooltipDelayTimer = null;
        void this.init();
    }

    /**
     * 初始化应用程序
     * 按顺序初始化各个功能模块和事件监听器
     */
    private async init(): Promise<void> {
        new BackToTop();
        new EasterEgg();
        void watermelonGame;
        void groupChat;
        new MusicPlayer();
        this.initLazyLoad();
        this.initComponents();
        this.initTooltips();
        util.updateTimeTags();
        this.initPjaxEvents();
        await this.initFetchContent();
        await signalrNotification.initSystemNotifications();
        this.initDebugConsole();
        this.setUnSelect();
        this.initMobileMenuEvents();
        this.initLogoutForm();

        const { logger } = await import('./modules/ConsoleLogger.ts');
        logger.outPutSuccess('应用脚本已就绪');
    }

    /**
     * 初始化图片懒加载功能
     */
    private initLazyLoad(): void {
        this._lazyLoadInstance = new LazyLoad({
            elements_selector: '.lazy',
        });
    }

    /**
     * 初始化 PJAX 事件监听
     * PJAX 用于实现无刷新页面跳转,提升用户体验
     */
    private initPjaxEvents(): void {
        $.pjax.defaults.timeout = 5000;
        $(document).pjax('a[data-pjax],#pager:not(#article-pager #pager) a', '#pjax-container');
        $(document).pjax('#article-pager #pager a', '#article-list');

        $(document).on('submit', 'form[data-pjax]', (event) => {
            $.pjax.submit(event, '#pjax-container');
        });

        $(document).on('pjax:send', () => {
            if (typeof NProgress !== 'undefined') {
                NProgress.start();
            }
            uiFeedbackService.hideTooltip();
        });

        $(document).on('pjax:complete', async (_event, xhr) => {
            const titleValue = xhr.getResponseHeader('title');
            this.updateDocumentTitle(titleValue);
        });

        $(document).on('pjax:end', async () => {
            if (typeof NProgress !== 'undefined') {
                NProgress.done();
            }
            this._nav.closeMobileMenu();
            await this.initFetchContent();
            this.initComponents();
            util.updateTimeTags();
            if (this._lazyLoadInstance !== null) {
                this._lazyLoadInstance.update();
            }
            this.setUnSelect();
        });
    }

    /**
     * 更新文档标题
     * 标题通过 Base64 编码传输,需要解码为 UTF-8 字符串
     */
    private updateDocumentTitle(titleValue: string | null): void {
        if (titleValue === '' || titleValue === null) {
            document.title = '(;´д`)ゞ标题不见啦 - 叫我阿胖';
            return;
        }

        try {
            // 先用 atob 解码 Base64
            const binaryString = atob(titleValue);
            // 转换为字节数组
            const bytes = new Uint8Array(binaryString.length);
            for (let i = 0; i < binaryString.length; i++) {
                bytes[i] = binaryString.charCodeAt(i);
            }
            // 使用 UTF-8 解码
            const decoder = new TextDecoder('utf-8');
            document.title = decoder.decode(bytes);
        } catch (e) {
            logger.outPutError('标题解码失败', e);
            document.title = titleValue;
        }
    }

    /**
     * 初始化移动端菜单交互事件
     */
    private initMobileMenuEvents(): void {
        // 移动端菜单点击展开
        $('.mobile-menu-toggle').on('click', () => {
            $('.mobile-nav').addClass('is-open');
            $('.mobile-nav-overlay').addClass('is-open');
        });

        // 点击遮罩或菜单项时关闭
        $('.mobile-nav-overlay, .mobile-nav a').on('click', () => {
            $('.mobile-nav').removeClass('is-open');
            $('.mobile-nav-overlay').removeClass('is-open');
        });
    }

    /**
     * 初始化登出表单,避免重复提交并在移动端提交前收起导航抽屉。
     */
    private initLogoutForm(): void {
        document.addEventListener('submit', (event) => {
            const form = (event.target as HTMLElement | null)?.closest<HTMLFormElement>(
                '[data-logout-form]'
            );
            if (form === null) {
                return;
            }

            event.preventDefault();
            if (form.dataset.logoutPending === 'true') {
                return;
            }

            form.dataset.logoutPending = 'true';
            const submitButton = form.querySelector<HTMLButtonElement>('button[type="submit"]');
            void this.confirmLogout(form, submitButton);
        });
    }

    /**
     * 确认登出后再提交表单,避免误触导致当前登录态立即失效。
     */
    private async confirmLogout(
        form: HTMLFormElement,
        submitButton: HTMLButtonElement | null
    ): Promise<void> {
        const confirmed = await uiFeedbackService.confirm('确定要退出登录吗?', '退出登录');
        if (!confirmed) {
            delete form.dataset.logoutPending;
            return;
        }

        this._nav.closeMobileMenu();
        if (submitButton !== null) {
            submitButton.disabled = true;
            submitButton.setAttribute('aria-busy', 'true');
        }
        form.submit();
    }

    /**
     * 初始化页面功能组件
     * 根据页面内容初始化对应的功能模块
     */
    private initComponents(): void {
        new Home();
        new ArticleRead();
        new Steam();
        new MumbleApp();
        new BookmarkManager();
        new CodeExplorer();
        new Friends();
        new Timeline();
        new Albums();
        new Archive();
        new Video();
        new CodeArea();
        codeAiLive.init();

        new Gallery();

        void this.loadPageSpecificModules();
    }

    /**
     * 根据路径动态加载特定页面的模块
     */
    private async loadPageSpecificModules(): Promise<void> {
        const pathname = window.location.pathname;
        if (pathname.includes('/dialog-demo.html')) {
            const { DialogDemo } = await import('./modules/DialogDemo.ts');
            new DialogDemo();

            logger.outPutSuccess('脚本已就绪');
        }
    }

    /**
     * 初始化工具提示功能
     * 处理鼠标悬停和触摸事件的提示显示
     */
    private initTooltips(): void {
        const hideGlobal = (): void => {
            this.cancelTooltipShow();
            uiFeedbackService.hideTooltip();
        };
        document.addEventListener('click', hideGlobal, true);

        let isTouch = false;
        document.addEventListener(
            'touchstart',
            () => {
                isTouch = true;
                hideGlobal();
                setTimeout(() => {
                    isTouch = false;
                }, 1000);
            },
            { passive: true, capture: true }
        );

        document.addEventListener('contextmenu', (event) => {
            if (!isTouch) {
                return;
            }
            const target = this.getClosestTarget(event, '[title], [data-original-title]');
            if (target !== null) {
                event.preventDefault();
            }
        });

        document.body.addEventListener(
            'mouseenter',
            (event) => {
                const target = this.getEventTargetElement(event);
                if (target === null) {
                    return;
                }
                if (!target.hasAttribute('title')) {
                    return;
                }

                const title = target.getAttribute('title');
                if (!title) {
                    return;
                }

                target.setAttribute('data-original-title', title);
                target.removeAttribute('title');
                this.scheduleTooltipShow(target, title);
            },
            true
        );

        document.body.addEventListener(
            'mouseleave',
            (event) => {
                const target = this.getEventTargetElement(event);
                if (target === null) {
                    return;
                }
                if (!target.hasAttribute('data-original-title')) {
                    return;
                }

                const title = target.getAttribute('data-original-title');
                if (title !== null) {
                    target.setAttribute('title', title);
                }
                target.removeAttribute('data-original-title');
                this.cancelTooltipShow();
                uiFeedbackService.hideTooltip();
            },
            true
        );

        document.body.addEventListener(
            'touchstart',
            (event) => {
                this.handleTimeTooltip(event);
            },
            { passive: true }
        );
        document.body.addEventListener(
            'touchend',
            (event) => {
                this.handleTimeTooltipHide(event);
            },
            { passive: true }
        );
    }

    /**
     * 处理时间元素的工具提示显示
     */
    private handleTimeTooltip(event: Event): void {
        const timeElement = this.getClosestTarget(event, 'time');
        if (timeElement === null) {
            return;
        }

        const datetime = timeElement.getAttribute('datetime');
        if (datetime) {
            this.scheduleTooltipShow(timeElement, datetime);
        }
    }

    /**
     * 处理时间元素的工具提示隐藏
     */
    private handleTimeTooltipHide(event: Event): void {
        const timeElement = this.getClosestTarget(event, 'time');
        if (timeElement === null) {
            return;
        }

        this.cancelTooltipShow();

        if (event.type === 'touchend') {
            setTimeout(() => {
                uiFeedbackService.hideTooltip();
            }, 1500);
            return;
        }

        uiFeedbackService.hideTooltip();
    }

    /**
     * 延迟显示 Tooltip
     */
    private scheduleTooltipShow(element: HTMLElement, text: string): void {
        this.cancelTooltipShow();
        this._tooltipDelayTimer = window.setTimeout(() => {
            this._tooltipDelayTimer = null;
            uiFeedbackService.showTooltip(element, text);
        }, App.TOOLTIP_SHOW_DELAY_MS);
    }

    /**
     * 取消待显示的 Tooltip,防止离开目标后仍触发显示。
     */
    private cancelTooltipShow(): void {
        if (this._tooltipDelayTimer !== null) {
            window.clearTimeout(this._tooltipDelayTimer);
            this._tooltipDelayTimer = null;
        }
    }

    /**
     * 获取事件目标最近的匹配元素
     */
    private getClosestTarget(event: Event, selector: string): HTMLElement | null {
        const target = this.getEventTargetElement(event);
        if (target === null) {
            return null;
        }
        return target.closest<HTMLElement>(selector);
    }

    /**
     * 获取事件目标元素
     */
    private getEventTargetElement(event: Event): HTMLElement | null {
        if (!(event.target instanceof HTMLElement)) {
            return null;
        }
        return event.target;
    }

    /**
     * 自动获取并加载带有 data-request 属性的内容
     * 用于延迟加载页面部分内容
     */
    private async initFetchContent(): Promise<void> {
        const fetchContents = document.querySelectorAll<HTMLElement>('[data-request]');
        for (const item of fetchContents) {
            const loaded = item.dataset.status;
            if (loaded === 'loaded') {
                continue;
            }

            try {
                const requestUrl = item.dataset.request;
                if (!requestUrl) {
                    continue;
                }

                const response = await fetch(requestUrl, {
                    method: 'GET',
                });

                if (response.ok) {
                    item.innerHTML = await response.text();
                    item.dataset.status = 'loaded';
                    const comment = item.querySelector<HTMLElement>('.comment-section');
                    if (comment !== null) {
                        new CommentSystem(comment);
                    }
                    const timeElements = item.querySelectorAll('time');
                    if (timeElements.length > 0) {
                        util.updateTimeTags(item);
                    }
                } else {
                    console.error('Failed to fetch content for', requestUrl);
                    item.innerHTML = '<div class="error-message">加载失败</div>';
                }
            } catch (error) {
                console.error('Error fetching content:', error);
            }
        }
    }

    /**
     * 显示彩蛋功能帮助信息
     */
    public help(): void {
        console.group('🥚 彩蛋指南');
        console.log('1. 连续点击页脚版本号 5 次:触发破碎特效');
        console.log('2. 连续点击页脚版本号 10 次:开启群组聊天');
        console.log('3. 控制台输入 "openGroupChat()":直接开启群组聊天');
        console.log('4. 控制台输入 "openWatermelonGame()":开启合成大西瓜');
        console.log('5. 连续点击页脚版权信息 5 次:开启移动端调试工具');
        console.groupEnd();
    }

    /**
     * 初始化调试控制台加载器
     * 连续点击页脚版权信息 5 次后初始化 Eruda 调试工具
     */
    private initDebugConsole(): void {
        let clickCount = 0;
        let lastClickTime = 0;
        // 2 秒内连续点击有效,超过则重置计数
        const resetTimeout = 2000;

        const selector = '.site-footer .container > p:last-child';
        const copyrightElement = document.querySelector<HTMLElement>(selector);
        if (copyrightElement === null) {
            return;
        }

        copyrightElement.style.cursor = 'pointer';
        copyrightElement.style.userSelect = 'none';

        copyrightElement.addEventListener('click', () => {
            const now = Date.now();
            // 如果距离上次点击超过 2 秒,重置计数
            if (now - lastClickTime > resetTimeout) {
                clickCount = 0;
            }

            clickCount += 1;
            lastClickTime = now;
            if (clickCount >= 5) {
                clickCount = 0;
                void this.initEruda();
            }
        });
    }

    /**
     * 初始化 Eruda 移动端调试工具
     */
    private async initEruda(): Promise<void> {
        const eruda = await import('eruda');
        eruda.default.init();
    }

    /**
     * 禁止某些元素的文本选择
     * 防止用户选中时间和标题属性文本
     */
    private setUnSelect(): void {
        const selector = 'time[datetime],[title]';
        document.querySelectorAll<HTMLElement>(selector).forEach((item) => {
            if (item.style.userSelect !== 'none') {
                item.style.userSelect = 'none';
            }
        });
    }
}

document.addEventListener('DOMContentLoaded', () => {
    const app = new App();
    window.help = () => {
        app.help();
    };
});
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个 TypeScript 前端应用的主入口文件,采用面向对象设计模式,负责初始化和协调整个 Web 应用的各个功能模块。

核心架构

1. App 类 - 应用主控制器

App 类是整个应用的核心,采用单例模式管理应用生命周期。

2. 主要职责

模块化管理

导入并初始化 17+ 个功能模块:

  • 评论系统 (CommentSystem)
  • 导航栏 (Nav)
  • 首页 (Home)
  • 文章阅读 (ArticleRead)
  • 图库、音乐播放器、书签管理等
  • WebSocket 通知 (signalrNotification)
  • 群聊和小游戏彩蛋

PJAX 无刷新跳转

$(document).pjax('a[data-pjax]', '#pjax-container');
  • 实现单页应用体验
  • 自动更新页面标题(Base64 解码)
  • 集成进度条 (NProgress)
  • 刷新懒加载和各组件状态

图片懒加载

this._lazyLoadInstance = new LazyLoad({
    elements_selector: '.lazy',
});

使用 vanilla-lazyload 库优化图片加载性能。

智能 Tooltip 系统

  • 防抖机制:100ms 延迟避免频繁触发
  • 双输入支持
    • 鼠标悬停:接管 title 属性
    • 触摸设备:长按显示,1.5 秒后自动隐藏
  • 特殊处理<time> 元素显示完整时间戳

动态内容加载

const fetchContents = document.querySelectorAll('[data-request]');

自动拉取标记为 data-request 的延迟内容,避免首屏阻塞。

移动端适配

  • 汉堡菜单交互
  • 登出表单二次确认
  • Eruda 调试工具(点击页脚版权 5 次触发)

3. 彩蛋系统

通过特定交互触发隐藏功能:

  • 点击页脚版本号 5 次:破碎特效
  • 点击 10 次:开启群聊
  • 控制台输入 help() 查看完整彩蛋列表

4. 技术特性

类型安全

  • 完整的 TypeScript 类型注解
  • 全局类型声明扩展 (Window.help)

性能优化

  • 事件委托 (捕获阶段监听 mouseenter/mouseleave)
  • 计时器管理防止内存泄漏
  • 条件加载特定页面模块

用户体验细节

  • 禁止选中时间和 title 文本
  • 触摸设备阻止长按菜单误触
  • 404 页面特殊标题处理

关键设计模式

  1. 门面模式App 类统一管理所有子模块
  2. 观察者模式:PJAX 事件监听机制
  3. 策略模式:根据设备类型(触摸/鼠标)切换 Tooltip 策略
  4. 延迟加载:动态导入非首屏模块

初始化流程

DOMContentLoaded 触发
  ↓
创建 App 实例
  ↓
init() 方法执行
  ↓
按序初始化:基础模块 → PJAX → 懒加载 → 组件 → 事件监听
  ↓
日志输出 "应用脚本已就绪"

这是一个工程化程度很高的个人博客/网站前端架构,强调模块化、可维护性和用户体验优化。

评论加载中...