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 页面特殊标题处理
关键设计模式
- 门面模式:
App类统一管理所有子模块 - 观察者模式:PJAX 事件监听机制
- 策略模式:根据设备类型(触摸/鼠标)切换 Tooltip 策略
- 延迟加载:动态导入非首屏模块
初始化流程
DOMContentLoaded 触发
↓
创建 App 实例
↓
init() 方法执行
↓
按序初始化:基础模块 → PJAX → 懒加载 → 组件 → 事件监听
↓
日志输出 "应用脚本已就绪"
这是一个工程化程度很高的个人博客/网站前端架构,强调模块化、可维护性和用户体验优化。
AI 正在分析代码…
评论加载中...