优化搜索数据规则,优化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,
|
||||
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 = () => (
|
||||
|
@ -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
@ -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
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
|
Loading…
Reference in New Issue
Block a user