diff --git a/src/assets/wasm/search/search_wasm_bg.wasm b/src/assets/wasm/search/search_wasm_bg.wasm index 14ecec5..1ab0863 100644 Binary files a/src/assets/wasm/search/search_wasm_bg.wasm and b/src/assets/wasm/search/search_wasm_bg.wasm differ diff --git a/src/components/ArticleFilter.tsx b/src/components/ArticleFilter.tsx index a5d16fd..54dee3a 100644 --- a/src/components/ArticleFilter.tsx +++ b/src/components/ArticleFilter.tsx @@ -1092,12 +1092,20 @@ const ArticleFilter: React.FC = ({ 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 = () => ( diff --git a/src/components/Breadcrumb.astro b/src/components/Breadcrumb.astro index fce9d71..acc50e8 100644 --- a/src/components/Breadcrumb.astro +++ b/src/components/Breadcrumb.astro @@ -187,99 +187,101 @@ const breadcrumbs: Breadcrumb[] = pathSegments \ No newline at end of file diff --git a/src/components/Header.astro b/src/components/Header.astro index ac84403..8826b14 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -332,8 +332,85 @@ const navSelectorClassName = "mr-4"; diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 6bbf830..ae576aa 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -204,7 +204,6 @@ const Search: React.FC = ({ // 如果是取消的请求,不显示错误 if (err instanceof Error && (err.name === 'AbortError' || err.message.includes('aborted'))) { - console.log('索引加载请求被取消:', err.message); return; } @@ -239,7 +238,7 @@ const Search: React.FC = ({ } }; - 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 = ({ // 处理点击外部关闭搜索结果和建议 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 = ({ setSuggestions([]); setInlineSuggestion(prev => ({ ...prev, visible: false })); setSelectedSuggestionIndex(0); // 重置选中索引 + console.log("[建议] 没有符合条件的查询或模块未加载"); return; } @@ -324,6 +341,7 @@ const Search: React.FC = ({ } const searchResult = JSON.parse(result) as SearchResult; + // 检查组件是否仍然挂载 if (!isMountedRef.current) return; @@ -347,6 +365,7 @@ const Search: React.FC = ({ const firstSuggestion = searchResult.suggestions[0]; if (firstSuggestion) { + setInlineSuggestion(prev => ({ ...prev, text: firstSuggestion.text, @@ -363,7 +382,7 @@ const Search: React.FC = ({ // 检查组件是否仍然挂载 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 = ({ // 修改处理键盘导航的函数,增加上下箭头键切换建议 const handleKeyDown = (e: React.KeyboardEvent) => { + // Tab键处理内联建议补全 if (e.key === "Tab" && inlineSuggestion.visible && inlineSuggestion.text) { e.preventDefault(); // 阻止默认的Tab行为 + e.stopPropagation(); // 防止事件冒泡 completeInlineSuggestion(); return; } @@ -532,50 +553,13 @@ const Search: React.FC = ({ } }; - 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 = ({ }); } }; + + // 自动补全内联建议 - 不使用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 = ({ 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 = ({ 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 = ({ matchedText: "", suggestionText: "" }); - setShowResults(false); + + // 保持结果区域可见,但无内容 + setShowResults(true); setCurrentPage(1); setHasMoreResults(true); + // 确保输入框保持焦点 if (searchInputRef.current) { searchInputRef.current.focus(); } @@ -844,8 +956,8 @@ const Search: React.FC = ({ } }; - 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 = ({ }; }, [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 = ({ 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 = ({ }} > {/* 修改显示方式,确保与输入文本对齐,同时支持响应式布局 */} -
{/* 使用与输入框相同的水平内边距,添加溢出隐藏 */} +
{/* 使用与输入框相同的水平内边距,添加溢出隐藏 */} {/* 纠正建议和补全建议都显示在已输入内容的右侧 */} <> {/* 创建与输入文本宽度完全相等的不可见占位 */}
- {query} + {query}
{/* 显示建议的剩余部分 */} -
+
{inlineSuggestion.suggestionText} @@ -1135,43 +1243,89 @@ const Search: React.FC = ({ )} {/* 搜索图标 */} -
- +
+
{/* 加载指示器或清除按钮 */} -
+
{isLoading ? ( -
+
) : query ? ( <> {inlineSuggestion.visible && inlineSuggestion.text && (
{ + // 阻止冒泡和默认行为 + 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' }} > -
+
TAB
@@ -1179,11 +1333,11 @@ const Search: React.FC = ({ ) : error ? (
-
+
) : isLoadingIndex ? (
-
+
) : null}
diff --git a/src/components/ThemeToggle.astro b/src/components/ThemeToggle.astro index c8b49b1..9bdbed7 100644 --- a/src/components/ThemeToggle.astro +++ b/src/components/ThemeToggle.astro @@ -116,21 +116,20 @@ const { \ No newline at end of file diff --git a/src/pages/articles/[...id].astro b/src/pages/articles/[...id].astro index 99339ad..ff834b8 100644 --- a/src/pages/articles/[...id].astro +++ b/src/pages/articles/[...id].astro @@ -473,356 +473,471 @@ const tableOfContents = generateTableOfContents(headings); diff --git a/src/styles/articles-code.css b/src/styles/articles-code.css index 9bc3343..ba96530 100644 --- a/src/styles/articles-code.css +++ b/src/styles/articles-code.css @@ -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); +} + diff --git a/wasm/search/src/lib.rs b/wasm/search/src/lib.rs index e28a509..fa958f8 100644 --- a/wasm/search/src/lib.rs +++ b/wasm/search/src/lib.rs @@ -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(),