优化搜索数据规则,优化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 isArticlePage = document.querySelector('.back-button');
|
||||||
// 存储事件监听器,便于统一清理
|
if (!isArticlePage) {
|
||||||
const listeners = [];
|
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 || [];
|
|
||||||
otherClickHandlers.forEach(handler => {
|
|
||||||
try {
|
|
||||||
button.removeEventListener('click', handler);
|
|
||||||
} catch (e) {
|
|
||||||
// 忽略错误
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 重置处理函数数组
|
|
||||||
button.__backButtonClickHandlers = [];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 集中管理所有事件监听器
|
||||||
|
const allListeners = [];
|
||||||
|
|
||||||
|
// 单独保存清理事件的监听器引用
|
||||||
|
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 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化函数
|
|
||||||
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 window.swup !== 'undefined') {
|
||||||
window[eventKey] = callback;
|
addListener(document, 'swup:contentReplaced', pageLoadHandler);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置页面转换事件监听
|
// 判断DOM是否已加载
|
||||||
setupPageTransitionEvents();
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", init, { once: true });
|
||||||
// 在页面加载后初始化
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
init();
|
|
||||||
}, { once: true });
|
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => {
|
init();
|
||||||
init();
|
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,6 +341,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()) {
|
||||||
@ -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) => {
|
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 ${
|
||||||
|
// 根据建议类型调整最大宽度
|
||||||
|
inlineSuggestion.type === 'correction'
|
||||||
|
? 'max-w-[calc(100%-1.25rem)]' // 纠正建议给予更多空间,但仍然保留一些边距
|
||||||
|
: 'max-w-[80%]' // 补全建议使用固定比例
|
||||||
|
}`}>
|
||||||
<span
|
<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'
|
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'
|
: '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,356 +473,471 @@ 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;
|
|
||||||
|
// 创建一次性事件处理函数
|
||||||
copyButtons.forEach(button => {
|
const beforeSwapHandler = () => {
|
||||||
addListener(button, 'click', async () => {
|
console.log(`[文章脚本:${scriptInstanceId}] astro:before-swap 触发,执行自销毁`);
|
||||||
try {
|
selfDestruct();
|
||||||
// 使用Base64解码获取代码文本
|
};
|
||||||
const encodedCode = button.getAttribute('data-code');
|
|
||||||
if (!encodedCode) return;
|
const beforeUnloadHandler = () => {
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] beforeunload 触发,执行自销毁`);
|
||||||
// 解码并复制到剪贴板
|
selfDestruct();
|
||||||
const code = atob(encodedCode);
|
};
|
||||||
await navigator.clipboard.writeText(code);
|
|
||||||
|
// 添加清理事件监听器并保存引用
|
||||||
const originalHTML = button.innerHTML;
|
document.addEventListener("astro:before-swap", beforeSwapHandler, { once: true });
|
||||||
button.classList.add('copied');
|
window.addEventListener("beforeunload", beforeUnloadHandler, { once: true });
|
||||||
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>
|
cleanupListeners.push(
|
||||||
</svg>
|
{ element: document, eventType: "astro:before-swap", handler: beforeSwapHandler, options: { once: true } },
|
||||||
已复制
|
{ element: window, eventType: "beforeunload", handler: beforeUnloadHandler, options: { once: true } }
|
||||||
`;
|
);
|
||||||
|
}
|
||||||
setTimeout(() => {
|
|
||||||
button.classList.remove('copied');
|
// 初始化所有功能
|
||||||
button.innerHTML = originalHTML;
|
function initializeFeatures() {
|
||||||
}, 2000);
|
console.log(`[文章脚本:${scriptInstanceId}] 开始初始化各功能`);
|
||||||
} catch (err) {
|
|
||||||
console.error('复制失败:', err);
|
// 1. 代码块复制功能
|
||||||
button.innerHTML = `
|
function setupCodeCopy() {
|
||||||
<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">
|
console.log(`[文章脚本:${scriptInstanceId}] 初始化代码复制功能`);
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
const copyButtons = document.querySelectorAll('.code-block-copy');
|
||||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
if (copyButtons.length === 0) {
|
||||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
console.log(`[文章脚本:${scriptInstanceId}] 未找到代码复制按钮`);
|
||||||
</svg>
|
return;
|
||||||
失败
|
}
|
||||||
`;
|
|
||||||
setTimeout(() => {
|
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 = `
|
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">
|
||||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
<path d="M20 6L9 17l-5-5"></path>
|
||||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
|
||||||
</svg>
|
</svg>
|
||||||
复制
|
已复制
|
||||||
`;
|
`;
|
||||||
}, 2000);
|
|
||||||
}
|
setTimeout(() => {
|
||||||
});
|
button.classList.remove('copied');
|
||||||
});
|
button.innerHTML = originalHTML;
|
||||||
}
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
// 4. 设置阅读进度条
|
console.error(`[文章脚本:${scriptInstanceId}] 复制失败:`, err);
|
||||||
function setupProgressBar() {
|
button.innerHTML = `
|
||||||
const progressBar = document.getElementById("progress-bar");
|
<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">
|
||||||
const backToTopButton = document.getElementById("back-to-top");
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||||
if (!progressBar) return;
|
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||||
|
</svg>
|
||||||
function updateReadingProgress() {
|
失败
|
||||||
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
`;
|
||||||
const scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
|
setTimeout(() => {
|
||||||
const progress = (scrollTop / scrollHeight) * 100;
|
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">
|
||||||
progressBar.style.width = `${progress}%`;
|
<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>
|
||||||
if (backToTopButton) {
|
</svg>
|
||||||
if (scrollTop > 300) {
|
复制
|
||||||
backToTopButton.classList.add(
|
`;
|
||||||
"opacity-100",
|
}, 2000);
|
||||||
"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",
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateReadingProgress();
|
// 2. 阅读进度条
|
||||||
}
|
function setupProgressBar() {
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 初始化阅读进度条`);
|
||||||
// 5. 管理目录交互
|
const progressBar = document.getElementById("progress-bar");
|
||||||
function setupTableOfContents() {
|
const backToTopButton = document.getElementById("back-to-top");
|
||||||
const tocContent = document.getElementById("toc-content");
|
|
||||||
const tocPanel = document.querySelector("#toc-panel");
|
|
||||||
|
|
||||||
if (!tocPanel || !tocContent) return;
|
if (!progressBar) {
|
||||||
|
console.warn(`[文章脚本:${scriptInstanceId}] 未找到进度条元素`);
|
||||||
// 检查窗口大小调整目录面板显示
|
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");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
addListener(window, "resize", checkTocVisibility);
|
function updateReadingProgress() {
|
||||||
checkTocVisibility();
|
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
||||||
|
const scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
|
||||||
|
const progress = (scrollTop / scrollHeight) * 100;
|
||||||
|
|
||||||
// 处理目录链接点击跳转
|
progressBar.style.width = `${progress}%`;
|
||||||
const tocLinks = tocContent.querySelectorAll("a");
|
|
||||||
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 (backToTopButton) {
|
||||||
if (targetElement) {
|
if (scrollTop > 300) {
|
||||||
const offset = 100;
|
backToTopButton.classList.add(
|
||||||
const targetPosition = targetElement.getBoundingClientRect().top + window.scrollY - offset;
|
"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({
|
window.scrollTo({
|
||||||
top: targetPosition,
|
top: 0,
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
targetElement.classList.add("bg-primary-50", "dark:bg-primary-900/20");
|
// 初始更新一次进度条
|
||||||
setTimeout(() => {
|
updateReadingProgress();
|
||||||
targetElement.classList.remove("bg-primary-50", "dark:bg-primary-900/20");
|
}
|
||||||
}, 2000);
|
|
||||||
}
|
// 3. 目录交互
|
||||||
});
|
function setupTableOfContents() {
|
||||||
});
|
console.log(`[文章脚本:${scriptInstanceId}] 初始化目录交互`);
|
||||||
|
const tocContent = document.getElementById("toc-content");
|
||||||
|
const tocPanel = document.querySelector("#toc-panel");
|
||||||
|
|
||||||
// 监听滚动以更新当前活动的目录项
|
if (!tocPanel || !tocContent) {
|
||||||
const article = document.querySelector("article");
|
console.warn(`[文章脚本:${scriptInstanceId}] 未找到目录面板或内容`);
|
||||||
if (!article) return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let ticking = false;
|
// 检查窗口大小调整目录面板显示
|
||||||
|
function checkTocVisibility() {
|
||||||
function updateActiveHeading() {
|
if (window.innerWidth < 1536) {
|
||||||
const headings = Array.from(article.querySelectorAll("h1, h2, h3, h4, h5, h6"));
|
tocPanel.classList.add("hidden");
|
||||||
const tocLinks = Array.from(tocContent.querySelectorAll("a"));
|
tocPanel.classList.remove("2xl:block");
|
||||||
|
|
||||||
// 清除所有活动状态
|
|
||||||
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 {
|
} else {
|
||||||
break;
|
tocPanel.classList.remove("hidden");
|
||||||
|
tocPanel.classList.add("2xl:block");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 高亮当前标题对应的目录项
|
addListener(window, "resize", checkTocVisibility);
|
||||||
if (currentHeading) {
|
checkTocVisibility();
|
||||||
const id = currentHeading.getAttribute('id');
|
|
||||||
if (id) {
|
// 处理目录链接点击跳转
|
||||||
const activeLink = tocLinks.find(
|
const tocLinks = tocContent.querySelectorAll("a");
|
||||||
(link) => link.getAttribute("href") === `#${id}`
|
console.log(`[文章脚本:${scriptInstanceId}] 找到目录链接数量:`, tocLinks.length);
|
||||||
);
|
|
||||||
if (activeLink) {
|
tocLinks.forEach(link => {
|
||||||
// 高亮当前目录项
|
addListener(link, "click", (e) => {
|
||||||
activeLink.classList.add("text-primary-600", "dark:text-primary-400", "font-medium");
|
e.preventDefault();
|
||||||
|
const targetId = link.getAttribute("href")?.substring(1);
|
||||||
// 可选: 确保当前激活的目录项在可视区域内
|
if (!targetId) return;
|
||||||
const tocContainer = tocContent.querySelector('ul');
|
|
||||||
if (tocContainer) {
|
const targetElement = document.getElementById(targetId);
|
||||||
const linkOffsetTop = activeLink.offsetTop;
|
if (targetElement) {
|
||||||
const containerScrollTop = tocContainer.scrollTop;
|
const offset = 100;
|
||||||
const containerHeight = tocContainer.clientHeight;
|
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 ||
|
const tocContainer = tocContent.querySelector('ul');
|
||||||
linkOffsetTop > containerScrollTop + containerHeight) {
|
if (tocContainer) {
|
||||||
tocContainer.scrollTop = linkOffsetTop - containerHeight / 2;
|
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", () => {
|
// 4. Mermaid图表渲染
|
||||||
if (!ticking) {
|
function setupMermaid() {
|
||||||
window.requestAnimationFrame(() => {
|
console.log(`[文章脚本:${scriptInstanceId}] 检查Mermaid图表`);
|
||||||
updateActiveHeading();
|
// 查找所有mermaid代码块
|
||||||
ticking = false;
|
const mermaidBlocks = document.querySelectorAll(
|
||||||
});
|
'pre.language-mermaid, pre > code.language-mermaid, .mermaid'
|
||||||
ticking = true;
|
);
|
||||||
|
|
||||||
|
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控制样式
|
console.log(`[文章脚本:${scriptInstanceId}] 找到Mermaid图表数量:`, mermaidBlocks.length);
|
||||||
window.mermaid.initialize({
|
|
||||||
startOnLoad: false,
|
|
||||||
theme: 'default',
|
|
||||||
securityLevel: 'loose'
|
|
||||||
});
|
|
||||||
|
|
||||||
// 将所有mermaid代码块转换为可渲染的格式
|
// 动态加载mermaid库
|
||||||
mermaidBlocks.forEach((block, index) => {
|
const script = document.createElement('script');
|
||||||
// 获取mermaid代码
|
script.src = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js';
|
||||||
let code = '';
|
|
||||||
|
script.onload = function() {
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] Mermaid库加载成功`);
|
||||||
|
|
||||||
// 检查元素类型并相应处理
|
if (!window.mermaid) {
|
||||||
if (block.tagName === 'CODE' && block.classList.contains('language-mermaid')) {
|
console.error(`[文章脚本:${scriptInstanceId}] Mermaid库加载后window.mermaid不存在`);
|
||||||
// 处理 code.language-mermaid 元素
|
return;
|
||||||
code = block.textContent || '';
|
}
|
||||||
const pre = block.closest('pre');
|
|
||||||
if (pre) {
|
// 初始化mermaid配置
|
||||||
// 创建新的div元素替换整个pre
|
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');
|
const div = document.createElement('div');
|
||||||
div.className = 'mermaid';
|
div.className = 'mermaid';
|
||||||
div.id = 'mermaid-diagram-' + index;
|
div.id = 'mermaid-diagram-' + index;
|
||||||
div.textContent = code;
|
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);
|
// 初始化渲染
|
||||||
}
|
try {
|
||||||
};
|
console.log(`[文章脚本:${scriptInstanceId}] 开始渲染Mermaid图表`);
|
||||||
|
window.mermaid.run().catch(err => {
|
||||||
script.onerror = function() {
|
console.error(`[文章脚本:${scriptInstanceId}] Mermaid渲染出错:`, err);
|
||||||
console.error('加载Mermaid库失败');
|
});
|
||||||
// 显示错误信息
|
} catch (error) {
|
||||||
mermaidBlocks.forEach(block => {
|
console.error(`[文章脚本:${scriptInstanceId}] 初始化Mermaid渲染失败:`, error);
|
||||||
if (block.tagName === 'CODE') block = block.closest('pre');
|
|
||||||
if (block) {
|
|
||||||
block.innerHTML = '<div class="mermaid-error-message">无法加载Mermaid图表库</div>';
|
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
};
|
|
||||||
|
script.onerror = function() {
|
||||||
document.head.appendChild(script);
|
console.error(`[文章脚本:${scriptInstanceId}] 加载Mermaid库失败`);
|
||||||
|
// 显示错误信息
|
||||||
// 添加到清理列表,确保后续页面跳转时能删除脚本
|
mermaidBlocks.forEach(block => {
|
||||||
listeners.push({
|
if (block.tagName === 'CODE') block = block.closest('pre');
|
||||||
element: script,
|
if (block) {
|
||||||
eventType: 'remove',
|
block.innerHTML = '<div class="mermaid-error-message">无法加载Mermaid图表库</div>';
|
||||||
handler: () => {
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
document.head.appendChild(script);
|
||||||
|
|
||||||
|
// 添加Mermaid清理任务
|
||||||
|
customCleanupTasks.push(() => {
|
||||||
|
console.log(`[文章脚本:${scriptInstanceId}] 执行Mermaid特殊清理`);
|
||||||
|
|
||||||
|
// 移除脚本标签
|
||||||
if (script.parentNode) {
|
if (script.parentNode) {
|
||||||
script.parentNode.removeChild(script);
|
script.parentNode.removeChild(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除全局mermaid对象
|
// 清除全局mermaid对象
|
||||||
if (window.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相关元素
|
// 移除页面上可能留下的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() {
|
|
||||||
if (!document.querySelector("article")) return;
|
|
||||||
|
|
||||||
setupCodeCopy(); // 只保留代码复制功能
|
// 启动所有功能
|
||||||
|
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