前言
因为个人原因,想要在大纲视图下,把鼠标放在标题上可以显示出,这个标题到下个标题之间的字数,但无人回应,所以只好自己想办法,花了几天的时间研究,最后终于用 AI 跑了一段能用的代码。有同样需求的可以试试,
虽然我这里是没有什么问题吧,但是还是建议大家慎重使用,及时做好备份,如果有问题,欢迎各位指出。
目前仅适用于纯文本文档,有图片和表格的正在修改。
最后感谢 JeffreyChen 大佬告诉我元素的名字,不然怕是再跑一天,ai 也跑不出代码。
演示视频
代码
// 大纲字数统计代码片段
// 作者:AI Assistant
// 版本:1.0.4
// 添加全局配置
const CONFIG = {
HOVER_DELAY: 200, // 悬停延迟(毫秒)
CACHE_TIMEOUT: 10000, // 缓存超时(毫秒)
DEBUG: false // 调试模式
};
// 防抖函数
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 添加样式
const style = document.createElement('style');
style.textContent = `
.outline-count {
margin-left: 8px;
color: var(--b3-theme-on-surface);
font-size: 12px;
opacity: 0.8;
transition: opacity 0.2s ease;
background-color: var(--b3-theme-background);
padding: 0 4px;
border-radius: 4px;
}
.b3-list-item:hover .outline-count {
opacity: 1;
}
`;
document.head.appendChild(style);
// 添加缓存系统
const cache = {
wordCounts: new Map(), // 缓存字数统计结果
blockContents: new Map(), // 缓存块内容
nextHeadings: new Map(), // 缓存下一个标题信息
cacheTimeout: CONFIG.CACHE_TIMEOUT, // 缓存有效期
// 获取缓存
get(key, type = 'wordCounts') {
const cache = this[type];
const item = cache.get(key);
if (!item) return null;
// 检查缓存是否过期
if (Date.now() - item.timestamp > this.cacheTimeout) {
cache.delete(key);
return null;
}
return item.value;
},
// 设置缓存
set(key, value, type = 'wordCounts') {
this[type].set(key, {
value,
timestamp: Date.now()
});
},
// 清除特定块的所有相关缓存
clearBlockCache(blockId) {
this.wordCounts.delete(blockId);
this.blockContents.delete(blockId);
this.nextHeadings.delete(blockId);
},
// 清除所有缓存
clearAll() {
this.wordCounts.clear();
this.blockContents.clear();
this.nextHeadings.clear();
}
};
// 获取块的内容
async function getBlockContent(blockId) {
try {
// 检查缓存
const cachedContent = cache.get(blockId, 'blockContents');
if (cachedContent !== null) {
return cachedContent;
}
const response = await fetch('/api/block/getBlockKramdown', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: blockId
})
});
const data = await response.json();
const content = data.data?.kramdown || '';
// 缓存结果
cache.set(blockId, content, 'blockContents');
return content;
} catch (error) {
return '';
}
}
// 计算字数
function countWords(text) {
if (typeof text !== 'string') {
return 0;
}
// 移除Markdown标记
text = text.replace(/^#+\s+/gm, ''); // 移除标题标记
text = text.replace(/\*\*([^*]+)\*\*/g, '$1'); // 移除加粗
text = text.replace(/\*([^*]+)\*/g, '$1'); // 移除斜体
text = text.replace(/~~([^~]+)~~/g, '$1'); // 移除删除线
text = text.replace(/`([^`]+)`/g, '$1'); // 移除代码
text = text.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1'); // 移除链接
text = text.replace(/!\[([^\]]*)\]\([^\)]+\)/g, ''); // 移除图片
text = text.replace(/\{:[^}]*\}/g, ''); // 移除属性标记
// 移除特殊字符和空白
text = text.replace(/[\r\n\s]+/g, '');
return text.length;
}
// 获取子块列表
async function getSubBlocks(blockId) {
try {
const response = await fetch('/api/block/getChildBlocks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: blockId
})
});
const data = await response.json();
return data.data || [];
} catch (error) {
return [];
}
}
// 获取下一个标题的ID
async function getNextHeadingId(currentId) {
try {
// 获取文档信息
const response = await fetch('/api/block/getDocInfo', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: currentId
})
});
const data = await response.json();
if (!data.data) {
return null;
}
// 获取大纲
const outlineResponse = await fetch('/api/outline/getDocOutline', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: data.data.rootID
})
});
const outlineData = await outlineResponse.json();
if (!outlineData.data) {
return null;
}
// 找到当前标题的下一个标题
let foundCurrent = false;
for (let i = 0; i < outlineData.data.length; i++) {
if (foundCurrent) {
return outlineData.data[i].id;
}
if (outlineData.data[i].id === currentId) {
foundCurrent = true;
}
}
return null;
} catch (error) {
return null;
}
}
// 计算区间字数
async function calculateSectionWordCount(blockId) {
try {
// 检查缓存
const cachedCount = cache.get(blockId);
if (cachedCount !== null) {
return cachedCount;
}
// 获取子块列表
const subBlocks = await getSubBlocks(blockId);
let totalCount = 0;
// 获取下一个标题ID(使用缓存)
const nextHeadingId = await getNextHeadingId(blockId);
// 使用Promise.all优化并行请求
const contents = await Promise.all(
subBlocks
.filter(block => block.id !== nextHeadingId)
.map(block => getBlockContent(block.id))
);
totalCount = contents.reduce((sum, content) => sum + countWords(content), 0);
// 缓存结果
cache.set(blockId, totalCount);
return totalCount;
} catch (error) {
return 0;
}
}
// 显示字数
function showCount(item, count) {
const countElement = document.createElement('span');
countElement.className = 'outline-count';
countElement.textContent = `${count}`;
item.appendChild(countElement);
}
// 设置大纲悬浮事件
async function setupOutlineHover() {
const outlineSelectors = [
'.sy__outline .b3-list-item[data-node-id]', // 大纲面板中的项目
'.dock__layout .b3-list-item[data-node-id]' // 侧边栏大纲中的项目
];
const items = document.querySelectorAll(outlineSelectors.join(','));
// 确保tooltip容器存在
let tooltipElement = document.getElementById('tooltip');
if (!tooltipElement) {
tooltipElement = document.createElement('div');
tooltipElement.id = 'tooltip';
tooltipElement.className = 'tooltip fn__none';
document.body.appendChild(tooltipElement);
}
items.forEach(item => {
if (!item || !document.body.contains(item)) {
return;
}
// 检查元素是否已经初始化过
if (item.hasAttribute('data-outline-events-initialized')) {
const oldHandlers = item._outlineHandlers;
if (oldHandlers) {
item.removeEventListener('mouseenter', oldHandlers.handleMouseEnter);
item.removeEventListener('mouseleave', oldHandlers.handleMouseLeave);
// 清除可能存在的定时器
if (oldHandlers.timer) {
clearTimeout(oldHandlers.timer);
}
}
}
// 标记元素已初始化
item.setAttribute('data-outline-events-initialized', 'true');
const createEventHandler = (currentItem) => {
let isCalculating = false;
let timer = null;
const handleMouseEnter = async (event) => {
if (!currentItem || !document.body.contains(currentItem)) {
return;
}
const blockId = currentItem.getAttribute('data-node-id');
if (!blockId) {
return;
}
// 清除之前的定时器
if (timer) {
clearTimeout(timer);
}
// 添加新的定时器
timer = setTimeout(async () => {
if (isCalculating) return;
isCalculating = true;
try {
const count = await calculateSectionWordCount(blockId);
if (!document.body.contains(currentItem)) return;
const text = currentItem.textContent.trim();
const tooltipElement = document.getElementById('tooltip');
if (tooltipElement) {
tooltipElement.innerHTML = `${text} ${count}`; // 6个空格
tooltipElement.classList.remove('fn__none');
const rect = currentItem.getBoundingClientRect();
const tooltipRect = tooltipElement.getBoundingClientRect();
const left = Math.min(
rect.right + 8,
window.innerWidth - tooltipRect.width - 8
);
const top = Math.max(0, Math.min(
rect.top + (rect.height - tooltipRect.height) / 2,
window.innerHeight - tooltipRect.height
));
tooltipElement.style.left = `${left}px`;
tooltipElement.style.top = `${top}px`;
}
} catch (error) {
return;
} finally {
isCalculating = false;
}
}, CONFIG.HOVER_DELAY);
};
const handleMouseLeave = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
const tooltipElement = document.getElementById('tooltip');
if (tooltipElement) {
tooltipElement.classList.add('fn__none');
}
};
return { handleMouseEnter, handleMouseLeave, timer };
};
// 为每个元素创建独立的事件处理函数
const handlers = createEventHandler(item);
// 保存处理函数的引用以便后续移除
item._outlineHandlers = handlers;
// 添加事件监听器
item.addEventListener('mouseenter', handlers.handleMouseEnter);
item.addEventListener('mouseleave', handlers.handleMouseLeave);
});
}
// 添加tooltip样式
const tooltipStyle = document.createElement('style');
tooltipStyle.textContent = `
.tooltip {
position: fixed;
z-index: 1000000;
padding: 4px 8px;
font-size: 12px;
font-weight: normal;
-webkit-font-smoothing: subpixel-antialiased;
color: var(--b3-tooltips-color);
word-wrap: break-word;
background-color: var(--b3-tooltips-background);
border-radius: var(--b3-border-radius);
line-height: 17px;
max-width: 320px;
animation-duration: 150ms;
animation-fill-mode: both;
animation-name: zoomIn;
max-height: 90vh;
overflow: auto;
box-sizing: border-box;
white-space: pre; // 修改为pre以保持空格
pointer-events: none;
}
`;
document.head.appendChild(tooltipStyle);
// 创建观察器
const observer = new MutationObserver(debounce((mutations) => {
let shouldSetup = false;
for (const mutation of mutations) {
if (mutation.addedNodes.length || mutation.type === 'childList') {
shouldSetup = true;
break;
}
}
if (shouldSetup) {
setupOutlineHover();
}
}, 100));
// 监听文档变化以清除缓存
const docObserver = new MutationObserver(debounce((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
// 如果发生了文档内容变化,清除所有缓存
cache.clearAll();
break;
}
}
}, 1000));
// 检查大纲面板
function checkOutlinePanel() {
const outlineSelectors = [
'.sy__outline',
'.dock__layout' // 只监听大纲面板相关的容器
];
try {
for (const selector of outlineSelectors) {
const panel = document.querySelector(selector);
if (panel) {
observer.observe(panel, {
childList: true,
subtree: true
});
setupOutlineHover();
return true;
}
}
setTimeout(checkOutlinePanel, 1000);
return false;
} catch (error) {
setTimeout(checkOutlinePanel, 1000);
return false;
}
}
// 初始化函数
function initialize() {
try {
// 检查是否已经初始化
if (window.outlineCount && window.outlineCount.initialized) {
return;
}
// 确保样式只添加一次
if (!document.querySelector('style[data-outline-count]')) {
const style = document.createElement('style');
style.setAttribute('data-outline-count', 'true');
style.textContent = `
.outline-count {
margin-left: 8px;
color: var(--b3-theme-on-surface);
font-size: 12px;
opacity: 0.8;
transition: opacity 0.2s ease;
background-color: var(--b3-theme-background);
padding: 0 4px;
border-radius: 4px;
}
.b3-list-item:hover .outline-count {
opacity: 1;
}
`;
document.head.appendChild(style);
}
// 启动检查大纲面板
checkOutlinePanel();
// 监听文档变化
const editor = document.querySelector('.protyle-content');
if (editor) {
docObserver.observe(editor, {
childList: true,
subtree: true
});
}
// 标记为已初始化
window.outlineCount = window.outlineCount || {};
window.outlineCount.initialized = true;
window.outlineCount.cache = cache;
// 添加清理函数
window.addEventListener('pagehide', () => {
observer.disconnect();
docObserver.disconnect();
cache.clearAll();
// 清理所有事件监听器和属性
document.querySelectorAll('[data-word-count-initialized]')
.forEach(item => {
item.removeAttribute('data-word-count-initialized');
// 通过克隆节点清除所有事件监听器
item.replaceWith(item.cloneNode(true));
});
});
} catch (error) {
return;
}
}
// 确保在DOM加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
// 将关键函数挂载到window对象上(如果需要外部访问)
window.outlineCount = {
initialize,
setupOutlineHover,
checkOutlinePanel
};
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于