This commit is contained in:
2025-12-08 01:03:07 +08:00
commit 5c77d25b6d
334 changed files with 71475 additions and 0 deletions

359
public/scroll-protection.js Normal file
View File

@@ -0,0 +1,359 @@
/**
* 强力滚动保护脚本
* 通过劫持 window.scrollTo 和相关滚动方法来阻止意外的滚动跳转
* 专门解决 Twikoo 评论系统的滚动问题
*/
(() => {
// 保存原始的滚动方法
const originalScrollTo = window.scrollTo;
const originalScrollBy = window.scrollBy;
const originalScrollIntoView = Element.prototype.scrollIntoView;
// 滚动保护状态
const scrollProtection = {
enabled: false,
allowedY: null,
startTime: 0,
duration: 0,
timeout: null,
};
// 检测是否为TOC导航触发的滚动
function checkIsTOCNavigation() {
// 检查调用堆栈看是否来自TOC组件
const stack = new Error().stack;
if (
stack &&
(stack.includes("handleAnchorClick") || stack.includes("TOC.astro"))
) {
return true;
}
// 检查最近是否有TOC点击事件
if (
window.tocClickTimestamp &&
Date.now() - window.tocClickTimestamp < 1000
) {
return true;
}
// 检查是否在TOC元素上
const activeElement = document.activeElement;
if (activeElement && activeElement.closest("#toc, .table-of-contents")) {
return true;
}
return false;
}
// 启动滚动保护
function enableScrollProtection(duration = 3000, currentY = null) {
scrollProtection.enabled = true;
scrollProtection.allowedY =
currentY !== null ? currentY : window.scrollY || window.pageYOffset;
scrollProtection.startTime = Date.now();
scrollProtection.duration = duration;
// 清除之前的定时器
if (scrollProtection.timeout) {
clearTimeout(scrollProtection.timeout);
}
// 设置保护结束时间
scrollProtection.timeout = setTimeout(() => {
scrollProtection.enabled = false;
console.log("[强力滚动保护] 保护期结束");
}, duration);
console.log(
`[强力滚动保护] 启动保护 ${duration}ms允许Y位置:`,
scrollProtection.allowedY,
);
}
// 检查滚动是否被允许
function isScrollAllowed(x, y) {
if (!scrollProtection.enabled) {
return true;
}
// 检查是否是TOC或MD导航触发的滚动
const isTOCNavigation = checkIsTOCNavigation();
if (isTOCNavigation) {
console.log("[强力滚动保护] 检测到TOC导航允许滚动");
return true;
}
// 允许小幅度的滚动调整±50像素
const tolerance = 50;
const allowedY = scrollProtection.allowedY;
if (Math.abs(y - allowedY) <= tolerance) {
return true;
}
// 如果尝试滚动到顶部y < 100而当前位置在更下方阻止
if (y < 100 && allowedY > 100) {
console.log(
"[强力滚动保护] 阻止滚动到顶部目标Y:",
y,
"允许Y:",
allowedY,
);
return false;
}
return true;
}
// 劫持 window.scrollTo
window.scrollTo = (x, y) => {
// 处理参数为对象的情况
if (typeof x === "object") {
const options = x;
x = options.left || 0;
y = options.top || 0;
}
if (isScrollAllowed(x, y)) {
originalScrollTo.call(window, x, y);
} else {
console.log("[强力滚动保护] 阻止 scrollTo:", x, y);
// 如果被阻止,滚动到允许的位置
originalScrollTo.call(window, x, scrollProtection.allowedY);
}
};
// 劫持 window.scrollBy
window.scrollBy = (x, y) => {
const currentY = window.scrollY || window.pageYOffset;
const targetY = currentY + y;
if (typeof x === "object") {
const options = x;
x = options.left || 0;
y = options.top || 0;
}
if (isScrollAllowed(x, targetY)) {
originalScrollBy.call(window, x, y);
} else {
console.log("[强力滚动保护] 阻止 scrollBy:", x, y);
}
};
// 劫持 Element.scrollIntoView
Element.prototype.scrollIntoView = function (options) {
if (!scrollProtection.enabled) {
originalScrollIntoView.call(this, options);
return;
}
// 在保护期内,尝试阻止 scrollIntoView
const rect = this.getBoundingClientRect();
const currentY = window.scrollY || window.pageYOffset;
const targetY = currentY + rect.top;
if (isScrollAllowed(0, targetY)) {
originalScrollIntoView.call(this, options);
} else {
console.log("[强力滚动保护] 阻止 scrollIntoView");
}
};
// 监听 Twikoo 相关的交互事件
document.addEventListener(
"click",
(event) => {
const target = event.target;
// 检查是否点击了TOC导航
if (
target.closest("#toc, .table-of-contents") &&
target.closest('a[href^="#"]')
) {
window.tocClickTimestamp = Date.now();
console.log("[强力滚动保护] 检测到TOC导航点击");
return; // 不启动保护允许TOC正常工作
}
// 检查是否点击了 Twikoo 相关元素
if (
target.closest("#tcomment") ||
target.matches(
".tk-action-icon, .tk-submit, .tk-cancel, .tk-preview, .tk-owo, .tk-admin, .tk-edit, .tk-delete, .tk-reply, .tk-expand",
) ||
target.closest(
".tk-action-icon, .tk-submit, .tk-cancel, .tk-preview, .tk-owo, .tk-admin, .tk-edit, .tk-delete, .tk-reply, .tk-expand",
)
) {
// 立即启动保护
enableScrollProtection(4000); // 增加保护时间到4秒
console.log("[强力滚动保护] 检测到 Twikoo 交互,启动保护");
}
// 特别检查管理面板相关操作(包括关闭操作)
if (
target.matches(
".tk-admin-panel, .tk-admin-overlay, .tk-modal, .tk-dialog, .tk-admin-close, .tk-close",
) ||
target.closest(
".tk-admin-panel, .tk-admin-overlay, .tk-modal, .tk-dialog, .tk-admin-close, .tk-close",
) ||
target.classList.contains("tk-admin") ||
target.closest(".tk-admin")
) {
enableScrollProtection(6000); // 管理面板操作保护更长时间
console.log("[强力滚动保护] 检测到 Twikoo 管理面板操作,启动长期保护");
}
// 检查是否点击了遮罩层(通常用于关闭模态框)
if (
target.classList.contains("tk-overlay") ||
target.classList.contains("tk-mask") ||
target.matches('[class*="overlay"]') ||
target.matches('[class*="mask"]') ||
target.matches('[class*="backdrop"]')
) {
// 检查是否在 Twikoo 区域内
const tcommentEl = document.querySelector("#tcomment");
if (
tcommentEl &&
(target.closest("#tcomment") || tcommentEl.contains(target))
) {
enableScrollProtection(4000);
console.log("[强力滚动保护] 检测到 Twikoo 遮罩层点击,启动保护");
}
}
},
true,
); // 使用捕获阶段
// 监听表单提交
document.addEventListener(
"submit",
(event) => {
if (event.target.closest("#tcomment")) {
enableScrollProtection(4000);
console.log("[强力滚动保护] 检测到 Twikoo 表单提交,启动保护");
}
},
true,
);
// 监听键盘事件(特别是 ESC 键,用于关闭管理面板)
document.addEventListener(
"keydown",
(event) => {
if (event.key === "Escape" || event.keyCode === 27) {
// 检查是否在 Twikoo 区域内有活动的管理面板
const tcommentEl = document.querySelector("#tcomment");
if (tcommentEl) {
// 检查是否有可见的管理面板或模态框
const adminPanel = tcommentEl.querySelector(
".tk-admin-panel, .tk-modal, .tk-dialog, [class*='admin'], [class*='modal']",
);
if (adminPanel && adminPanel.offsetParent !== null) {
// 面板可见,启动保护
enableScrollProtection(3000);
console.log(
"[强力滚动保护] 检测到 ESC 键关闭 Twikoo 管理面板,启动保护",
);
}
}
}
},
true,
);
// 监听 DOM 变化,检测管理面板的关闭
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === "childList" || mutation.type === "attributes") {
const target = mutation.target;
// 检查是否是 Twikoo 相关的 DOM 变化
if (target.closest && target.closest("#tcomment")) {
// 检查是否有元素被移除或隐藏(可能是面板关闭)
if (
mutation.removedNodes.length > 0 ||
(mutation.type === "attributes" &&
mutation.attributeName === "style")
) {
enableScrollProtection(2000);
console.log(
"[强力滚动保护] 检测到 Twikoo DOM 变化(可能是面板关闭),启动保护",
);
}
}
}
});
});
// 开始监听 DOM 变化
if (document.body) {
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["style", "class"],
});
} else {
document.addEventListener("DOMContentLoaded", () => {
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["style", "class"],
});
});
}
// 提供全局接口
window.scrollProtectionManager = {
enable: enableScrollProtection,
disable: () => {
scrollProtection.enabled = false;
if (scrollProtection.timeout) {
clearTimeout(scrollProtection.timeout);
}
console.log("[强力滚动保护] 手动停止保护");
},
isEnabled: () => scrollProtection.enabled,
getStatus: () => ({ ...scrollProtection }),
// 新增:强制保护模式(用于调试)
forceProtect: (duration = 10000) => {
enableScrollProtection(duration);
console.log(`[强力滚动保护] 强制保护模式启动 ${duration}ms`);
},
// 新增:获取当前滚动位置
getCurrentScroll: () => {
return {
x: window.scrollX || window.pageXOffset,
y: window.scrollY || window.pageYOffset,
};
},
// 新增:检测 Twikoo 状态
checkTwikooStatus: () => {
const tcomment = document.querySelector("#tcomment");
if (!tcomment) return { exists: false };
const adminPanels = tcomment.querySelectorAll(
".tk-admin-panel, .tk-modal, .tk-dialog, [class*='admin'], [class*='modal']",
);
const visiblePanels = Array.from(adminPanels).filter(
(panel) => panel.offsetParent !== null,
);
return {
exists: true,
adminPanelsCount: adminPanels.length,
visiblePanelsCount: visiblePanels.length,
hasVisiblePanels: visiblePanels.length > 0,
};
},
};
console.log("[强力滚动保护] 初始化完成");
})();