优化搜索数据规则,优化dom绑定和监听,优化代码样式
This commit is contained in:
parent
f153c65faa
commit
afbdc61605
Binary file not shown.
@ -1092,12 +1092,20 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
|
|||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
date: "all",
|
date: "all",
|
||||||
};
|
};
|
||||||
|
|
||||||
// 先更新UI状态
|
// 先更新UI状态
|
||||||
setActiveFilters(defaultFilters);
|
setActiveFilters(defaultFilters);
|
||||||
|
|
||||||
// 直接传递重置后的状态给筛选函数,并更新URL
|
// 清除URL参数
|
||||||
applyFilters(defaultFilters);
|
if (typeof window !== 'undefined') {
|
||||||
}, []);
|
window.history.pushState({}, "", window.location.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果WASM模块已加载,直接调用筛选逻辑以确保实际应用
|
||||||
|
if (wasmModule && isArticlesLoaded) {
|
||||||
|
applyFilteringLogic(defaultFilters);
|
||||||
|
}
|
||||||
|
}, [wasmModule, isArticlesLoaded]);
|
||||||
|
|
||||||
// 渲染错误信息
|
// 渲染错误信息
|
||||||
const renderError = () => (
|
const renderError = () => (
|
||||||
|
@ -187,99 +187,101 @@ const breadcrumbs: Breadcrumb[] = pathSegments
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
// 返回按钮点击事件处理
|
// 面包屑返回按钮处理 - 自销毁模式
|
||||||
(function() {
|
(function() {
|
||||||
// 页面导航计数器
|
|
||||||
let pageNavigationCount = 0;
|
|
||||||
|
|
||||||
// 存储事件监听器,便于统一清理
|
// 跳过非文章页面,只在文章详情页执行
|
||||||
const listeners = [];
|
const isArticlePage = document.querySelector('.back-button');
|
||||||
|
if (!isArticlePage) {
|
||||||
// 清理按钮事件监听器
|
return;
|
||||||
function cleanupButtonListeners() {
|
|
||||||
// 查找所有返回按钮
|
|
||||||
const buttons = document.querySelectorAll('.back-button');
|
|
||||||
|
|
||||||
buttons.forEach(button => {
|
|
||||||
// 移除所有可能的事件
|
|
||||||
if (button._clickHandler) {
|
|
||||||
button.removeEventListener('click', button._clickHandler);
|
|
||||||
delete button._clickHandler;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除其他可能的事件
|
// 集中管理所有事件监听器
|
||||||
const otherClickHandlers = button.__backButtonClickHandlers || [];
|
const allListeners = [];
|
||||||
otherClickHandlers.forEach(handler => {
|
|
||||||
try {
|
|
||||||
button.removeEventListener('click', handler);
|
|
||||||
} catch (e) {
|
|
||||||
// 忽略错误
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 重置处理函数数组
|
// 单独保存清理事件的监听器引用
|
||||||
button.__backButtonClickHandlers = [];
|
const cleanupListeners = [];
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加事件监听器并记录,方便后续统一清理
|
// 添加事件监听器并记录,方便后续统一清理
|
||||||
function addListener(element, eventType, handler, options) {
|
function addListener(element, eventType, handler, options) {
|
||||||
if (!element) return null;
|
if (!element) {
|
||||||
|
console.warn(`[面包屑尝试为不存在的元素添加事件`);
|
||||||
// 确保先移除可能已存在的同类型事件处理函数
|
return null;
|
||||||
if (eventType === 'click' && element.classList.contains('back-button')) {
|
|
||||||
if (element._clickHandler) {
|
|
||||||
element.removeEventListener('click', element._clickHandler);
|
|
||||||
}
|
|
||||||
element._clickHandler = handler;
|
|
||||||
|
|
||||||
// 保存到数组中以便清理
|
|
||||||
if (!element.__backButtonClickHandlers) {
|
|
||||||
element.__backButtonClickHandlers = [];
|
|
||||||
}
|
|
||||||
element.__backButtonClickHandlers.push(handler);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
element.addEventListener(eventType, handler, options);
|
element.addEventListener(eventType, handler, options);
|
||||||
listeners.push({ element, eventType, handler, options });
|
allListeners.push({ element, eventType, handler, options });
|
||||||
return handler;
|
return handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理函数 - 移除所有事件监听器
|
// 统一的清理函数,执行完整清理并自销毁
|
||||||
function cleanup() {
|
function selfDestruct() {
|
||||||
// 先直接从按钮清理事件
|
// 1. 移除所有普通事件监听器
|
||||||
cleanupButtonListeners();
|
allListeners.forEach(({ element, eventType, handler, options }) => {
|
||||||
|
|
||||||
// 移除所有监听器
|
|
||||||
listeners.forEach(({ element, eventType, handler, options }) => {
|
|
||||||
try {
|
try {
|
||||||
element.removeEventListener(eventType, handler, options);
|
element.removeEventListener(eventType, handler, options);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 忽略错误
|
console.error(`[面包屑移除事件监听器出错:`, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 清空数组
|
// 清空监听器数组
|
||||||
listeners.length = 0;
|
allListeners.length = 0;
|
||||||
|
|
||||||
|
// 2. 最后移除清理事件监听器自身
|
||||||
|
cleanupListeners.forEach(({ element, eventType, handler, options }) => {
|
||||||
|
try {
|
||||||
|
element.removeEventListener(eventType, handler, options);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[面包屑移除清理监听器出错:`, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清空清理监听器数组
|
||||||
|
cleanupListeners.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置返回按钮事件
|
// 注册清理事件,并保存引用
|
||||||
function setupBackButton() {
|
function registerCleanupEvents() {
|
||||||
// 确保当前没有活动的返回按钮事件
|
// 创建一次性事件处理函数
|
||||||
cleanup();
|
const beforeSwapHandler = () => {
|
||||||
|
selfDestruct();
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeUnloadHandler = () => {
|
||||||
|
selfDestruct();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加清理事件监听器并保存引用
|
||||||
|
document.addEventListener("astro:before-swap", beforeSwapHandler, { once: true });
|
||||||
|
window.addEventListener("beforeunload", beforeUnloadHandler, { once: true });
|
||||||
|
|
||||||
|
// 如果页面使用swup,也注册swup相关的清理事件
|
||||||
|
if (typeof window.swup !== 'undefined') {
|
||||||
|
document.addEventListener("swup:willReplaceContent", beforeSwapHandler, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存清理事件引用,用于完全销毁
|
||||||
|
cleanupListeners.push(
|
||||||
|
{ element: document, eventType: "astro:before-swap", handler: beforeSwapHandler, options: { once: true } },
|
||||||
|
{ element: window, eventType: "beforeunload", handler: beforeUnloadHandler, options: { once: true } }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof window.swup !== 'undefined') {
|
||||||
|
cleanupListeners.push(
|
||||||
|
{ element: document, eventType: "swup:willReplaceContent", handler: beforeSwapHandler, options: { once: true } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置返回按钮功能
|
||||||
|
function setupBackButton() {
|
||||||
const backButton = document.querySelector('.back-button');
|
const backButton = document.querySelector('.back-button');
|
||||||
|
|
||||||
if (!backButton) {
|
if (!backButton) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
backButton.style.pointerEvents = 'auto';
|
|
||||||
} catch (e) {
|
|
||||||
// 忽略样式错误
|
|
||||||
}
|
|
||||||
|
|
||||||
const clickHandler = (e) => {
|
const clickHandler = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@ -301,100 +303,43 @@ const breadcrumbs: Breadcrumb[] = pathSegments
|
|||||||
addListener(backButton, 'click', clickHandler);
|
addListener(backButton, 'click', clickHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册清理函数 - 确保在每次页面转换前清理事件
|
// 主初始化函数
|
||||||
function registerCleanup() {
|
function init() {
|
||||||
const cleanupEvents = [
|
// 注册清理事件
|
||||||
'astro:before-preparation',
|
registerCleanupEvents();
|
||||||
'astro:before-swap',
|
|
||||||
'astro:beforeload',
|
|
||||||
'swup:willReplaceContent'
|
|
||||||
];
|
|
||||||
|
|
||||||
// 为每个事件注册一次性清理函数
|
// 设置返回按钮
|
||||||
cleanupEvents.forEach(eventName => {
|
setupBackButton();
|
||||||
const handler = () => {
|
|
||||||
cleanup();
|
// 注册页面加载后的处理函数 - 仅当使用View Transitions或Swup时
|
||||||
|
if (typeof document.startViewTransition !== 'undefined' || typeof window.swup !== 'undefined') {
|
||||||
|
// 仅监听一个事件,保持最小侵入性
|
||||||
|
const pageLoadHandler = () => {
|
||||||
|
// 检查是否在文章页面
|
||||||
|
const backButton = document.querySelector('.back-button');
|
||||||
|
if (backButton) {
|
||||||
|
// 先销毁已有的所有处理函数
|
||||||
|
selfDestruct();
|
||||||
|
// 重新初始化
|
||||||
|
init();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener(eventName, handler, { once: true });
|
if (typeof document.startViewTransition !== 'undefined') {
|
||||||
});
|
addListener(document, 'astro:page-load', pageLoadHandler);
|
||||||
|
|
||||||
// 页面卸载时清理
|
|
||||||
window.addEventListener('beforeunload', () => {
|
|
||||||
cleanup();
|
|
||||||
}, { once: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化函数
|
if (typeof window.swup !== 'undefined') {
|
||||||
function init() {
|
addListener(document, 'swup:contentReplaced', pageLoadHandler);
|
||||||
pageNavigationCount++;
|
}
|
||||||
setupBackButton();
|
}
|
||||||
registerCleanup();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听页面转换事件
|
// 判断DOM是否已加载
|
||||||
function setupPageTransitionEvents() {
|
if (document.readyState === "loading") {
|
||||||
// 确保事件处理程序唯一性的函数
|
document.addEventListener("DOMContentLoaded", init, { once: true });
|
||||||
function setupUniqueEvent(eventName, callback) {
|
|
||||||
const eventKey = `__back_button_event_${eventName.replace(/:/g, '_')}`;
|
|
||||||
|
|
||||||
// 移除可能存在的旧处理函数
|
|
||||||
if (window[eventKey]) {
|
|
||||||
document.removeEventListener(eventName, window[eventKey]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存新处理函数并注册
|
|
||||||
window[eventKey] = callback;
|
|
||||||
document.addEventListener(eventName, window[eventKey]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面转换后事件
|
|
||||||
const pageTransitionEvents = [
|
|
||||||
{ name: 'astro:after-swap', delay: 10 },
|
|
||||||
{ name: 'astro:page-load', delay: 10 },
|
|
||||||
{ name: 'swup:contentReplaced', delay: 10 }
|
|
||||||
];
|
|
||||||
|
|
||||||
// 设置每个页面转换事件
|
|
||||||
pageTransitionEvents.forEach(({ name, delay }) => {
|
|
||||||
setupUniqueEvent(name, () => {
|
|
||||||
cleanupButtonListeners(); // 立即清理按钮上的事件
|
|
||||||
|
|
||||||
// 延迟初始化,确保DOM完全更新
|
|
||||||
setTimeout(() => {
|
|
||||||
cleanupButtonListeners(); // 再次清理,确保没有遗漏
|
|
||||||
init();
|
|
||||||
}, delay);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 特别处理 swup:pageView 事件
|
|
||||||
setupUniqueEvent('swup:pageView', () => {
|
|
||||||
// 对于偶数次页面跳转,特别确保事件被正确重新绑定
|
|
||||||
if (pageNavigationCount % 2 === 0) {
|
|
||||||
setTimeout(() => {
|
|
||||||
const buttons = document.querySelectorAll('.back-button');
|
|
||||||
if (buttons.length > 0) {
|
|
||||||
cleanupButtonListeners();
|
|
||||||
setupBackButton();
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置页面转换事件监听
|
|
||||||
setupPageTransitionEvents();
|
|
||||||
|
|
||||||
// 在页面加载后初始化
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
init();
|
|
||||||
}, { once: true });
|
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => {
|
|
||||||
init();
|
init();
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
@ -332,8 +332,85 @@ const navSelectorClassName = "mr-4";
|
|||||||
|
|
||||||
|
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
// 自执行函数封装所有导航逻辑
|
// 导航逻辑 - 自销毁模式
|
||||||
(function initNavSelector() {
|
(function() {
|
||||||
|
// 尽早检查导航元素是否存在,如果不存在则终止执行
|
||||||
|
const navSelector = document.querySelector('.nav-selector');
|
||||||
|
if (!navSelector) {
|
||||||
|
console.warn(`导航脚本未找到导航选择器元素,不执行导航脚本`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 集中管理所有事件监听器
|
||||||
|
const allListeners = [];
|
||||||
|
|
||||||
|
// 单独保存清理事件的监听器引用
|
||||||
|
const cleanupListeners = [];
|
||||||
|
|
||||||
|
// 添加事件监听器并记录,方便后续统一清理
|
||||||
|
function addListener(element, eventType, handler, options) {
|
||||||
|
if (!element) {
|
||||||
|
console.warn(`导航脚本尝试为不存在的元素添加事件:`, eventType);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.addEventListener(eventType, handler, options);
|
||||||
|
allListeners.push({ element, eventType, handler, options });
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的清理函数,执行完整清理并自销毁
|
||||||
|
function selfDestruct() {
|
||||||
|
|
||||||
|
// 1. 移除所有普通事件监听器
|
||||||
|
allListeners.forEach(({ element, eventType, handler, options }) => {
|
||||||
|
try {
|
||||||
|
element.removeEventListener(eventType, handler, options);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`导航脚本移除事件监听器出错:`, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清空监听器数组
|
||||||
|
allListeners.length = 0;
|
||||||
|
|
||||||
|
// 2. 最后移除清理事件监听器自身
|
||||||
|
cleanupListeners.forEach(({ element, eventType, handler, options }) => {
|
||||||
|
try {
|
||||||
|
element.removeEventListener(eventType, handler, options);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`导航脚本移除清理监听器出错:`, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清空清理监听器数组
|
||||||
|
cleanupListeners.length = 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册清理事件,并保存引用
|
||||||
|
function registerCleanupEvents() {
|
||||||
|
|
||||||
|
// 创建一次性事件处理函数
|
||||||
|
const beforeSwapHandler = () => {
|
||||||
|
selfDestruct();
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeUnloadHandler = () => {
|
||||||
|
selfDestruct();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加清理事件监听器并保存引用
|
||||||
|
document.addEventListener("astro:before-swap", beforeSwapHandler, { once: true });
|
||||||
|
window.addEventListener("beforeunload", beforeUnloadHandler, { once: true });
|
||||||
|
|
||||||
|
// 保存清理事件引用,用于完全销毁
|
||||||
|
cleanupListeners.push(
|
||||||
|
{ element: document, eventType: "astro:before-swap", handler: beforeSwapHandler, options: { once: true } },
|
||||||
|
{ element: window, eventType: "beforeunload", handler: beforeUnloadHandler, options: { once: true } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 创建一个共享的计算高亮位置的函数
|
// 创建一个共享的计算高亮位置的函数
|
||||||
function calculateHighlightPositions(options = {}) {
|
function calculateHighlightPositions(options = {}) {
|
||||||
const { navSelector, immediate = false } = options;
|
const { navSelector, immediate = false } = options;
|
||||||
@ -537,7 +614,6 @@ const navSelectorClassName = "mr-4";
|
|||||||
|
|
||||||
// 在DOMContentLoaded前预先执行一次定位,减少闪烁
|
// 在DOMContentLoaded前预先执行一次定位,减少闪烁
|
||||||
(function prePositionHighlights() {
|
(function prePositionHighlights() {
|
||||||
const navSelector = document.querySelector('.nav-selector');
|
|
||||||
if (!navSelector) return;
|
if (!navSelector) return;
|
||||||
|
|
||||||
// 如果有活动项,尝试预先定位高亮
|
// 如果有活动项,尝试预先定位高亮
|
||||||
@ -553,35 +629,33 @@ const navSelectorClassName = "mr-4";
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// DOM加载完成后执行
|
// 注册清理事件
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
registerCleanupEvents();
|
||||||
// 存储所有事件监听器,便于统一清理
|
|
||||||
const listeners = [];
|
|
||||||
|
|
||||||
// 添加事件监听器并记录,方便后续统一清理
|
// DOM加载完成后执行初始化
|
||||||
function addListener(element, eventType, handler, options) {
|
function initNavigation() {
|
||||||
if (!element) return null;
|
// 检查DOM是否已加载
|
||||||
element.addEventListener(eventType, handler, options);
|
if (document.readyState === "loading") {
|
||||||
listeners.push({ element, eventType, handler, options });
|
document.addEventListener("DOMContentLoaded", setupNavigation, { once: true });
|
||||||
return handler;
|
} else {
|
||||||
|
setupNavigation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理函数 - 移除所有事件监听器
|
// 主要设置函数
|
||||||
function cleanup() {
|
function setupNavigation() {
|
||||||
listeners.forEach(({ element, eventType, handler, options }) => {
|
|
||||||
try {
|
// 设置桌面导航
|
||||||
element.removeEventListener(eventType, handler, options);
|
setupNavSelector();
|
||||||
} catch (err) {
|
|
||||||
console.error('移除导航事件监听器出错:', err);
|
// 设置移动端导航
|
||||||
}
|
setupMobileNav();
|
||||||
});
|
|
||||||
listeners.length = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化导航选择器
|
// 初始化导航选择器
|
||||||
function setupNavSelector() {
|
function setupNavSelector() {
|
||||||
// 获取DOM元素
|
// 获取DOM元素
|
||||||
const navSelector = document.querySelector('.nav-selector');
|
|
||||||
if (!navSelector) return;
|
if (!navSelector) return;
|
||||||
|
|
||||||
const primaryHighlight = document.getElementById('nav-primary-highlight');
|
const primaryHighlight = document.getElementById('nav-primary-highlight');
|
||||||
@ -591,6 +665,10 @@ const navSelectorClassName = "mr-4";
|
|||||||
const navToggles = document.querySelectorAll('.nav-group-toggle');
|
const navToggles = document.querySelectorAll('.nav-group-toggle');
|
||||||
const navSubItems = document.querySelectorAll('.nav-subitem');
|
const navSubItems = document.querySelectorAll('.nav-subitem');
|
||||||
|
|
||||||
|
if (!primaryHighlight || !secondaryHighlight) {
|
||||||
|
console.warn(`导航脚本未找到高亮元素,导航功能可能不完整`);
|
||||||
|
}
|
||||||
|
|
||||||
// 获取过渡动画持续时间
|
// 获取过渡动画持续时间
|
||||||
const transitionDuration = parseInt(navSelector.dataset.duration || 300);
|
const transitionDuration = parseInt(navSelector.dataset.duration || 300);
|
||||||
const activeClass = "font-medium";
|
const activeClass = "font-medium";
|
||||||
@ -658,7 +736,9 @@ const navSelectorClassName = "mr-4";
|
|||||||
// 兼容swup的导航方法
|
// 兼容swup的导航方法
|
||||||
function navigateTo(href) {
|
function navigateTo(href) {
|
||||||
// 如果使用swup
|
// 如果使用swup
|
||||||
if (window.swup) {
|
const hasSwup = typeof window.swup !== 'undefined';
|
||||||
|
|
||||||
|
if (hasSwup) {
|
||||||
try {
|
try {
|
||||||
// 正确使用swup的API
|
// 正确使用swup的API
|
||||||
window.swup.navigate(href);
|
window.swup.navigate(href);
|
||||||
@ -671,7 +751,7 @@ const navSelectorClassName = "mr-4";
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
} catch (err2) {
|
} catch (err2) {
|
||||||
console.error("Swup替代导航也出错:", err2);
|
console.warn(`导航脚本Swup导航失败:`, err2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1009,64 +1089,6 @@ const navSelectorClassName = "mr-4";
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找匹配当前路径的导航项
|
|
||||||
function findMatchingNavItem() {
|
|
||||||
const currentPath = window.location.pathname;
|
|
||||||
const normalizedPath = currentPath === '/' ? '/' :
|
|
||||||
currentPath.endsWith('/') ? currentPath.slice(0, -1) : currentPath;
|
|
||||||
|
|
||||||
// 首先检查子项
|
|
||||||
for (const subItem of navSubItems) {
|
|
||||||
const href = subItem.getAttribute('href');
|
|
||||||
if (href === normalizedPath) {
|
|
||||||
return {
|
|
||||||
type: 'subitem',
|
|
||||||
subitemId: subItem.dataset.subitemId,
|
|
||||||
parentId: subItem.dataset.parentId,
|
|
||||||
element: subItem
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 再检查主导航项
|
|
||||||
for (const item of navItems) {
|
|
||||||
const href = item.getAttribute('href');
|
|
||||||
if (href === normalizedPath) {
|
|
||||||
return {
|
|
||||||
type: 'item',
|
|
||||||
itemId: item.dataset.itemId,
|
|
||||||
element: item
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 没有完全匹配,尝试前缀匹配(用于文章详情等页面)
|
|
||||||
for (const subItem of navSubItems) {
|
|
||||||
const href = subItem.getAttribute('href');
|
|
||||||
if (normalizedPath.startsWith(href) && href !== '/') {
|
|
||||||
return {
|
|
||||||
type: 'subitem',
|
|
||||||
subitemId: subItem.dataset.subitemId,
|
|
||||||
parentId: subItem.dataset.parentId,
|
|
||||||
element: subItem
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const item of navItems) {
|
|
||||||
const href = item.getAttribute('href');
|
|
||||||
if (normalizedPath.startsWith(href) && href !== '/') {
|
|
||||||
return {
|
|
||||||
type: 'item',
|
|
||||||
itemId: item.dataset.itemId,
|
|
||||||
element: item
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化当前页面的激活状态
|
// 初始化当前页面的激活状态
|
||||||
function initActiveState() {
|
function initActiveState() {
|
||||||
// 高亮背景已经在服务器端渲染时预设,现在需确保应用正确的文字颜色
|
// 高亮背景已经在服务器端渲染时预设,现在需确保应用正确的文字颜色
|
||||||
@ -1154,18 +1176,16 @@ const navSelectorClassName = "mr-4";
|
|||||||
initActiveState();
|
initActiveState();
|
||||||
|
|
||||||
// 处理页面切换
|
// 处理页面切换
|
||||||
if (window.swup) {
|
const hasSwup = typeof window.swup !== 'undefined';
|
||||||
|
if (hasSwup) {
|
||||||
// 页面内容替换后重新初始化
|
// 页面内容替换后重新初始化
|
||||||
addListener(document, 'swup:contentReplaced', () => {
|
addListener(document, 'swup:contentReplaced', () => {
|
||||||
setTimeout(initActiveState, 50);
|
setTimeout(initActiveState, 50);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 页面内容替换前保存当前状态
|
|
||||||
addListener(document, 'swup:willReplaceContent', () => {
|
|
||||||
});
|
|
||||||
|
|
||||||
// 视图转换开始
|
// 视图转换开始
|
||||||
addListener(document, 'swup:animationInStart', () => {
|
addListener(document, 'swup:animationInStart', () => {
|
||||||
|
// 这里可以添加动画开始时的处理逻辑
|
||||||
});
|
});
|
||||||
|
|
||||||
// 视图转换结束
|
// 视图转换结束
|
||||||
@ -1177,50 +1197,23 @@ const navSelectorClassName = "mr-4";
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加Astro View Transitions事件监听
|
||||||
|
addListener(document, 'astro:page-load', () => {
|
||||||
|
setTimeout(initActiveState, 50);
|
||||||
|
updateHighlights(true);
|
||||||
|
});
|
||||||
|
|
||||||
// 窗口大小变化时重新计算高亮位置
|
// 窗口大小变化时重新计算高亮位置
|
||||||
addListener(window, 'resize', () => {
|
addListener(window, 'resize', () => {
|
||||||
updateHighlights(true);
|
updateHighlights(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 在document上添加自定义方法,方便外部调用
|
// 在document上添加自定义方法,方便外部调用,但使用弱引用避免内存泄漏
|
||||||
|
if (!document.resetNavigation) {
|
||||||
document.resetNavigation = function() {
|
document.resetNavigation = function() {
|
||||||
initActiveState();
|
initActiveState();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 动态检测swup实例
|
|
||||||
function checkSwup() {
|
|
||||||
if (!window.swup && typeof MutationObserver !== 'undefined') {
|
|
||||||
// 监听DOM变化,检查swup实例是否被后续加载
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
if (window.swup) {
|
|
||||||
// 添加swup事件监听
|
|
||||||
addListener(document, 'swup:contentReplaced', () => {
|
|
||||||
setTimeout(initActiveState, 50);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 停止观察
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// 开始观察文档
|
|
||||||
observer.observe(document.documentElement, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// 5秒后自动停止观察,避免长时间监听
|
|
||||||
setTimeout(() => {
|
|
||||||
observer.disconnect();
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检测swup实例
|
|
||||||
checkSwup();
|
|
||||||
|
|
||||||
// 返回清理函数
|
|
||||||
return cleanup;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化移动端菜单和搜索功能
|
// 初始化移动端菜单和搜索功能
|
||||||
@ -1232,8 +1225,14 @@ const navSelectorClassName = "mr-4";
|
|||||||
const menuOpenIcon = document.getElementById('menu-open-icon');
|
const menuOpenIcon = document.getElementById('menu-open-icon');
|
||||||
const menuCloseIcon = document.getElementById('menu-close-icon');
|
const menuCloseIcon = document.getElementById('menu-close-icon');
|
||||||
|
|
||||||
|
if (!mobileMenuButton || !mobileMenu) {
|
||||||
|
console.warn(`导航脚本未找到移动端菜单组件`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 更新移动端菜单高亮状态的函数
|
// 更新移动端菜单高亮状态的函数
|
||||||
function updateMobileMenuHighlight() {
|
function updateMobileMenuHighlight() {
|
||||||
|
|
||||||
// 获取当前路径
|
// 获取当前路径
|
||||||
const currentPath = window.location.pathname;
|
const currentPath = window.location.pathname;
|
||||||
const normalizedPath = currentPath === '/' ? '/' :
|
const normalizedPath = currentPath === '/' ? '/' :
|
||||||
@ -1257,7 +1256,7 @@ const navSelectorClassName = "mr-4";
|
|||||||
const parentId = submenu.getAttribute('data-parent-id');
|
const parentId = submenu.getAttribute('data-parent-id');
|
||||||
const toggle = document.querySelector(`[data-mobile-menu-toggle="${parentId}"]`);
|
const toggle = document.querySelector(`[data-mobile-menu-toggle="${parentId}"]`);
|
||||||
if (toggle && submenu.classList.contains('hidden')) {
|
if (toggle && submenu.classList.contains('hidden')) {
|
||||||
// 模拟点击父菜单,展开子菜单
|
// 展开子菜单
|
||||||
toggleSubmenu(parentId, true);
|
toggleSubmenu(parentId, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1303,6 +1302,11 @@ const navSelectorClassName = "mr-4";
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 为所有子菜单切换按钮添加点击事件
|
||||||
|
function setupMobileSubmenuToggles() {
|
||||||
|
// 关闭所有子菜单(默认状态)
|
||||||
|
closeAllSubmenus();
|
||||||
|
|
||||||
// 为所有子菜单切换按钮添加点击事件
|
// 为所有子菜单切换按钮添加点击事件
|
||||||
const submenuToggles = document.querySelectorAll('[data-mobile-menu-toggle]');
|
const submenuToggles = document.querySelectorAll('[data-mobile-menu-toggle]');
|
||||||
submenuToggles.forEach(toggle => {
|
submenuToggles.forEach(toggle => {
|
||||||
@ -1313,6 +1317,7 @@ const navSelectorClassName = "mr-4";
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 关闭移动端菜单
|
// 关闭移动端菜单
|
||||||
function closeMobileMenu() {
|
function closeMobileMenu() {
|
||||||
@ -1345,9 +1350,7 @@ const navSelectorClassName = "mr-4";
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始调用一次更新高亮状态(会自动展开包含当前页面的子菜单)
|
// 移动端菜单按钮点击事件
|
||||||
updateMobileMenuHighlight();
|
|
||||||
|
|
||||||
if (mobileMenuButton && mobileMenu) {
|
if (mobileMenuButton && mobileMenu) {
|
||||||
addListener(mobileMenuButton, 'click', () => {
|
addListener(mobileMenuButton, 'click', () => {
|
||||||
// 切换菜单显示状态
|
// 切换菜单显示状态
|
||||||
@ -1373,6 +1376,7 @@ const navSelectorClassName = "mr-4";
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 移动端搜索按钮点击事件
|
||||||
if (mobileSearchButton && mobileSearch) {
|
if (mobileSearchButton && mobileSearch) {
|
||||||
addListener(mobileSearchButton, 'click', () => {
|
addListener(mobileSearchButton, 'click', () => {
|
||||||
// 切换搜索面板显示状态
|
// 切换搜索面板显示状态
|
||||||
@ -1394,12 +1398,16 @@ const navSelectorClassName = "mr-4";
|
|||||||
mobileMenuLinks.forEach(link => {
|
mobileMenuLinks.forEach(link => {
|
||||||
addListener(link, 'click', (e) => {
|
addListener(link, 'click', (e) => {
|
||||||
// 如果使用客户端路由(如swup或Astro View Transitions),阻止默认行为
|
// 如果使用客户端路由(如swup或Astro View Transitions),阻止默认行为
|
||||||
if (window.swup || document.startViewTransition) {
|
const hasSwup = typeof window.swup !== 'undefined';
|
||||||
|
const hasViewTransitions = typeof document.startViewTransition !== 'undefined';
|
||||||
|
|
||||||
|
if (hasSwup || hasViewTransitions) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// 获取链接地址
|
// 获取链接地址
|
||||||
const href = link.getAttribute('href');
|
const href = link.getAttribute('href');
|
||||||
if (href) {
|
if (!href) return;
|
||||||
|
|
||||||
// 先关闭菜单
|
// 先关闭菜单
|
||||||
closeMobileMenu();
|
closeMobileMenu();
|
||||||
closeMobileSearch();
|
closeMobileSearch();
|
||||||
@ -1407,7 +1415,7 @@ const navSelectorClassName = "mr-4";
|
|||||||
// 延迟导航,确保菜单关闭动画完成
|
// 延迟导航,确保菜单关闭动画完成
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 使用适当的导航方法
|
// 使用适当的导航方法
|
||||||
if (window.swup) {
|
if (hasSwup) {
|
||||||
try {
|
try {
|
||||||
window.swup.navigate(href);
|
window.swup.navigate(href);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -1417,7 +1425,7 @@ const navSelectorClassName = "mr-4";
|
|||||||
window.location.href = href;
|
window.location.href = href;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (document.startViewTransition) {
|
} else if (hasViewTransitions) {
|
||||||
document.startViewTransition(() => {
|
document.startViewTransition(() => {
|
||||||
window.location.href = href;
|
window.location.href = href;
|
||||||
});
|
});
|
||||||
@ -1425,7 +1433,6 @@ const navSelectorClassName = "mr-4";
|
|||||||
window.location.href = href;
|
window.location.href = href;
|
||||||
}
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 普通链接导航,浏览器会自动处理跳转
|
// 普通链接导航,浏览器会自动处理跳转
|
||||||
// 但仍然需要关闭菜单
|
// 但仍然需要关闭菜单
|
||||||
@ -1435,86 +1442,28 @@ const navSelectorClassName = "mr-4";
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理主题切换容器点击
|
// 为Astro View Transitions添加事件处理
|
||||||
const themeToggleContainer = document.getElementById('theme-toggle-container');
|
|
||||||
if (themeToggleContainer) {
|
|
||||||
addListener(themeToggleContainer, 'click', (e) => {
|
|
||||||
// 触发其中的ThemeToggle组件点击
|
|
||||||
const themeToggle = themeToggleContainer.querySelector('button');
|
|
||||||
if (themeToggle) {
|
|
||||||
themeToggle.click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理页面切换,确保移动端菜单高亮状态更新
|
|
||||||
if (window.swup) {
|
|
||||||
// swup页面内容替换后
|
|
||||||
addListener(document, 'swup:contentReplaced', () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
// 更新移动端菜单项的事件监听器,因为DOM已经更新
|
|
||||||
setupMobileSubmenuToggles();
|
|
||||||
// 更新高亮状态
|
|
||||||
updateMobileMenuHighlight();
|
|
||||||
// 确保菜单关闭
|
|
||||||
closeMobileMenu();
|
|
||||||
closeMobileSearch();
|
|
||||||
}, 50);
|
|
||||||
});
|
|
||||||
|
|
||||||
// swup动画结束后
|
|
||||||
addListener(document, 'swup:animationInDone', () => {
|
|
||||||
updateMobileMenuHighlight();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果使用Astro的View Transitions
|
|
||||||
addListener(document, 'astro:page-load', () => {
|
addListener(document, 'astro:page-load', () => {
|
||||||
// 更新移动端菜单项的事件监听器,因为DOM已经更新
|
|
||||||
setupMobileSubmenuToggles();
|
setupMobileSubmenuToggles();
|
||||||
// 更新高亮状态
|
|
||||||
updateMobileMenuHighlight();
|
updateMobileMenuHighlight();
|
||||||
// 确保菜单关闭
|
|
||||||
closeMobileMenu();
|
closeMobileMenu();
|
||||||
closeMobileSearch();
|
closeMobileSearch();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 普通页面加载事件
|
// 监听地址栏变化
|
||||||
addListener(window, 'popstate', () => {
|
addListener(window, 'popstate', () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 更新移动端菜单项的事件监听器,因为DOM可能已经更新
|
|
||||||
setupMobileSubmenuToggles();
|
|
||||||
// 更新高亮状态
|
|
||||||
updateMobileMenuHighlight();
|
updateMobileMenuHighlight();
|
||||||
// 确保菜单关闭
|
|
||||||
closeMobileMenu();
|
closeMobileMenu();
|
||||||
closeMobileSearch();
|
closeMobileSearch();
|
||||||
}, 50);
|
}, 50);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 辅助函数:为移动端子菜单添加切换事件(用于页面切换后重新绑定)
|
// 初始调用一次设置子菜单切换按钮和更新高亮状态
|
||||||
function setupMobileSubmenuToggles() {
|
setupMobileSubmenuToggles();
|
||||||
// 关闭所有子菜单(默认状态)
|
updateMobileMenuHighlight();
|
||||||
closeAllSubmenus();
|
|
||||||
|
|
||||||
// 重新添加事件监听器
|
// 在document上添加自定义方法,方便外部调用(可选)
|
||||||
const submenuToggles = document.querySelectorAll('[data-mobile-menu-toggle]');
|
|
||||||
submenuToggles.forEach(toggle => {
|
|
||||||
// 移除可能存在的旧监听器(通过克隆和替换元素)
|
|
||||||
const newToggle = toggle.cloneNode(true);
|
|
||||||
toggle.parentNode.replaceChild(newToggle, toggle);
|
|
||||||
|
|
||||||
// 添加新的事件监听器
|
|
||||||
addListener(newToggle, 'click', (e) => {
|
|
||||||
const parentId = newToggle.getAttribute('data-mobile-menu-toggle');
|
|
||||||
if (parentId) {
|
|
||||||
toggleSubmenu(parentId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 暴露更新和关闭函数到全局,方便其他地方调用
|
|
||||||
document.updateMobileMenuHighlight = updateMobileMenuHighlight;
|
document.updateMobileMenuHighlight = updateMobileMenuHighlight;
|
||||||
document.closeMobileMenu = closeMobileMenu;
|
document.closeMobileMenu = closeMobileMenu;
|
||||||
document.closeMobileSearch = closeMobileSearch;
|
document.closeMobileSearch = closeMobileSearch;
|
||||||
@ -1523,13 +1472,8 @@ const navSelectorClassName = "mr-4";
|
|||||||
return updateMobileMenuHighlight;
|
return updateMobileMenuHighlight;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行初始化
|
// 开始初始化
|
||||||
const cleanupNav = setupNavSelector();
|
initNavigation();
|
||||||
const updateMobileHighlight = setupMobileNav();
|
|
||||||
|
|
||||||
// 页面卸载时清理
|
|
||||||
window.addEventListener('beforeunload', cleanupNav);
|
|
||||||
});
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -204,7 +204,6 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
|
|
||||||
// 如果是取消的请求,不显示错误
|
// 如果是取消的请求,不显示错误
|
||||||
if (err instanceof Error && (err.name === 'AbortError' || err.message.includes('aborted'))) {
|
if (err instanceof Error && (err.name === 'AbortError' || err.message.includes('aborted'))) {
|
||||||
console.log('索引加载请求被取消:', err.message);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,7 +238,7 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize, { passive: true });
|
window.addEventListener('resize', handleResize, { passive: false });
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', handleResize);
|
window.removeEventListener('resize', handleResize);
|
||||||
};
|
};
|
||||||
@ -248,18 +247,35 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
// 处理点击外部关闭搜索结果和建议
|
// 处理点击外部关闭搜索结果和建议
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
// 获取事件目标元素
|
||||||
|
const target = event.target as Node;
|
||||||
|
|
||||||
|
// 检查是否点击了清除按钮、Tab按钮或其子元素
|
||||||
|
const clearButtonEl = document.querySelector('.clear-search-button');
|
||||||
|
const tabButtonEl = document.querySelector('.tab-completion-button');
|
||||||
|
|
||||||
|
const isClickOnClearButton = clearButtonEl && (clearButtonEl === target || clearButtonEl.contains(target));
|
||||||
|
const isClickOnTabButton = tabButtonEl && (tabButtonEl === target || tabButtonEl.contains(target));
|
||||||
|
|
||||||
|
// 如果点击了清除按钮或Tab按钮,不做任何操作
|
||||||
|
if (isClickOnClearButton || isClickOnTabButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 原有的逻辑:点击搜索框和结果区域之外时关闭
|
||||||
if (
|
if (
|
||||||
searchResultsRef.current &&
|
searchResultsRef.current &&
|
||||||
!searchResultsRef.current.contains(event.target as Node) &&
|
!searchResultsRef.current.contains(target) &&
|
||||||
searchInputRef.current &&
|
searchInputRef.current &&
|
||||||
!searchInputRef.current.contains(event.target as Node)
|
!searchInputRef.current.contains(target)
|
||||||
) {
|
) {
|
||||||
|
// 当点击搜索框和结果区域之外时,才隐藏结果
|
||||||
setShowResults(false);
|
setShowResults(false);
|
||||||
setInlineSuggestion(prev => ({ ...prev, visible: false })); // 也隐藏内联建议
|
setInlineSuggestion(prev => ({ ...prev, visible: false })); // 也隐藏内联建议
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside, { passive: true });
|
document.addEventListener("mousedown", handleClickOutside, { passive: false });
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
};
|
};
|
||||||
@ -299,6 +315,7 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
setInlineSuggestion(prev => ({ ...prev, visible: false }));
|
setInlineSuggestion(prev => ({ ...prev, visible: false }));
|
||||||
setSelectedSuggestionIndex(0); // 重置选中索引
|
setSelectedSuggestionIndex(0); // 重置选中索引
|
||||||
|
console.log("[建议] 没有符合条件的查询或模块未加载");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,6 +342,7 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
|
|
||||||
const searchResult = JSON.parse(result) as SearchResult;
|
const searchResult = JSON.parse(result) as SearchResult;
|
||||||
|
|
||||||
|
|
||||||
// 检查组件是否仍然挂载
|
// 检查组件是否仍然挂载
|
||||||
if (!isMountedRef.current) return;
|
if (!isMountedRef.current) return;
|
||||||
|
|
||||||
@ -347,6 +365,7 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
const firstSuggestion = searchResult.suggestions[0];
|
const firstSuggestion = searchResult.suggestions[0];
|
||||||
|
|
||||||
if (firstSuggestion) {
|
if (firstSuggestion) {
|
||||||
|
|
||||||
setInlineSuggestion(prev => ({
|
setInlineSuggestion(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
text: firstSuggestion.text,
|
text: firstSuggestion.text,
|
||||||
@ -363,7 +382,7 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
// 检查组件是否仍然挂载
|
// 检查组件是否仍然挂载
|
||||||
if (!isMountedRef.current) return;
|
if (!isMountedRef.current) return;
|
||||||
|
|
||||||
console.error("获取内联建议失败:", err);
|
console.error("[建议错误]", err);
|
||||||
setInlineSuggestion(prev => ({ ...prev, visible: false }));
|
setInlineSuggestion(prev => ({ ...prev, visible: false }));
|
||||||
setSelectedSuggestionIndex(0); // 重置选中索引
|
setSelectedSuggestionIndex(0); // 重置选中索引
|
||||||
}
|
}
|
||||||
@ -432,9 +451,11 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
|
|
||||||
// 修改处理键盘导航的函数,增加上下箭头键切换建议
|
// 修改处理键盘导航的函数,增加上下箭头键切换建议
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
|
||||||
// Tab键处理内联建议补全
|
// Tab键处理内联建议补全
|
||||||
if (e.key === "Tab" && inlineSuggestion.visible && inlineSuggestion.text) {
|
if (e.key === "Tab" && inlineSuggestion.visible && inlineSuggestion.text) {
|
||||||
e.preventDefault(); // 阻止默认的Tab行为
|
e.preventDefault(); // 阻止默认的Tab行为
|
||||||
|
e.stopPropagation(); // 防止事件冒泡
|
||||||
completeInlineSuggestion();
|
completeInlineSuggestion();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -532,50 +553,13 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('selectionchange', handleSelectionChange, { passive: true });
|
// 使用非被动模式,确保在某些上下文中可以调用preventDefault
|
||||||
|
document.addEventListener('selectionchange', handleSelectionChange, { passive: false });
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('selectionchange', handleSelectionChange);
|
document.removeEventListener('selectionchange', handleSelectionChange);
|
||||||
};
|
};
|
||||||
}, [updateCaretPosition]);
|
}, [updateCaretPosition]);
|
||||||
|
|
||||||
// 自动补全内联建议
|
|
||||||
const completeInlineSuggestion = () => {
|
|
||||||
if (inlineSuggestion.visible && inlineSuggestion.text) {
|
|
||||||
// 保存建议文本
|
|
||||||
const suggestionText = inlineSuggestion.text;
|
|
||||||
const isCorrection = inlineSuggestion.type === 'correction';
|
|
||||||
|
|
||||||
// 完全清除内联建议状态
|
|
||||||
setInlineSuggestion({
|
|
||||||
text: "",
|
|
||||||
visible: false,
|
|
||||||
caretPosition: 0,
|
|
||||||
selection: {start: 0, end: 0},
|
|
||||||
type: 'completion',
|
|
||||||
matchedText: "",
|
|
||||||
suggestionText: ""
|
|
||||||
});
|
|
||||||
|
|
||||||
// 设置完整的建议作为新的查询
|
|
||||||
setQuery(suggestionText);
|
|
||||||
|
|
||||||
// 将光标移到末尾并执行搜索
|
|
||||||
setTimeout(() => {
|
|
||||||
if (searchInputRef.current) {
|
|
||||||
searchInputRef.current.focus();
|
|
||||||
searchInputRef.current.setSelectionRange(
|
|
||||||
suggestionText.length,
|
|
||||||
suggestionText.length
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 直接使用suggestionText执行搜索,而不是依赖query状态
|
|
||||||
// 因为React状态更新是异步的,此时query可能还未更新
|
|
||||||
performSearch(suggestionText, false);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 执行搜索
|
// 执行搜索
|
||||||
const performSearch = async (searchQuery: string, isLoadMore: boolean = false) => {
|
const performSearch = async (searchQuery: string, isLoadMore: boolean = false) => {
|
||||||
if (!wasmModule || !isIndexLoaded || !indexData || !searchQuery.trim()) {
|
if (!wasmModule || !isIndexLoaded || !indexData || !searchQuery.trim()) {
|
||||||
@ -667,6 +651,46 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 自动补全内联建议 - 不使用useCallback,避免循环依赖
|
||||||
|
const completeInlineSuggestion = () => {
|
||||||
|
if (inlineSuggestion.visible && inlineSuggestion.text) {
|
||||||
|
// 保存建议文本
|
||||||
|
const textToComplete = inlineSuggestion.text;
|
||||||
|
|
||||||
|
// 直接更新DOM和状态
|
||||||
|
if (searchInputRef.current) {
|
||||||
|
// 立即更新输入框值
|
||||||
|
searchInputRef.current.value = textToComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除内联建议状态
|
||||||
|
setInlineSuggestion({
|
||||||
|
text: "",
|
||||||
|
visible: false,
|
||||||
|
caretPosition: 0,
|
||||||
|
selection: {start: 0, end: 0},
|
||||||
|
type: 'completion',
|
||||||
|
matchedText: "",
|
||||||
|
suggestionText: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新React状态
|
||||||
|
setQuery(textToComplete);
|
||||||
|
|
||||||
|
// 立即执行搜索
|
||||||
|
performSearch(textToComplete, false);
|
||||||
|
|
||||||
|
// 聚焦输入框并设置光标位置
|
||||||
|
if (searchInputRef.current) {
|
||||||
|
searchInputRef.current.focus();
|
||||||
|
searchInputRef.current.setSelectionRange(
|
||||||
|
textToComplete.length,
|
||||||
|
textToComplete.length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 高亮显示匹配文本 - 不再处理高亮,完全依赖后端
|
// 高亮显示匹配文本 - 不再处理高亮,完全依赖后端
|
||||||
const processHighlightedContent = (content: string) => {
|
const processHighlightedContent = (content: string) => {
|
||||||
// 检查内容是否为空
|
// 检查内容是否为空
|
||||||
@ -790,6 +814,59 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
suggestionEl.style.fontWeight = inputStyle.fontWeight;
|
suggestionEl.style.fontWeight = inputStyle.fontWeight;
|
||||||
suggestionEl.style.letterSpacing = inputStyle.letterSpacing;
|
suggestionEl.style.letterSpacing = inputStyle.letterSpacing;
|
||||||
suggestionEl.style.lineHeight = inputStyle.lineHeight;
|
suggestionEl.style.lineHeight = inputStyle.lineHeight;
|
||||||
|
|
||||||
|
// 计算并调整可用空间
|
||||||
|
if (inlineSuggestion.type === 'correction') {
|
||||||
|
// 获取输入框宽度
|
||||||
|
const inputWidth = searchInputRef.current!.offsetWidth;
|
||||||
|
// 估算查询文本宽度 (使用更精确的字体宽度估算方法)
|
||||||
|
// 创建一个临时元素用于测量实际宽度
|
||||||
|
const tempMeasureEl = document.createElement('span');
|
||||||
|
tempMeasureEl.style.visibility = 'hidden';
|
||||||
|
tempMeasureEl.style.position = 'absolute';
|
||||||
|
tempMeasureEl.style.whiteSpace = 'pre';
|
||||||
|
tempMeasureEl.style.fontSize = inputStyle.fontSize;
|
||||||
|
tempMeasureEl.style.fontFamily = inputStyle.fontFamily;
|
||||||
|
tempMeasureEl.style.fontWeight = inputStyle.fontWeight;
|
||||||
|
tempMeasureEl.style.letterSpacing = inputStyle.letterSpacing;
|
||||||
|
tempMeasureEl.innerText = query;
|
||||||
|
document.body.appendChild(tempMeasureEl);
|
||||||
|
const queryTextWidth = tempMeasureEl.offsetWidth;
|
||||||
|
document.body.removeChild(tempMeasureEl);
|
||||||
|
|
||||||
|
// 计算右侧边距 (确保TAB按钮和清除按钮有足够空间)
|
||||||
|
// 根据屏幕尺寸调整右侧边距
|
||||||
|
let rightMargin = 90; // 默认桌面环境
|
||||||
|
|
||||||
|
// 根据窗口宽度调整边距(响应式设计)
|
||||||
|
if (window.innerWidth < 640) { // 小屏幕设备
|
||||||
|
rightMargin = 100; // 移动设备上按钮占据更多相对空间
|
||||||
|
} else if (window.innerWidth < 768) { // 中等屏幕设备
|
||||||
|
rightMargin = 95;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算建议可用最大宽度
|
||||||
|
// 根据屏幕尺寸调整最大宽度百分比
|
||||||
|
let maxWidthPercentage = 0.8; // 默认最大宽度百分比
|
||||||
|
|
||||||
|
if (window.innerWidth < 640) {
|
||||||
|
maxWidthPercentage = 0.7; // 在小屏幕上减少最大宽度百分比
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxAllowedWidth = Math.floor(inputWidth * maxWidthPercentage);
|
||||||
|
|
||||||
|
// 计算最终的可用宽度
|
||||||
|
const availableWidth = Math.min(
|
||||||
|
maxAllowedWidth,
|
||||||
|
Math.max(inputWidth - queryTextWidth - rightMargin, 80) // 最小宽度降低到80px以适应更小的设备
|
||||||
|
);
|
||||||
|
|
||||||
|
// 设置最大宽度
|
||||||
|
const suggestionTextContainer = suggestionEl.querySelector('div > div:nth-child(2) > span');
|
||||||
|
if (suggestionTextContainer) {
|
||||||
|
(suggestionTextContainer as HTMLElement).style.maxWidth = `${availableWidth}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
updateSuggestionStyles();
|
updateSuggestionStyles();
|
||||||
@ -802,14 +879,46 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [inlineSuggestion.visible, query]);
|
}, [inlineSuggestion.visible, query, inlineSuggestion.type]);
|
||||||
|
|
||||||
|
// 处理Tab键盘事件 - 简化逻辑,改进处理方式
|
||||||
|
useEffect(() => {
|
||||||
|
// 创建键盘事件处理函数
|
||||||
|
const handleTabKey = (e: KeyboardEvent) => {
|
||||||
|
const isFocused = document.activeElement === searchInputRef.current;
|
||||||
|
const hasVisibleSuggestion = inlineSuggestion.visible && inlineSuggestion.text;
|
||||||
|
|
||||||
|
if (e.key === 'Tab' && isFocused && hasVisibleSuggestion) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
completeInlineSuggestion();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加键盘事件监听器,确保使用非被动模式
|
||||||
|
document.addEventListener('keydown', handleTabKey, { passive: false, capture: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleTabKey, { capture: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
// 清除搜索
|
// 清除搜索
|
||||||
const clearSearch = () => {
|
const clearSearch = () => {
|
||||||
|
// 清除查询和结果,但保持搜索框的可见状态
|
||||||
setQuery("");
|
setQuery("");
|
||||||
|
if (searchInputRef.current) {
|
||||||
|
searchInputRef.current.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除搜索结果
|
||||||
setSearchResults(null);
|
setSearchResults(null);
|
||||||
setAllItems([]);
|
setAllItems([]);
|
||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
|
|
||||||
|
// 清除内联建议
|
||||||
setInlineSuggestion({
|
setInlineSuggestion({
|
||||||
text: "",
|
text: "",
|
||||||
visible: false,
|
visible: false,
|
||||||
@ -819,10 +928,13 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
matchedText: "",
|
matchedText: "",
|
||||||
suggestionText: ""
|
suggestionText: ""
|
||||||
});
|
});
|
||||||
setShowResults(false);
|
|
||||||
|
// 保持结果区域可见,但无内容
|
||||||
|
setShowResults(true);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
setHasMoreResults(true);
|
setHasMoreResults(true);
|
||||||
|
|
||||||
|
// 确保输入框保持焦点
|
||||||
if (searchInputRef.current) {
|
if (searchInputRef.current) {
|
||||||
searchInputRef.current.focus();
|
searchInputRef.current.focus();
|
||||||
}
|
}
|
||||||
@ -844,8 +956,8 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('focus', checkFocus, { passive: true, capture: true });
|
window.addEventListener('focus', checkFocus, { passive: false, capture: true });
|
||||||
window.addEventListener('blur', checkFocus, { passive: true, capture: true });
|
window.addEventListener('blur', checkFocus, { passive: false, capture: true });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('focus', checkFocus, { capture: true });
|
window.removeEventListener('focus', checkFocus, { capture: true });
|
||||||
@ -853,22 +965,6 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
};
|
};
|
||||||
}, [isFocused]);
|
}, [isFocused]);
|
||||||
|
|
||||||
// 处理Tab键盘事件
|
|
||||||
useEffect(() => {
|
|
||||||
const handleTabKey = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Tab' && document.activeElement === searchInputRef.current && inlineSuggestion.visible) {
|
|
||||||
e.preventDefault();
|
|
||||||
completeInlineSuggestion();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('keydown', handleTabKey);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('keydown', handleTabKey);
|
|
||||||
};
|
|
||||||
}, [inlineSuggestion.visible, completeInlineSuggestion]);
|
|
||||||
|
|
||||||
// 获取当前placeholder文本
|
// 获取当前placeholder文本
|
||||||
const getCurrentPlaceholder = () => {
|
const getCurrentPlaceholder = () => {
|
||||||
if (isLoadingIndex) {
|
if (isLoadingIndex) {
|
||||||
@ -1090,7 +1186,7 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
onSelect={updateCaretPosition}
|
onSelect={updateCaretPosition}
|
||||||
onFocus={handleInputFocus}
|
onFocus={handleInputFocus}
|
||||||
placeholder={getCurrentPlaceholder()}
|
placeholder={getCurrentPlaceholder()}
|
||||||
className="w-full py-1.5 md:py-1.5 lg:py-2.5 pl-8 md:pl-8 lg:pl-10 pr-8 md:pr-8 lg:pr-10 text-sm md:text-sm lg:text-base bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg md:rounded-lg lg:rounded-xl text-gray-800 dark:text-gray-200 focus:outline-none focus:ring-1 focus:ring-primary-500 focus:border-transparent focus:shadow-md transition-all duration-200 relative z-10"
|
className="w-full py-2.5 md:py-1.5 lg:py-2.5 pl-10 md:pl-8 lg:pl-10 pr-10 md:pr-8 lg:pr-10 text-base md:text-sm lg:text-base bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg md:rounded-lg lg:rounded-xl text-gray-800 dark:text-gray-200 focus:outline-none focus:ring-1 focus:ring-primary-500 focus:border-transparent focus:shadow-md transition-all duration-200 relative z-10"
|
||||||
disabled={isLoadingIndex || !isIndexLoaded}
|
disabled={isLoadingIndex || !isIndexLoaded}
|
||||||
style={{ backgroundColor: 'transparent' }} // 确保背景是透明的,这样可以看到下面的建议
|
style={{ backgroundColor: 'transparent' }} // 确保背景是透明的,这样可以看到下面的建议
|
||||||
/>
|
/>
|
||||||
@ -1106,25 +1202,37 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 修改显示方式,确保与输入文本对齐,同时支持响应式布局 */}
|
{/* 修改显示方式,确保与输入文本对齐,同时支持响应式布局 */}
|
||||||
<div className="flex w-full px-8 md:px-8 lg:px-10 overflow-hidden"> {/* 使用与输入框相同的水平内边距,添加溢出隐藏 */}
|
<div className="flex w-full px-10 md:px-8 lg:px-10 overflow-hidden"> {/* 使用与输入框相同的水平内边距,添加溢出隐藏 */}
|
||||||
{/* 纠正建议和补全建议都显示在已输入内容的右侧 */}
|
{/* 纠正建议和补全建议都显示在已输入内容的右侧 */}
|
||||||
<>
|
<>
|
||||||
{/* 创建与输入文本宽度完全相等的不可见占位 */}
|
{/* 创建与输入文本宽度完全相等的不可见占位 */}
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<span className="invisible whitespace-pre text-sm md:text-sm lg:text-base">{query}</span>
|
<span className="invisible whitespace-pre text-base md:text-sm lg:text-base">{query}</span>
|
||||||
</div>
|
</div>
|
||||||
{/* 显示建议的剩余部分 */}
|
{/* 显示建议的剩余部分 */}
|
||||||
<div className="flex-shrink-0 max-w-[70%]">
|
<div className={`flex-shrink-0 ${
|
||||||
<span
|
// 根据建议类型调整最大宽度
|
||||||
className={`whitespace-pre text-sm md:text-sm lg:text-base truncate block ${
|
|
||||||
inlineSuggestion.type === 'correction'
|
inlineSuggestion.type === 'correction'
|
||||||
? 'text-amber-500/80 dark:text-amber-400/80 ml-1'
|
? 'max-w-[calc(100%-1.25rem)]' // 纠正建议给予更多空间,但仍然保留一些边距
|
||||||
|
: 'max-w-[80%]' // 补全建议使用固定比例
|
||||||
|
}`}>
|
||||||
|
<span
|
||||||
|
className={`whitespace-pre text-base md:text-sm lg:text-base ${
|
||||||
|
// 对纠正建议使用ellipsis确保文本不会溢出
|
||||||
|
inlineSuggestion.type === 'correction'
|
||||||
|
? 'text-amber-500/80 dark:text-amber-400/80 ml-1 block truncate'
|
||||||
: 'text-gray-400/70 dark:text-gray-500/70'
|
: 'text-gray-400/70 dark:text-gray-500/70'
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
fontWeight: 'bold',
|
fontWeight: inlineSuggestion.type === 'correction' ? '600' : 'bold',
|
||||||
|
textDecoration: inlineSuggestion.type === 'correction' ? 'underline dotted 1px' : 'none',
|
||||||
|
textUnderlineOffset: '2px',
|
||||||
marginLeft: inlineSuggestion.type === 'completion' ? '0px' : undefined,
|
marginLeft: inlineSuggestion.type === 'completion' ? '0px' : undefined,
|
||||||
|
// 确保溢出时有优雅的省略效果
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
}}
|
}}
|
||||||
|
title={inlineSuggestion.type === 'correction' ? inlineSuggestion.text : undefined} // 在纠正模式下添加完整文本提示
|
||||||
>
|
>
|
||||||
{inlineSuggestion.suggestionText}
|
{inlineSuggestion.suggestionText}
|
||||||
</span>
|
</span>
|
||||||
@ -1135,43 +1243,89 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 搜索图标 */}
|
{/* 搜索图标 */}
|
||||||
<div className="absolute left-2.5 md:left-2.5 left-3.5 top-1/2 transform -translate-y-1/2 z-20">
|
<div className="absolute left-3.5 md:left-2.5 lg:left-3.5 top-1/2 transform -translate-y-1/2 z-20">
|
||||||
<svg className="h-3.5 w-3.5 md:h-3.5 md:w-3.5 h-4.5 w-4.5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
<svg className="h-5 w-5 md:h-3.5 md:w-3.5 lg:h-4.5 lg:w-4.5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 加载指示器或清除按钮 */}
|
{/* 加载指示器或清除按钮 */}
|
||||||
<div className="absolute right-2.5 md:right-2.5 right-3.5 top-1/2 transform -translate-y-1/2 z-20 flex items-center">
|
<div className="absolute right-3.5 md:right-2.5 lg:right-3.5 top-1/2 transform -translate-y-1/2 z-20 flex items-center">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="animate-spin rounded-full h-3.5 w-3.5 md:h-3.5 md:w-3.5 h-4.5 w-4.5 border-2 border-primary-600 border-t-transparent"></div>
|
<div className="animate-spin rounded-full h-5 w-5 md:h-3.5 md:w-3.5 lg:h-4.5 lg:w-4.5 border-2 border-primary-600 border-t-transparent"></div>
|
||||||
) : query ? (
|
) : query ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={clearSearch}
|
className="text-gray-400 hover:text-primary-500 dark:hover:text-primary-400 focus:outline-none active:text-primary-600 dark:active:text-primary-300 flex items-center justify-center p-2 -m-1 clear-search-button"
|
||||||
className="text-gray-400 hover:text-primary-500 dark:hover:text-primary-400 focus:outline-none active:text-primary-600 dark:active:text-primary-300 flex items-center justify-center p-1 -m-1"
|
|
||||||
title="清除搜索"
|
title="清除搜索"
|
||||||
|
style={{ touchAction: 'none' }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
e.nativeEvent.stopImmediatePropagation(); // 阻止事件冒泡到document
|
||||||
|
// 只清除文本,始终不关闭搜索框
|
||||||
|
clearSearch();
|
||||||
|
}}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onTouchEnd={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
e.nativeEvent.stopImmediatePropagation(); // 阻止事件冒泡到document
|
||||||
|
clearSearch();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<svg className="h-3.5 w-3.5 md:h-3.5 md:w-3.5 h-4.5 w-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
<svg className="h-5 w-5 md:h-3.5 md:w-3.5 lg:h-4.5 lg:w-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{inlineSuggestion.visible && inlineSuggestion.text && (
|
{inlineSuggestion.visible && inlineSuggestion.text && (
|
||||||
<div
|
<div
|
||||||
className="text-gray-400 hover:text-primary-500 dark:hover:text-primary-400 active:text-primary-600 dark:active:text-primary-300 flex items-center justify-center cursor-pointer p-1 ml-1"
|
className={`text-gray-400 hover:text-primary-500 dark:hover:text-primary-400 active:text-primary-600 dark:active:text-primary-300 flex items-center justify-center cursor-pointer p-1 ml-1 tab-completion-button ${
|
||||||
title="按Tab键补全"
|
inlineSuggestion.type === 'correction' ? 'animate-pulse' : ''
|
||||||
onClick={completeInlineSuggestion}
|
}`}
|
||||||
|
title={inlineSuggestion.type === 'correction' ? "按Tab键接受纠正" : "按Tab键补全"}
|
||||||
|
onClick={(e) => {
|
||||||
|
// 阻止冒泡和默认行为
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// 直接执行补全操作,不再使用延迟和多次更新
|
||||||
|
completeInlineSuggestion();
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
// 阻止失去焦点
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onTouchEnd={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
completeInlineSuggestion();
|
||||||
|
}}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
completeInlineSuggestion();
|
completeInlineSuggestion();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
style={{ touchAction: 'none' }}
|
||||||
>
|
>
|
||||||
<div className="border border-current rounded px-1 py-px text-[8px] md:text-[8px] leading-none font-semibold flex items-center justify-center">
|
<div className={`border ${
|
||||||
|
inlineSuggestion.type === 'correction'
|
||||||
|
? 'border-amber-500/80 text-amber-500/90 dark:border-amber-400/80 dark:text-amber-400/90'
|
||||||
|
: 'border-current'
|
||||||
|
} rounded px-1 py-px text-[10px] md:text-[8px] lg:text-[8px] leading-none font-semibold flex items-center justify-center`}>
|
||||||
TAB
|
TAB
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1179,11 +1333,11 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
</>
|
</>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="rounded-full h-2 w-2 md:h-2 md:w-2 h-3 w-3 bg-red-500 shadow-sm shadow-red-500/50"></div>
|
<div className="rounded-full h-3 w-3 md:h-2 md:w-2 lg:h-3 lg:w-3 bg-red-500 shadow-sm shadow-red-500/50"></div>
|
||||||
</div>
|
</div>
|
||||||
) : isLoadingIndex ? (
|
) : isLoadingIndex ? (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="animate-pulse rounded-full h-2 w-2 md:h-2 md:w-2 h-3 w-3 bg-yellow-500 shadow-sm shadow-yellow-500/50"></div>
|
<div className="animate-pulse rounded-full h-3 w-3 md:h-2 md:w-2 lg:h-3 lg:w-3 bg-yellow-500 shadow-sm shadow-yellow-500/50"></div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -473,46 +473,129 @@ const tableOfContents = generateTableOfContents(headings);
|
|||||||
|
|
||||||
<!-- 文章页面脚本 -->
|
<!-- 文章页面脚本 -->
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
// 文章页面交互脚本
|
// 文章页面交互脚本 - 自销毁模式
|
||||||
(function() {
|
(function() {
|
||||||
// 存储事件监听器,便于统一清理
|
// 如果不是文章页面,立即退出,不执行任何代码
|
||||||
const listeners = [];
|
if (!document.querySelector("article")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scriptInstanceId = Date.now();
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 检测到文章页面,开始初始化`);
|
||||||
|
|
||||||
|
// 集中管理所有事件监听器
|
||||||
|
const allListeners = [];
|
||||||
|
|
||||||
|
// 为特殊清理任务准备的数组
|
||||||
|
const customCleanupTasks = [];
|
||||||
|
|
||||||
|
// 单独保存清理事件的监听器引用
|
||||||
|
const cleanupListeners = [];
|
||||||
|
|
||||||
// 添加事件监听器并记录,方便后续统一清理
|
// 添加事件监听器并记录,方便后续统一清理
|
||||||
function addListener(element, eventType, handler, options) {
|
function addListener(element, eventType, handler, options) {
|
||||||
if (!element) return null;
|
if (!element) {
|
||||||
|
console.warn(`[文章脚本:${scriptInstanceId}] 尝试为不存在的元素添加事件:`, eventType);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 添加事件监听器: ${eventType} 到`, element.tagName || "Window/Document");
|
||||||
element.addEventListener(eventType, handler, options);
|
element.addEventListener(eventType, handler, options);
|
||||||
listeners.push({ element, eventType, handler, options });
|
allListeners.push({ element, eventType, handler, options });
|
||||||
return handler;
|
return handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理函数,移除所有事件监听器
|
// 统一的清理函数,执行完整清理并自销毁
|
||||||
function cleanup() {
|
function selfDestruct() {
|
||||||
listeners.forEach(({ element, eventType, handler, options }) => {
|
console.log(`[文章脚本:${scriptInstanceId}] 执行自销毁流程`);
|
||||||
|
|
||||||
|
// 1. 先移除普通事件监听器
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 移除常规监听器,数量:`, allListeners.length);
|
||||||
|
allListeners.forEach(({ element, eventType, handler, options }) => {
|
||||||
try {
|
try {
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 移除事件监听器: ${eventType} 从`, element.tagName || "Window/Document");
|
||||||
element.removeEventListener(eventType, handler, options);
|
element.removeEventListener(eventType, handler, options);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 忽略错误
|
console.error(`[文章脚本:${scriptInstanceId}] 移除事件监听器出错:`, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
listeners.length = 0;
|
// 清空监听器数组
|
||||||
|
allListeners.length = 0;
|
||||||
|
|
||||||
|
// 2. 执行特殊清理任务
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 执行特殊清理任务,数量:`, customCleanupTasks.length);
|
||||||
|
customCleanupTasks.forEach(task => {
|
||||||
|
try {
|
||||||
|
task();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[文章脚本:${scriptInstanceId}] 执行特殊清理任务出错:`, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清空特殊任务数组
|
||||||
|
customCleanupTasks.length = 0;
|
||||||
|
|
||||||
|
// 3. 最后移除清理事件监听器自身
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 移除清理监听器,数量:`, cleanupListeners.length);
|
||||||
|
cleanupListeners.forEach(({ element, eventType, handler, options }) => {
|
||||||
|
try {
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 移除清理监听器: ${eventType} 从`, element.tagName || "Window/Document");
|
||||||
|
element.removeEventListener(eventType, handler, options);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[文章脚本:${scriptInstanceId}] 移除清理监听器出错:`, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 完全销毁完成`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 代码块复制功能
|
// 注册清理事件,并保存引用
|
||||||
function setupCodeCopy() {
|
function registerCleanupEvents() {
|
||||||
const copyButtons = document.querySelectorAll('.code-block-copy');
|
console.log(`[文章脚本:${scriptInstanceId}] 注册清理事件`);
|
||||||
if (copyButtons.length === 0) return;
|
|
||||||
|
|
||||||
|
// 创建一次性事件处理函数
|
||||||
|
const beforeSwapHandler = () => {
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] astro:before-swap 触发,执行自销毁`);
|
||||||
|
selfDestruct();
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeUnloadHandler = () => {
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] beforeunload 触发,执行自销毁`);
|
||||||
|
selfDestruct();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加清理事件监听器并保存引用
|
||||||
|
document.addEventListener("astro:before-swap", beforeSwapHandler, { once: true });
|
||||||
|
window.addEventListener("beforeunload", beforeUnloadHandler, { once: true });
|
||||||
|
|
||||||
|
// 保存清理事件引用,用于完全销毁
|
||||||
|
cleanupListeners.push(
|
||||||
|
{ element: document, eventType: "astro:before-swap", handler: beforeSwapHandler, options: { once: true } },
|
||||||
|
{ element: window, eventType: "beforeunload", handler: beforeUnloadHandler, options: { once: true } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化所有功能
|
||||||
|
function initializeFeatures() {
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 开始初始化各功能`);
|
||||||
|
|
||||||
|
// 1. 代码块复制功能
|
||||||
|
function setupCodeCopy() {
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 初始化代码复制功能`);
|
||||||
|
const copyButtons = document.querySelectorAll('.code-block-copy');
|
||||||
|
if (copyButtons.length === 0) {
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 未找到代码复制按钮`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 找到代码复制按钮数量:`, copyButtons.length);
|
||||||
copyButtons.forEach(button => {
|
copyButtons.forEach(button => {
|
||||||
addListener(button, 'click', async () => {
|
addListener(button, 'click', async () => {
|
||||||
try {
|
try {
|
||||||
// 使用Base64解码获取代码文本
|
|
||||||
const encodedCode = button.getAttribute('data-code');
|
const encodedCode = button.getAttribute('data-code');
|
||||||
if (!encodedCode) return;
|
if (!encodedCode) return;
|
||||||
|
|
||||||
// 解码并复制到剪贴板
|
|
||||||
const code = atob(encodedCode);
|
const code = atob(encodedCode);
|
||||||
await navigator.clipboard.writeText(code);
|
await navigator.clipboard.writeText(code);
|
||||||
|
|
||||||
@ -530,7 +613,7 @@ const tableOfContents = generateTableOfContents(headings);
|
|||||||
button.innerHTML = originalHTML;
|
button.innerHTML = originalHTML;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('复制失败:', err);
|
console.error(`[文章脚本:${scriptInstanceId}] 复制失败:`, err);
|
||||||
button.innerHTML = `
|
button.innerHTML = `
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4">
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
@ -553,12 +636,16 @@ const tableOfContents = generateTableOfContents(headings);
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 设置阅读进度条
|
// 2. 阅读进度条
|
||||||
function setupProgressBar() {
|
function setupProgressBar() {
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 初始化阅读进度条`);
|
||||||
const progressBar = document.getElementById("progress-bar");
|
const progressBar = document.getElementById("progress-bar");
|
||||||
const backToTopButton = document.getElementById("back-to-top");
|
const backToTopButton = document.getElementById("back-to-top");
|
||||||
|
|
||||||
if (!progressBar) return;
|
if (!progressBar) {
|
||||||
|
console.warn(`[文章脚本:${scriptInstanceId}] 未找到进度条元素`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
function updateReadingProgress() {
|
function updateReadingProgress() {
|
||||||
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
||||||
@ -605,15 +692,20 @@ const tableOfContents = generateTableOfContents(headings);
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始更新一次进度条
|
||||||
updateReadingProgress();
|
updateReadingProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 管理目录交互
|
// 3. 目录交互
|
||||||
function setupTableOfContents() {
|
function setupTableOfContents() {
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 初始化目录交互`);
|
||||||
const tocContent = document.getElementById("toc-content");
|
const tocContent = document.getElementById("toc-content");
|
||||||
const tocPanel = document.querySelector("#toc-panel");
|
const tocPanel = document.querySelector("#toc-panel");
|
||||||
|
|
||||||
if (!tocPanel || !tocContent) return;
|
if (!tocPanel || !tocContent) {
|
||||||
|
console.warn(`[文章脚本:${scriptInstanceId}] 未找到目录面板或内容`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 检查窗口大小调整目录面板显示
|
// 检查窗口大小调整目录面板显示
|
||||||
function checkTocVisibility() {
|
function checkTocVisibility() {
|
||||||
@ -631,6 +723,8 @@ const tableOfContents = generateTableOfContents(headings);
|
|||||||
|
|
||||||
// 处理目录链接点击跳转
|
// 处理目录链接点击跳转
|
||||||
const tocLinks = tocContent.querySelectorAll("a");
|
const tocLinks = tocContent.querySelectorAll("a");
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 找到目录链接数量:`, tocLinks.length);
|
||||||
|
|
||||||
tocLinks.forEach(link => {
|
tocLinks.forEach(link => {
|
||||||
addListener(link, "click", (e) => {
|
addListener(link, "click", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -657,7 +751,10 @@ const tableOfContents = generateTableOfContents(headings);
|
|||||||
|
|
||||||
// 监听滚动以更新当前活动的目录项
|
// 监听滚动以更新当前活动的目录项
|
||||||
const article = document.querySelector("article");
|
const article = document.querySelector("article");
|
||||||
if (!article) return;
|
if (!article) {
|
||||||
|
console.warn(`[文章脚本:${scriptInstanceId}] 未找到文章内容元素`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let ticking = false;
|
let ticking = false;
|
||||||
|
|
||||||
@ -725,25 +822,34 @@ const tableOfContents = generateTableOfContents(headings);
|
|||||||
updateActiveHeading();
|
updateActiveHeading();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 处理Mermaid图表渲染
|
// 4. Mermaid图表渲染
|
||||||
function setupMermaid() {
|
function setupMermaid() {
|
||||||
// 查找所有mermaid代码块 - 支持多种可能的类名和选择器
|
console.log(`[文章脚本:${scriptInstanceId}] 检查Mermaid图表`);
|
||||||
|
// 查找所有mermaid代码块
|
||||||
const mermaidBlocks = document.querySelectorAll(
|
const mermaidBlocks = document.querySelectorAll(
|
||||||
'pre.language-mermaid, pre > code.language-mermaid, .mermaid'
|
'pre.language-mermaid, pre > code.language-mermaid, .mermaid'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mermaidBlocks.length === 0) return;
|
if (mermaidBlocks.length === 0) {
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 未找到Mermaid图表`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('找到Mermaid代码块:', mermaidBlocks.length);
|
console.log(`[文章脚本:${scriptInstanceId}] 找到Mermaid图表数量:`, mermaidBlocks.length);
|
||||||
|
|
||||||
// 动态加载mermaid库
|
// 动态加载mermaid库
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.src = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js';
|
script.src = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js';
|
||||||
|
|
||||||
script.onload = function() {
|
script.onload = function() {
|
||||||
console.log('Mermaid库加载完成,开始渲染图表');
|
console.log(`[文章脚本:${scriptInstanceId}] Mermaid库加载成功`);
|
||||||
|
|
||||||
// 初始化mermaid配置 - 始终使用默认主题,通过CSS控制样式
|
if (!window.mermaid) {
|
||||||
|
console.error(`[文章脚本:${scriptInstanceId}] Mermaid库加载后window.mermaid不存在`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化mermaid配置
|
||||||
window.mermaid.initialize({
|
window.mermaid.initialize({
|
||||||
startOnLoad: false,
|
startOnLoad: false,
|
||||||
theme: 'default',
|
theme: 'default',
|
||||||
@ -789,17 +895,17 @@ const tableOfContents = generateTableOfContents(headings);
|
|||||||
|
|
||||||
// 初始化渲染
|
// 初始化渲染
|
||||||
try {
|
try {
|
||||||
console.log('开始渲染Mermaid图表');
|
console.log(`[文章脚本:${scriptInstanceId}] 开始渲染Mermaid图表`);
|
||||||
window.mermaid.run().catch(err => {
|
window.mermaid.run().catch(err => {
|
||||||
console.error('Mermaid渲染出错:', err);
|
console.error(`[文章脚本:${scriptInstanceId}] Mermaid渲染出错:`, err);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('初始化Mermaid渲染失败:', error);
|
console.error(`[文章脚本:${scriptInstanceId}] 初始化Mermaid渲染失败:`, error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
script.onerror = function() {
|
script.onerror = function() {
|
||||||
console.error('加载Mermaid库失败');
|
console.error(`[文章脚本:${scriptInstanceId}] 加载Mermaid库失败`);
|
||||||
// 显示错误信息
|
// 显示错误信息
|
||||||
mermaidBlocks.forEach(block => {
|
mermaidBlocks.forEach(block => {
|
||||||
if (block.tagName === 'CODE') block = block.closest('pre');
|
if (block.tagName === 'CODE') block = block.closest('pre');
|
||||||
@ -811,18 +917,27 @@ const tableOfContents = generateTableOfContents(headings);
|
|||||||
|
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
|
|
||||||
// 添加到清理列表,确保后续页面跳转时能删除脚本
|
// 添加Mermaid清理任务
|
||||||
listeners.push({
|
customCleanupTasks.push(() => {
|
||||||
element: script,
|
console.log(`[文章脚本:${scriptInstanceId}] 执行Mermaid特殊清理`);
|
||||||
eventType: 'remove',
|
|
||||||
handler: () => {
|
// 移除脚本标签
|
||||||
if (script.parentNode) {
|
if (script.parentNode) {
|
||||||
script.parentNode.removeChild(script);
|
script.parentNode.removeChild(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除全局mermaid对象
|
// 清除全局mermaid对象
|
||||||
if (window.mermaid) {
|
if (window.mermaid) {
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 清除window.mermaid对象`);
|
||||||
|
try {
|
||||||
|
// 尝试清理mermaid内部状态
|
||||||
|
if (typeof window.mermaid.destroy === 'function') {
|
||||||
|
window.mermaid.destroy();
|
||||||
|
}
|
||||||
window.mermaid = undefined;
|
window.mermaid = undefined;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[文章脚本:${scriptInstanceId}] 清理mermaid对象出错:`, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除页面上可能留下的mermaid相关元素
|
// 移除页面上可能留下的mermaid相关元素
|
||||||
@ -837,70 +952,27 @@ const tableOfContents = generateTableOfContents(headings);
|
|||||||
|
|
||||||
document.querySelectorAll(mermaidElements.join(', ')).forEach(el => {
|
document.querySelectorAll(mermaidElements.join(', ')).forEach(el => {
|
||||||
if (el && el.parentNode) {
|
if (el && el.parentNode) {
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 移除Mermaid元素:`, el.id || el.className);
|
||||||
el.parentNode.removeChild(el);
|
el.parentNode.removeChild(el);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('清理Mermaid元素时出错:', e);
|
console.error(`[文章脚本:${scriptInstanceId}] 清理Mermaid元素时出错:`, e);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
options: null
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化所有功能
|
// 启动所有功能
|
||||||
function init() {
|
setupCodeCopy();
|
||||||
if (!document.querySelector("article")) return;
|
|
||||||
|
|
||||||
setupCodeCopy(); // 只保留代码复制功能
|
|
||||||
setupProgressBar();
|
setupProgressBar();
|
||||||
setupTableOfContents();
|
setupTableOfContents();
|
||||||
setupMermaid();
|
setupMermaid();
|
||||||
|
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 所有功能初始化完成`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册清理函数
|
// 执行初始化
|
||||||
function registerCleanup() {
|
registerCleanupEvents();
|
||||||
// 使用 once: true 确保事件只触发一次
|
initializeFeatures();
|
||||||
document.addEventListener("astro:before-preparation", cleanup, { once: true });
|
|
||||||
document.addEventListener("astro:before-swap", cleanup, { once: true });
|
|
||||||
document.addEventListener("swup:willReplaceContent", cleanup, { once: true });
|
|
||||||
window.addEventListener("beforeunload", cleanup, { once: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理页面跳转事件
|
|
||||||
function setupPageTransitionEvents() {
|
|
||||||
// 页面转换后事件
|
|
||||||
const pageTransitionEvents = [
|
|
||||||
{ name: "astro:after-swap", delay: 10 },
|
|
||||||
{ name: "astro:page-load", delay: 10 },
|
|
||||||
{ name: "swup:contentReplaced", delay: 10 },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 设置每个页面转换事件
|
|
||||||
pageTransitionEvents.forEach(({ name, delay }) => {
|
|
||||||
document.addEventListener(name, () => {
|
|
||||||
cleanup(); // 立即清理
|
|
||||||
|
|
||||||
// 延迟初始化,确保DOM完全更新
|
|
||||||
setTimeout(() => {
|
|
||||||
cleanup(); // 再次清理,确保没有遗漏
|
|
||||||
init();
|
|
||||||
}, delay);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面加载后初始化
|
|
||||||
if (document.readyState === "loading") {
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
init();
|
|
||||||
registerCleanup();
|
|
||||||
setupPageTransitionEvents();
|
|
||||||
}, { once: true });
|
|
||||||
} else {
|
|
||||||
init();
|
|
||||||
registerCleanup();
|
|
||||||
setupPageTransitionEvents();
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/* 代码块容器样式 - 简化背景和阴影 */
|
/* 代码块容器样式 - 简化背景和阴影 */
|
||||||
.code-block-container {
|
.code-block-container {
|
||||||
margin: 1rem 0;
|
margin: 0.75rem 0;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.4rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@ -13,11 +13,11 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.4rem 0.8rem;
|
padding: 0.3rem 0.6rem;
|
||||||
background-color: #f1f5f9;
|
background-color: #f1f5f9;
|
||||||
border-bottom: 1px solid #e2e8f0;
|
border-bottom: 1px solid #e2e8f0;
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
font-size: 0.875rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 代码语言标签 */
|
/* 代码语言标签 */
|
||||||
@ -42,10 +42,10 @@
|
|||||||
color: #475569;
|
color: #475569;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.2rem;
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.2rem 0.4rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.7rem;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,14 +67,14 @@
|
|||||||
/* 基础代码块样式 - 减小内边距 */
|
/* 基础代码块样式 - 减小内边距 */
|
||||||
pre {
|
pre {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0.2rem 0;
|
padding: 0.15rem 0;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre code {
|
pre code {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
font-size: 1.05rem;
|
font-size: 0.9rem;
|
||||||
line-height: 1.5rem;
|
line-height: 1.4rem;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@ -92,7 +92,7 @@ pre code {
|
|||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 3rem;
|
width: 2.5rem;
|
||||||
background-color: #f1f5f9;
|
background-color: #f1f5f9;
|
||||||
border-right: 1px solid #e2e8f0;
|
border-right: 1px solid #e2e8f0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@ -102,9 +102,9 @@ pre code {
|
|||||||
.line-numbers .line {
|
.line-numbers .line {
|
||||||
position: relative;
|
position: relative;
|
||||||
counter-increment: line;
|
counter-increment: line;
|
||||||
padding-left: 3.5rem;
|
padding-left: 3rem;
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.4rem;
|
||||||
min-height: 1.5rem;
|
min-height: 1.4rem;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,11 +114,11 @@ pre code {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 3rem;
|
width: 2.5rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
font-size: 0.95rem;
|
font-size: 0.85rem;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -196,3 +196,57 @@ pre.shiki, pre.astro-code,
|
|||||||
color: var(--shiki-dark) !important;
|
color: var(--shiki-dark) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 行内代码块样式 */
|
||||||
|
:not(pre) > code {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-primary-700);
|
||||||
|
background-color: var(--color-primary-50);
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
margin: 0 0.2rem;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
border: 1px solid var(--color-primary-100);
|
||||||
|
white-space: normal;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
max-width: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 行内代码块黑暗模式样式 */
|
||||||
|
[data-theme='dark'] :not(pre) > code {
|
||||||
|
color: var(--color-primary-300);
|
||||||
|
background-color: rgba(75, 107, 255, 0.1);
|
||||||
|
border-color: var(--color-primary-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 长路径的行内代码块样式特殊处理 */
|
||||||
|
:not(pre) > code:has(path),
|
||||||
|
:not(pre) > code.file-path {
|
||||||
|
white-space: pre-wrap; /* 保留空格但允许换行 */
|
||||||
|
overflow-wrap: break-word; /* 允许在任何地方断行 */
|
||||||
|
word-break: break-all; /* 允许在任何字符间断行 */
|
||||||
|
max-width: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.85rem; /* 略微减小字体尺寸 */
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 针对文件路径的特殊样式 - 适用于Windows路径 */
|
||||||
|
:not(pre) > code.file-path {
|
||||||
|
color: var(--color-gray-700);
|
||||||
|
background-color: var(--color-gray-100);
|
||||||
|
border-color: var(--color-gray-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 针对文件路径的特殊样式 - 黑暗模式 */
|
||||||
|
[data-theme='dark'] :not(pre) > code.file-path {
|
||||||
|
color: var(--color-gray-300);
|
||||||
|
background-color: var(--color-gray-800);
|
||||||
|
border-color: var(--color-gray-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -140,7 +140,7 @@ fn get_search_suggestions(search_index: &ArticleSearchIndex, query: &str) -> Vec
|
|||||||
suggestion_type: SuggestionType::Completion,
|
suggestion_type: SuggestionType::Completion,
|
||||||
frequency: *freq
|
frequency: *freq
|
||||||
});
|
});
|
||||||
} else if query.starts_with(&term_lower) || term_lower.contains(&query) {
|
} else if term_lower.contains(&query) {
|
||||||
// 包含关系,作为纠正建议
|
// 包含关系,作为纠正建议
|
||||||
candidates.push(SuggestionCandidate {
|
candidates.push(SuggestionCandidate {
|
||||||
text: term.clone(),
|
text: term.clone(),
|
||||||
|
Loading…
Reference in New Issue
Block a user