优化搜索数据规则,优化dom绑定和监听,优化代码样式

This commit is contained in:
lsy 2025-05-05 19:40:03 +08:00
parent f153c65faa
commit afbdc61605
9 changed files with 2293 additions and 1939 deletions

View File

@ -1092,12 +1092,20 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
currentPage: 1,
date: "all",
};
// 先更新UI状态
setActiveFilters(defaultFilters);
// 直接传递重置后的状态给筛选函数并更新URL
applyFilters(defaultFilters);
}, []);
// 清除URL参数
if (typeof window !== 'undefined') {
window.history.pushState({}, "", window.location.pathname);
}
// 如果WASM模块已加载直接调用筛选逻辑以确保实际应用
if (wasmModule && isArticlesLoaded) {
applyFilteringLogic(defaultFilters);
}
}, [wasmModule, isArticlesLoaded]);
// 渲染错误信息
const renderError = () => (

View File

@ -187,99 +187,101 @@ const breadcrumbs: Breadcrumb[] = pathSegments
</div>
<script is:inline>
// 返回按钮点击事件处理
// 面包屑返回按钮处理 - 自销毁模式
(function() {
// 页面导航计数器
let pageNavigationCount = 0;
// 存储事件监听器,便于统一清理
const listeners = [];
// 清理按钮事件监听器
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 || [];
otherClickHandlers.forEach(handler => {
try {
button.removeEventListener('click', handler);
} catch (e) {
// 忽略错误
}
});
// 重置处理函数数组
button.__backButtonClickHandlers = [];
});
// 跳过非文章页面,只在文章详情页执行
const isArticlePage = document.querySelector('.back-button');
if (!isArticlePage) {
return;
}
// 集中管理所有事件监听器
const allListeners = [];
// 单独保存清理事件的监听器引用
const cleanupListeners = [];
// 添加事件监听器并记录,方便后续统一清理
function addListener(element, eventType, handler, options) {
if (!element) 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);
if (!element) {
console.warn(`[面包屑尝试为不存在的元素添加事件`);
return null;
}
element.addEventListener(eventType, handler, options);
listeners.push({ element, eventType, handler, options });
allListeners.push({ element, eventType, handler, options });
return handler;
}
// 清理函数 - 移除所有事件监听器
function cleanup() {
// 先直接从按钮清理事件
cleanupButtonListeners();
// 移除所有监听器
listeners.forEach(({ element, eventType, handler, options }) => {
// 统一的清理函数,执行完整清理并自销毁
function selfDestruct() {
// 1. 移除所有普通事件监听器
allListeners.forEach(({ element, eventType, handler, options }) => {
try {
element.removeEventListener(eventType, handler, options);
} 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() {
// 确保当前没有活动的返回按钮事件
cleanup();
// 注册清理事件,并保存引用
function registerCleanupEvents() {
// 创建一次性事件处理函数
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');
if (!backButton) {
return;
}
try {
backButton.style.pointerEvents = 'auto';
} catch (e) {
// 忽略样式错误
}
const clickHandler = (e) => {
e.preventDefault();
@ -301,100 +303,43 @@ const breadcrumbs: Breadcrumb[] = pathSegments
addListener(backButton, 'click', clickHandler);
}
// 注册清理函数 - 确保在每次页面转换前清理事件
function registerCleanup() {
const cleanupEvents = [
'astro:before-preparation',
'astro:before-swap',
'astro:beforeload',
'swup:willReplaceContent'
];
// 主初始化函数
function init() {
// 注册清理事件
registerCleanupEvents();
// 为每个事件注册一次性清理函数
cleanupEvents.forEach(eventName => {
const handler = () => {
cleanup();
// 设置返回按钮
setupBackButton();
// 注册页面加载后的处理函数 - 仅当使用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 });
});
// 页面卸载时清理
window.addEventListener('beforeunload', () => {
cleanup();
}, { once: true });
}
// 初始化函数
function init() {
pageNavigationCount++;
setupBackButton();
registerCleanup();
}
// 监听页面转换事件
function setupPageTransitionEvents() {
// 确保事件处理程序唯一性的函数
function setupUniqueEvent(eventName, callback) {
const eventKey = `__back_button_event_${eventName.replace(/:/g, '_')}`;
// 移除可能存在的旧处理函数
if (window[eventKey]) {
document.removeEventListener(eventName, window[eventKey]);
if (typeof document.startViewTransition !== 'undefined') {
addListener(document, 'astro:page-load', pageLoadHandler);
}
// 保存新处理函数并注册
window[eventKey] = callback;
document.addEventListener(eventName, window[eventKey]);
if (typeof window.swup !== 'undefined') {
addListener(document, 'swup:contentReplaced', pageLoadHandler);
}
}
// 页面转换后事件
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 });
// 判断DOM是否已加载
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init, { once: true });
} else {
setTimeout(() => {
init();
}, 0);
init();
}
})();
</script>

File diff suppressed because it is too large Load Diff

View File

@ -204,7 +204,6 @@ const Search: React.FC<SearchProps> = ({
// 如果是取消的请求,不显示错误
if (err instanceof Error && (err.name === 'AbortError' || err.message.includes('aborted'))) {
console.log('索引加载请求被取消:', err.message);
return;
}
@ -239,7 +238,7 @@ const Search: React.FC<SearchProps> = ({
}
};
window.addEventListener('resize', handleResize, { passive: true });
window.addEventListener('resize', handleResize, { passive: false });
return () => {
window.removeEventListener('resize', handleResize);
};
@ -248,18 +247,35 @@ const Search: React.FC<SearchProps> = ({
// 处理点击外部关闭搜索结果和建议
useEffect(() => {
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 (
searchResultsRef.current &&
!searchResultsRef.current.contains(event.target as Node) &&
!searchResultsRef.current.contains(target) &&
searchInputRef.current &&
!searchInputRef.current.contains(event.target as Node)
!searchInputRef.current.contains(target)
) {
// 当点击搜索框和结果区域之外时,才隐藏结果
setShowResults(false);
setInlineSuggestion(prev => ({ ...prev, visible: false })); // 也隐藏内联建议
}
};
document.addEventListener("mousedown", handleClickOutside, { passive: true });
document.addEventListener("mousedown", handleClickOutside, { passive: false });
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
@ -299,6 +315,7 @@ const Search: React.FC<SearchProps> = ({
setSuggestions([]);
setInlineSuggestion(prev => ({ ...prev, visible: false }));
setSelectedSuggestionIndex(0); // 重置选中索引
console.log("[建议] 没有符合条件的查询或模块未加载");
return;
}
@ -324,6 +341,7 @@ const Search: React.FC<SearchProps> = ({
}
const searchResult = JSON.parse(result) as SearchResult;
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
@ -347,6 +365,7 @@ const Search: React.FC<SearchProps> = ({
const firstSuggestion = searchResult.suggestions[0];
if (firstSuggestion) {
setInlineSuggestion(prev => ({
...prev,
text: firstSuggestion.text,
@ -363,7 +382,7 @@ const Search: React.FC<SearchProps> = ({
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
console.error("获取内联建议失败:", err);
console.error("[建议错误]", err);
setInlineSuggestion(prev => ({ ...prev, visible: false }));
setSelectedSuggestionIndex(0); // 重置选中索引
}
@ -432,9 +451,11 @@ const Search: React.FC<SearchProps> = ({
// 修改处理键盘导航的函数,增加上下箭头键切换建议
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
// Tab键处理内联建议补全
if (e.key === "Tab" && inlineSuggestion.visible && inlineSuggestion.text) {
e.preventDefault(); // 阻止默认的Tab行为
e.stopPropagation(); // 防止事件冒泡
completeInlineSuggestion();
return;
}
@ -532,50 +553,13 @@ const Search: React.FC<SearchProps> = ({
}
};
document.addEventListener('selectionchange', handleSelectionChange, { passive: true });
// 使用非被动模式确保在某些上下文中可以调用preventDefault
document.addEventListener('selectionchange', handleSelectionChange, { passive: false });
return () => {
document.removeEventListener('selectionchange', handleSelectionChange);
};
}, [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) => {
if (!wasmModule || !isIndexLoaded || !indexData || !searchQuery.trim()) {
@ -666,6 +650,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) => {
@ -790,6 +814,59 @@ const Search: React.FC<SearchProps> = ({
suggestionEl.style.fontWeight = inputStyle.fontWeight;
suggestionEl.style.letterSpacing = inputStyle.letterSpacing;
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();
@ -802,14 +879,46 @@ const Search: React.FC<SearchProps> = ({
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 = () => {
// 清除查询和结果,但保持搜索框的可见状态
setQuery("");
if (searchInputRef.current) {
searchInputRef.current.value = "";
}
// 清除搜索结果
setSearchResults(null);
setAllItems([]);
setSuggestions([]);
// 清除内联建议
setInlineSuggestion({
text: "",
visible: false,
@ -819,10 +928,13 @@ const Search: React.FC<SearchProps> = ({
matchedText: "",
suggestionText: ""
});
setShowResults(false);
// 保持结果区域可见,但无内容
setShowResults(true);
setCurrentPage(1);
setHasMoreResults(true);
// 确保输入框保持焦点
if (searchInputRef.current) {
searchInputRef.current.focus();
}
@ -844,8 +956,8 @@ const Search: React.FC<SearchProps> = ({
}
};
window.addEventListener('focus', checkFocus, { passive: true, capture: true });
window.addEventListener('blur', checkFocus, { passive: true, capture: true });
window.addEventListener('focus', checkFocus, { passive: false, capture: true });
window.addEventListener('blur', checkFocus, { passive: false, capture: true });
return () => {
window.removeEventListener('focus', checkFocus, { capture: true });
@ -853,22 +965,6 @@ const Search: React.FC<SearchProps> = ({
};
}, [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文本
const getCurrentPlaceholder = () => {
if (isLoadingIndex) {
@ -1090,7 +1186,7 @@ const Search: React.FC<SearchProps> = ({
onSelect={updateCaretPosition}
onFocus={handleInputFocus}
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}
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">
<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 className="flex-shrink-0 max-w-[70%]">
<div className={`flex-shrink-0 ${
// 根据建议类型调整最大宽度
inlineSuggestion.type === 'correction'
? 'max-w-[calc(100%-1.25rem)]' // 纠正建议给予更多空间,但仍然保留一些边距
: 'max-w-[80%]' // 补全建议使用固定比例
}`}>
<span
className={`whitespace-pre text-sm md:text-sm lg:text-base truncate block ${
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'
? 'text-amber-500/80 dark:text-amber-400/80 ml-1 block truncate'
: 'text-gray-400/70 dark:text-gray-500/70'
}`}
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,
// 确保溢出时有优雅的省略效果
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
title={inlineSuggestion.type === 'correction' ? inlineSuggestion.text : undefined} // 在纠正模式下添加完整文本提示
>
{inlineSuggestion.suggestionText}
</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">
<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">
<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-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" />
</svg>
</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 ? (
<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 ? (
<>
<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-1 -m-1"
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"
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" />
</svg>
</button>
{inlineSuggestion.visible && inlineSuggestion.text && (
<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"
title="按Tab键补全"
onClick={completeInlineSuggestion}
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 ${
inlineSuggestion.type === 'correction' ? 'animate-pulse' : ''
}`}
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"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.stopPropagation();
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
</div>
</div>
@ -1179,11 +1333,11 @@ const Search: React.FC<SearchProps> = ({
</>
) : error ? (
<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>
) : isLoadingIndex ? (
<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>
) : null}
</div>

File diff suppressed because it is too large Load Diff

View File

@ -473,356 +473,471 @@ const tableOfContents = generateTableOfContents(headings);
<!-- 文章页面脚本 -->
<script is:inline>
// 文章页面交互脚本
(function () {
// 存储事件监听器,便于统一清理
const listeners = [];
// 文章页面交互脚本 - 自销毁模式
(function() {
// 如果不是文章页面,立即退出,不执行任何代码
if (!document.querySelector("article")) {
return;
}
const scriptInstanceId = Date.now();
console.log(`[文章脚本:${scriptInstanceId}] 检测到文章页面,开始初始化`);
// 集中管理所有事件监听器
const allListeners = [];
// 为特殊清理任务准备的数组
const customCleanupTasks = [];
// 单独保存清理事件的监听器引用
const cleanupListeners = [];
// 添加事件监听器并记录,方便后续统一清理
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);
listeners.push({ element, eventType, handler, options });
allListeners.push({ element, eventType, handler, options });
return handler;
}
// 清理函数,移除所有事件监听器
function cleanup() {
listeners.forEach(({ element, eventType, handler, options }) => {
// 统一的清理函数,执行完整清理并自销毁
function selfDestruct() {
console.log(`[文章脚本:${scriptInstanceId}] 执行自销毁流程`);
// 1. 先移除普通事件监听器
console.log(`[文章脚本:${scriptInstanceId}] 移除常规监听器,数量:`, allListeners.length);
allListeners.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);
}
});
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() {
const copyButtons = document.querySelectorAll('.code-block-copy');
if (copyButtons.length === 0) return;
copyButtons.forEach(button => {
addListener(button, 'click', async () => {
try {
// 使用Base64解码获取代码文本
const encodedCode = button.getAttribute('data-code');
if (!encodedCode) return;
// 解码并复制到剪贴板
const code = atob(encodedCode);
await navigator.clipboard.writeText(code);
const originalHTML = button.innerHTML;
button.classList.add('copied');
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">
<path d="M20 6L9 17l-5-5"></path>
</svg>
已复制
`;
setTimeout(() => {
button.classList.remove('copied');
button.innerHTML = originalHTML;
}, 2000);
} catch (err) {
console.error('复制失败:', err);
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">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
失败
`;
setTimeout(() => {
// 注册清理事件,并保存引用
function registerCleanupEvents() {
console.log(`[文章脚本:${scriptInstanceId}] 注册清理事件`);
// 创建一次性事件处理函数
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 => {
addListener(button, 'click', async () => {
try {
const encodedCode = button.getAttribute('data-code');
if (!encodedCode) return;
const code = atob(encodedCode);
await navigator.clipboard.writeText(code);
const originalHTML = button.innerHTML;
button.classList.add('copied');
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">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
<path d="M20 6L9 17l-5-5"></path>
</svg>
复制
复制
`;
}, 2000);
}
});
});
}
// 4. 设置阅读进度条
function setupProgressBar() {
const progressBar = document.getElementById("progress-bar");
const backToTopButton = document.getElementById("back-to-top");
if (!progressBar) return;
function updateReadingProgress() {
const scrollTop = window.scrollY || document.documentElement.scrollTop;
const scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
const progress = (scrollTop / scrollHeight) * 100;
progressBar.style.width = `${progress}%`;
if (backToTopButton) {
if (scrollTop > 300) {
backToTopButton.classList.add(
"opacity-100",
"visible",
"translate-y-0"
);
backToTopButton.classList.remove(
"opacity-0",
"invisible",
"translate-y-5"
);
} else {
backToTopButton.classList.add(
"opacity-0",
"invisible",
"translate-y-5"
);
backToTopButton.classList.remove(
"opacity-100",
"visible",
"translate-y-0"
);
}
}
}
addListener(window, "scroll", updateReadingProgress);
if (backToTopButton) {
addListener(backToTopButton, "click", () => {
window.scrollTo({
top: 0,
behavior: "smooth",
setTimeout(() => {
button.classList.remove('copied');
button.innerHTML = originalHTML;
}, 2000);
} catch (err) {
console.error(`[文章脚本:${scriptInstanceId}] 复制失败:`, err);
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">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
失败
`;
setTimeout(() => {
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">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
复制
`;
}, 2000);
}
});
});
}
updateReadingProgress();
}
// 5. 管理目录交互
function setupTableOfContents() {
const tocContent = document.getElementById("toc-content");
const tocPanel = document.querySelector("#toc-panel");
// 2. 阅读进度条
function setupProgressBar() {
console.log(`[文章脚本:${scriptInstanceId}] 初始化阅读进度条`);
const progressBar = document.getElementById("progress-bar");
const backToTopButton = document.getElementById("back-to-top");
if (!tocPanel || !tocContent) return;
// 检查窗口大小调整目录面板显示
function checkTocVisibility() {
if (window.innerWidth < 1536) {
tocPanel.classList.add("hidden");
tocPanel.classList.remove("2xl:block");
} else {
tocPanel.classList.remove("hidden");
tocPanel.classList.add("2xl:block");
if (!progressBar) {
console.warn(`[文章脚本:${scriptInstanceId}] 未找到进度条元素`);
return;
}
}
addListener(window, "resize", checkTocVisibility);
checkTocVisibility();
function updateReadingProgress() {
const scrollTop = window.scrollY || document.documentElement.scrollTop;
const scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
const progress = (scrollTop / scrollHeight) * 100;
// 处理目录链接点击跳转
const tocLinks = tocContent.querySelectorAll("a");
tocLinks.forEach(link => {
addListener(link, "click", (e) => {
e.preventDefault();
const targetId = link.getAttribute("href")?.substring(1);
if (!targetId) return;
progressBar.style.width = `${progress}%`;
const targetElement = document.getElementById(targetId);
if (targetElement) {
const offset = 100;
const targetPosition = targetElement.getBoundingClientRect().top + window.scrollY - offset;
if (backToTopButton) {
if (scrollTop > 300) {
backToTopButton.classList.add(
"opacity-100",
"visible",
"translate-y-0"
);
backToTopButton.classList.remove(
"opacity-0",
"invisible",
"translate-y-5"
);
} else {
backToTopButton.classList.add(
"opacity-0",
"invisible",
"translate-y-5"
);
backToTopButton.classList.remove(
"opacity-100",
"visible",
"translate-y-0"
);
}
}
}
addListener(window, "scroll", updateReadingProgress);
if (backToTopButton) {
addListener(backToTopButton, "click", () => {
window.scrollTo({
top: targetPosition,
top: 0,
behavior: "smooth",
});
});
}
targetElement.classList.add("bg-primary-50", "dark:bg-primary-900/20");
setTimeout(() => {
targetElement.classList.remove("bg-primary-50", "dark:bg-primary-900/20");
}, 2000);
}
});
});
// 初始更新一次进度条
updateReadingProgress();
}
// 3. 目录交互
function setupTableOfContents() {
console.log(`[文章脚本:${scriptInstanceId}] 初始化目录交互`);
const tocContent = document.getElementById("toc-content");
const tocPanel = document.querySelector("#toc-panel");
// 监听滚动以更新当前活动的目录项
const article = document.querySelector("article");
if (!article) return;
if (!tocPanel || !tocContent) {
console.warn(`[文章脚本:${scriptInstanceId}] 未找到目录面板或内容`);
return;
}
let ticking = false;
function updateActiveHeading() {
const headings = Array.from(article.querySelectorAll("h1, h2, h3, h4, h5, h6"));
const tocLinks = Array.from(tocContent.querySelectorAll("a"));
// 清除所有活动状态
tocLinks.forEach((link) => {
link.classList.remove("text-primary-600", "dark:text-primary-400", "font-medium");
});
// 找出当前可见的标题
const scrollPosition = window.scrollY + 150;
let currentHeading = null;
for (const heading of headings) {
const headingTop = heading.getBoundingClientRect().top + window.scrollY;
if (headingTop <= scrollPosition) {
currentHeading = heading;
// 检查窗口大小调整目录面板显示
function checkTocVisibility() {
if (window.innerWidth < 1536) {
tocPanel.classList.add("hidden");
tocPanel.classList.remove("2xl:block");
} else {
break;
tocPanel.classList.remove("hidden");
tocPanel.classList.add("2xl:block");
}
}
// 高亮当前标题对应的目录项
if (currentHeading) {
const id = currentHeading.getAttribute('id');
if (id) {
const activeLink = tocLinks.find(
(link) => link.getAttribute("href") === `#${id}`
);
if (activeLink) {
// 高亮当前目录项
activeLink.classList.add("text-primary-600", "dark:text-primary-400", "font-medium");
// 可选: 确保当前激活的目录项在可视区域内
const tocContainer = tocContent.querySelector('ul');
if (tocContainer) {
const linkOffsetTop = activeLink.offsetTop;
const containerScrollTop = tocContainer.scrollTop;
const containerHeight = tocContainer.clientHeight;
addListener(window, "resize", checkTocVisibility);
checkTocVisibility();
// 处理目录链接点击跳转
const tocLinks = tocContent.querySelectorAll("a");
console.log(`[文章脚本:${scriptInstanceId}] 找到目录链接数量:`, tocLinks.length);
tocLinks.forEach(link => {
addListener(link, "click", (e) => {
e.preventDefault();
const targetId = link.getAttribute("href")?.substring(1);
if (!targetId) return;
const targetElement = document.getElementById(targetId);
if (targetElement) {
const offset = 100;
const targetPosition = targetElement.getBoundingClientRect().top + window.scrollY - offset;
window.scrollTo({
top: targetPosition,
behavior: "smooth",
});
targetElement.classList.add("bg-primary-50", "dark:bg-primary-900/20");
setTimeout(() => {
targetElement.classList.remove("bg-primary-50", "dark:bg-primary-900/20");
}, 2000);
}
});
});
// 监听滚动以更新当前活动的目录项
const article = document.querySelector("article");
if (!article) {
console.warn(`[文章脚本:${scriptInstanceId}] 未找到文章内容元素`);
return;
}
let ticking = false;
function updateActiveHeading() {
const headings = Array.from(article.querySelectorAll("h1, h2, h3, h4, h5, h6"));
const tocLinks = Array.from(tocContent.querySelectorAll("a"));
// 清除所有活动状态
tocLinks.forEach((link) => {
link.classList.remove("text-primary-600", "dark:text-primary-400", "font-medium");
});
// 找出当前可见的标题
const scrollPosition = window.scrollY + 150;
let currentHeading = null;
for (const heading of headings) {
const headingTop = heading.getBoundingClientRect().top + window.scrollY;
if (headingTop <= scrollPosition) {
currentHeading = heading;
} else {
break;
}
}
// 高亮当前标题对应的目录项
if (currentHeading) {
const id = currentHeading.getAttribute('id');
if (id) {
const activeLink = tocLinks.find(
(link) => link.getAttribute("href") === `#${id}`
);
if (activeLink) {
// 高亮当前目录项
activeLink.classList.add("text-primary-600", "dark:text-primary-400", "font-medium");
// 如果当前项不在视口内,滚动目录
if (linkOffsetTop < containerScrollTop ||
linkOffsetTop > containerScrollTop + containerHeight) {
tocContainer.scrollTop = linkOffsetTop - containerHeight / 2;
// 可选: 确保当前激活的目录项在可视区域内
const tocContainer = tocContent.querySelector('ul');
if (tocContainer) {
const linkOffsetTop = activeLink.offsetTop;
const containerScrollTop = tocContainer.scrollTop;
const containerHeight = tocContainer.clientHeight;
// 如果当前项不在视口内,滚动目录
if (linkOffsetTop < containerScrollTop ||
linkOffsetTop > containerScrollTop + containerHeight) {
tocContainer.scrollTop = linkOffsetTop - containerHeight / 2;
}
}
}
}
}
}
addListener(window, "scroll", () => {
if (!ticking) {
window.requestAnimationFrame(() => {
updateActiveHeading();
ticking = false;
});
ticking = true;
}
});
updateActiveHeading();
}
addListener(window, "scroll", () => {
if (!ticking) {
window.requestAnimationFrame(() => {
updateActiveHeading();
ticking = false;
});
ticking = true;
// 4. Mermaid图表渲染
function setupMermaid() {
console.log(`[文章脚本:${scriptInstanceId}] 检查Mermaid图表`);
// 查找所有mermaid代码块
const mermaidBlocks = document.querySelectorAll(
'pre.language-mermaid, pre > code.language-mermaid, .mermaid'
);
if (mermaidBlocks.length === 0) {
console.log(`[文章脚本:${scriptInstanceId}] 未找到Mermaid图表`);
return;
}
});
updateActiveHeading();
}
// 6. 处理Mermaid图表渲染
function setupMermaid() {
// 查找所有mermaid代码块 - 支持多种可能的类名和选择器
const mermaidBlocks = document.querySelectorAll(
'pre.language-mermaid, pre > code.language-mermaid, .mermaid'
);
if (mermaidBlocks.length === 0) return;
console.log('找到Mermaid代码块:', mermaidBlocks.length);
// 动态加载mermaid库
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js';
script.onload = function() {
console.log('Mermaid库加载完成开始渲染图表');
// 初始化mermaid配置 - 始终使用默认主题通过CSS控制样式
window.mermaid.initialize({
startOnLoad: false,
theme: 'default',
securityLevel: 'loose'
});
console.log(`[文章脚本:${scriptInstanceId}] 找到Mermaid图表数量:`, mermaidBlocks.length);
// 将所有mermaid代码块转换为可渲染的格式
mermaidBlocks.forEach((block, index) => {
// 获取mermaid代码
let code = '';
// 动态加载mermaid库
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js';
script.onload = function() {
console.log(`[文章脚本:${scriptInstanceId}] Mermaid库加载成功`);
// 检查元素类型并相应处理
if (block.tagName === 'CODE' && block.classList.contains('language-mermaid')) {
// 处理 code.language-mermaid 元素
code = block.textContent || '';
const pre = block.closest('pre');
if (pre) {
// 创建新的div元素替换整个pre
if (!window.mermaid) {
console.error(`[文章脚本:${scriptInstanceId}] Mermaid库加载后window.mermaid不存在`);
return;
}
// 初始化mermaid配置
window.mermaid.initialize({
startOnLoad: false,
theme: 'default',
securityLevel: 'loose'
});
// 将所有mermaid代码块转换为可渲染的格式
mermaidBlocks.forEach((block, index) => {
// 获取mermaid代码
let code = '';
// 检查元素类型并相应处理
if (block.tagName === 'CODE' && block.classList.contains('language-mermaid')) {
// 处理 code.language-mermaid 元素
code = block.textContent || '';
const pre = block.closest('pre');
if (pre) {
// 创建新的div元素替换整个pre
const div = document.createElement('div');
div.className = 'mermaid';
div.id = 'mermaid-diagram-' + index;
div.textContent = code;
pre.parentNode.replaceChild(div, pre);
}
} else if (block.tagName === 'PRE' && block.classList.contains('language-mermaid')) {
// 处理 pre.language-mermaid 元素
code = block.textContent || '';
const div = document.createElement('div');
div.className = 'mermaid';
div.id = 'mermaid-diagram-' + index;
div.textContent = code;
pre.parentNode.replaceChild(div, pre);
block.parentNode.replaceChild(div, block);
} else if (block.classList.contains('mermaid') && block.tagName !== 'DIV') {
// 如果是其他带mermaid类的元素但不是div转换为div
code = block.textContent || '';
const div = document.createElement('div');
div.className = 'mermaid';
div.id = 'mermaid-diagram-' + index;
div.textContent = code;
block.parentNode.replaceChild(div, block);
}
} else if (block.tagName === 'PRE' && block.classList.contains('language-mermaid')) {
// 处理 pre.language-mermaid 元素
code = block.textContent || '';
const div = document.createElement('div');
div.className = 'mermaid';
div.id = 'mermaid-diagram-' + index;
div.textContent = code;
block.parentNode.replaceChild(div, block);
} else if (block.classList.contains('mermaid') && block.tagName !== 'DIV') {
// 如果是其他带mermaid类的元素但不是div转换为div
code = block.textContent || '';
const div = document.createElement('div');
div.className = 'mermaid';
div.id = 'mermaid-diagram-' + index;
div.textContent = code;
block.parentNode.replaceChild(div, block);
}
});
// 初始化渲染
try {
console.log('开始渲染Mermaid图表');
window.mermaid.run().catch(err => {
console.error('Mermaid渲染出错:', err);
});
} catch (error) {
console.error('初始化Mermaid渲染失败:', error);
}
};
script.onerror = function() {
console.error('加载Mermaid库失败');
// 显示错误信息
mermaidBlocks.forEach(block => {
if (block.tagName === 'CODE') block = block.closest('pre');
if (block) {
block.innerHTML = '<div class="mermaid-error-message">无法加载Mermaid图表库</div>';
// 初始化渲染
try {
console.log(`[文章脚本:${scriptInstanceId}] 开始渲染Mermaid图表`);
window.mermaid.run().catch(err => {
console.error(`[文章脚本:${scriptInstanceId}] Mermaid渲染出错:`, err);
});
} catch (error) {
console.error(`[文章脚本:${scriptInstanceId}] 初始化Mermaid渲染失败:`, error);
}
});
};
document.head.appendChild(script);
// 添加到清理列表,确保后续页面跳转时能删除脚本
listeners.push({
element: script,
eventType: 'remove',
handler: () => {
};
script.onerror = function() {
console.error(`[文章脚本:${scriptInstanceId}] 加载Mermaid库失败`);
// 显示错误信息
mermaidBlocks.forEach(block => {
if (block.tagName === 'CODE') block = block.closest('pre');
if (block) {
block.innerHTML = '<div class="mermaid-error-message">无法加载Mermaid图表库</div>';
}
});
};
document.head.appendChild(script);
// 添加Mermaid清理任务
customCleanupTasks.push(() => {
console.log(`[文章脚本:${scriptInstanceId}] 执行Mermaid特殊清理`);
// 移除脚本标签
if (script.parentNode) {
script.parentNode.removeChild(script);
}
// 清除全局mermaid对象
if (window.mermaid) {
window.mermaid = undefined;
console.log(`[文章脚本:${scriptInstanceId}] 清除window.mermaid对象`);
try {
// 尝试清理mermaid内部状态
if (typeof window.mermaid.destroy === 'function') {
window.mermaid.destroy();
}
window.mermaid = undefined;
} catch (e) {
console.error(`[文章脚本:${scriptInstanceId}] 清理mermaid对象出错:`, e);
}
}
// 移除页面上可能留下的mermaid相关元素
@ -837,70 +952,27 @@ const tableOfContents = generateTableOfContents(headings);
document.querySelectorAll(mermaidElements.join(', ')).forEach(el => {
if (el && el.parentNode) {
console.log(`[文章脚本:${scriptInstanceId}] 移除Mermaid元素:`, el.id || el.className);
el.parentNode.removeChild(el);
}
});
} catch (e) {
console.error('清理Mermaid元素时出错:', e);
console.error(`[文章脚本:${scriptInstanceId}] 清理Mermaid元素时出错:`, e);
}
},
options: null
});
}
// 初始化所有功能
function init() {
if (!document.querySelector("article")) return;
});
}
setupCodeCopy(); // 只保留代码复制功能
// 启动所有功能
setupCodeCopy();
setupProgressBar();
setupTableOfContents();
setupMermaid();
console.log(`[文章脚本:${scriptInstanceId}] 所有功能初始化完成`);
}
// 注册清理函数
function registerCleanup() {
// 使用 once: true 确保事件只触发一次
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();
}
// 执行初始化
registerCleanupEvents();
initializeFeatures();
})();
</script>

View File

@ -1,7 +1,7 @@
/* 代码块容器样式 - 简化背景和阴影 */
.code-block-container {
margin: 1rem 0;
border-radius: 0.5rem;
margin: 0.75rem 0;
border-radius: 0.4rem;
overflow: hidden;
border: 1px solid #e2e8f0;
background-color: transparent;
@ -13,11 +13,11 @@
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.4rem 0.8rem;
padding: 0.3rem 0.6rem;
background-color: #f1f5f9;
border-bottom: 1px solid #e2e8f0;
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;
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
gap: 0.2rem;
padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-size: 0.7rem;
transition: all 0.2s ease;
}
@ -67,14 +67,14 @@
/* 基础代码块样式 - 减小内边距 */
pre {
margin: 0;
padding: 0.2rem 0;
padding: 0.15rem 0;
overflow-x: auto;
}
pre code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1.05rem;
line-height: 1.5rem;
font-size: 0.9rem;
line-height: 1.4rem;
padding: 0;
display: block;
}
@ -92,7 +92,7 @@ pre code {
left: 0;
top: 0;
bottom: 0;
width: 3rem;
width: 2.5rem;
background-color: #f1f5f9;
border-right: 1px solid #e2e8f0;
z-index: 1;
@ -102,9 +102,9 @@ pre code {
.line-numbers .line {
position: relative;
counter-increment: line;
padding-left: 3.5rem;
padding-right: 0.5rem;
min-height: 1.5rem;
padding-left: 3rem;
padding-right: 0.4rem;
min-height: 1.4rem;
white-space: pre;
}
@ -114,11 +114,11 @@ pre code {
position: absolute;
left: 0;
top: 0;
width: 3rem;
width: 2.5rem;
height: 100%;
text-align: center;
color: #94a3b8;
font-size: 0.95rem;
font-size: 0.85rem;
user-select: none;
z-index: 2;
display: flex;
@ -196,3 +196,57 @@ pre.shiki, pre.astro-code,
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);
}

View File

@ -140,7 +140,7 @@ fn get_search_suggestions(search_index: &ArticleSearchIndex, query: &str) -> Vec
suggestion_type: SuggestionType::Completion,
frequency: *freq
});
} else if query.starts_with(&term_lower) || term_lower.contains(&query) {
} else if term_lower.contains(&query) {
// 包含关系,作为纠正建议
candidates.push(SuggestionCandidate {
text: term.clone(),