[js] 思源笔记可移动的导航大纲 js 代码片段

本贴最后更新于 298 天前,其中的信息可能已经时异事殊

导航目录大体功能:(使用方法:把代码放进 js 代码片段,粘贴完注意第 8 条)

  1. 随着笔记标签页的切换,导航内容也能相应切换
  2. 目录字体大小可调
  3. 可折叠起来
  4. 导航目录过长,可滚动
  5. 鼠标点击目录,可以导航到正文相应位置
  6. 鼠标悬停在目录上,可弹出浮窗
  7. 鼠标单击块,浮动大纲中对应的标题内容颜色会改变。再次点击会找上一级标题
  8. image.png

看下图,应该能明白。更新时间 20250227,修复了一些 bug

screenshots.gif

// ========== 工具函数 ==========
// 使用原生DOMParser解析HTML并提取纯文本内容
const parseHtmlToText = (html) => {
    return html.replace(/<[^>]+>/g, '').replace(/ /g, ' '); // 注意,使用时要在 后面加一个分号;
};

// 通用高亮函数,返回是否匹配成功
function highlightOutlineElements(outlineContent, nodeId, textContent) {
    const outlineElements = outlineContent.querySelectorAll("[data-href]");
    let isMatched = false; // 用于标记是否找到匹配项
    outlineElements.forEach((element) => {
        const href = element.getAttribute('data-href');
        const hrefId = href.split('/').pop();
        const isMatch = nodeId ? hrefId === nodeId : element.textContent.trim() === textContent;

        // 只在必要时修改样式
        if (isMatch && element.style.backgroundColor !== 'green') {
            element.style.backgroundColor = 'green'; // 高亮背景颜色
            isMatched = true; // 标记匹配成功
        } else if (!isMatch && element.style.backgroundColor === 'green') {
            element.style.backgroundColor = ''; // 恢复默认背景颜色
        }
    });
    return isMatched; // 返回是否匹配成功
}

// ========== 数据获取函数 ==========
// 获取文档信息,包括 rootID
async function getRootID({ id }) {
    const response = await fetch(`/api/block/getDocInfo`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({ id }),
    });
    const data = await response.json();
    return data.data.rootID; // 直接返回 rootID
}

// 获取文档大纲的函数
const getDocOutline = async (docId) => {
    try {
        const response = await fetch(`/api/outline/getDocOutline`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ id: docId }),
        });
        if (!response.ok) {
            throw new Error(`网络请求失败:${response.status}`);
        }
        const data = await response.json();
        if (data.code === 0) {
            return data.data;
        } else {
            console.error('获取文档目录结构失败:', data.msg);
            return null;
        }
    } catch (error) {
        console.error('获取文档目录结构失败:', error.message);
        return null;
    }
};

// ========== 大纲处理函数 ==========
// 收集大纲标题的函数
const collectTitles = (data) => {
    let titles = [];
    for (let item of data) {
        let text = (item.name || item.content).trim();

        // 解析HTML为纯文本
        const parsedText = parseHtmlToText(text);
        titles.push({
            text,
            parsedText,
            id: item.id,
            depth: item.depth,
            needParse: text !== parsedText,
        });

        // 递归处理子标题
        if (item.count > 0) {
            titles = titles.concat(collectTitles(item.blocks ?? item.children));
        }
    }
    return titles;
};

// 生成大纲内容的函数
const generateOutline = async (nodeId, outlineContent) => {
    console.log("开始生成大纲,节点 ID:", nodeId); // 调试日志
    try {
        if (!nodeId) {
            outlineContent.innerHTML = "<li>未找到有效节点,请点击文档内容。</li>";
            return;
        }

        // 检查是否需要重新生成大纲
        const currentOutlineNodeId = outlineContent.getAttribute('data-current-node-id');
        if (currentOutlineNodeId === nodeId) {
            console.log("大纲已存在,无需重新生成"); // 调试日志
            return;
        }

        const docOutline = await getDocOutline(nodeId); // 获取文档大纲
        if (!docOutline) {
            outlineContent.innerHTML = "<li>无法获取文档目录结构。</li>";
            return;
        }

        const titles = collectTitles(docOutline); // 收集大纲标题
        const fragment = document.createDocumentFragment(); // 创建文档片段

        // 遍历标题并生成列表项
        titles.forEach(title => {
            const listItem = document.createElement("li");
            const link = document.createElement("span");
            link.setAttribute("data-type", "a");
            link.setAttribute("data-href", `siyuan://blocks/${title.id}`);

            // 使用 textContent 设置内容,避免HTML被解析
            link.textContent = title.parsedText;

            // 设置链接样式
            Object.assign(link.style, {
                color: "#000",
                whiteSpace: "nowrap",
                overflow: "hidden",
                textOverflow: "ellipsis",
                display: "inline-block",
                maxWidth: "100%",
            });

            // 添加链接点击事件
            link.addEventListener("mousedown", (e) => {
                if (e.button === 0) {
                    e.preventDefault();
                    const selection = window.getSelection();
                    const range = document.createRange();
                    range.selectNodeContents(link);
                    selection.removeAllRanges();
                    selection.addRange(range);
                }
            });

            link.addEventListener('click', function (event) {
                event.preventDefault();
                const href = this.getAttribute("data-href");
                window.open(href, '_blank');
            });

            // 设置列表项的缩进
            listItem.style.paddingLeft = `${title.depth * 15}px`;
            listItem.appendChild(link);
            fragment.appendChild(listItem);
        });

        // 清空并更新大纲内容
        outlineContent.innerHTML = "";
        outlineContent.appendChild(fragment);

        // 更新当前大纲的节点 ID
        outlineContent.setAttribute('data-current-node-id', nodeId);
        console.log("大纲生成完成"); // 调试日志
    } catch (error) {
        outlineContent.innerHTML = `<li>生成大纲失败:${error.message}</li>`;
    }
};

// ========== 页面交互函数 ==========
// 修改函数:根据 rootId 查找并返回所有兄弟元素的 title 文本数组
function getLastSiblingTitle(rootId) {
    const allBreadcrumbItems = document.querySelectorAll('.protyle-breadcrumb__item');
    for (const item of allBreadcrumbItems) {
        if (item.getAttribute('data-node-id') === rootId && item.classList.contains('protyle-breadcrumb__item--active')) {
            const parent = item.parentElement;
            const siblings = Array.from(parent.children).filter(sibling => sibling !== item);

            const titles = [];
            siblings.forEach(sibling => {
                const titleElement = sibling.querySelector('.protyle-breadcrumb__text');
                if (titleElement && titleElement.getAttribute('title')) {
                    titles.push(titleElement.getAttribute('title'));
                }
            });
            return titles; // 返回包含所有兄弟节点标题的数组
        }
    }
    return []; // 如果未找到符合条件的元素,返回空数组
}

// 处理 NodeHeading 类型
function handleNodeHeading(outlineContent, nodeId) {
    console.log("处理 NodeHeading 类型,节点 ID:", nodeId); // 调试日志
    highlightOutlineElements(outlineContent, nodeId, null);
}

// 处理非 NodeHeading 类型
function handleNonNodeHeading(outlineContent, textContentArray) {
    console.log("处理非 NodeHeading 类型,兄弟节点标题数组:", textContentArray); // 调试日志
    if (Array.isArray(textContentArray) && textContentArray.length > 0) {
        // 倒序遍历数组,依次尝试匹配
        for (let i = textContentArray.length - 1; i >= 0; i--) {
            const currentLine = textContentArray[i];
            if (highlightOutlineElements(outlineContent, null, currentLine)) {
                return; // 匹配成功后退出循环
            }
        }
    }
}

// 全局点击事件处理函数
const handleClick = async (e, outlineContent) => {
    console.log("点击事件触发,目标元素:", e.target); // 调试日志
    let target = e.target;
    while (target && target !== document && !target.hasAttribute('data-node-id')) {
        target = target.parentNode;
    }

    if (target && target.hasAttribute('data-node-id')) {
        const nodeId = target.getAttribute('data-node-id');
        try {
            // 获取 rootID
            const rootID = await getRootID({ id: nodeId });
            if (rootID) {
                console.log("开始生成大纲,节点 ID:", nodeId); // 调试日志
                await generateOutline(nodeId, outlineContent);
                if (target.getAttribute('data-type') === 'NodeHeading') {
                    handleNodeHeading(outlineContent, nodeId);
                } else {
                    const siblingTitles = getLastSiblingTitle(rootID);
                    if (siblingTitles.length > 0) {
                        handleNonNodeHeading(outlineContent, siblingTitles);
                    }
                }
            }
        } catch (error) {
            console.error('获取 rootID 失败:', error.message);
        }
    } else {
        outlineContent.innerHTML = "<li>未找到有效节点,请点击文档内容。</li>";
    }
};

// ========== UI 创建函数 ==========
// 创建大纲面板的函数
const createOutlinePanel = () => {
    const outlinePanel = document.createElement("div");
    Object.assign(outlinePanel.style, {
        position: "fixed",
        top: "100px",
        left: "1000px",
        width: "200px",
        height: "30px",
        background: "#f1f1f1",
        border: "1px solid #ccc",
        borderRadius: "5px",
        padding: "0",
        boxShadow: "0 0 10px rgba(0, 0, 0, 0.1)",
        zIndex: "1000",
        overflow: "hidden",
    });

    const topButtonsContainer = createTopButtonsContainer();
    outlinePanel.appendChild(topButtonsContainer);

    const outlineTitle = document.createElement("h3");
    outlineTitle.textContent = "大纲";
    Object.assign(outlineTitle.style, {
        margin: "0 0 10px",
    });
    outlinePanel.appendChild(outlineTitle);

    const outlineContent = document.createElement("ul");
    outlineContent.id = "outline-list";
    Object.assign(outlineContent.style, {
        listStyle: "none",
        padding: "0",
        margin: "0",
        fontSize: "14px",
        marginTop: "20px",
        overflowY: "auto",
        maxHeight: "340px",
    });
    outlineContent.setAttribute("draggable", "false");
    outlineContent.addEventListener("dragstart", (e) => e.preventDefault());
    outlinePanel.appendChild(outlineContent);

    const toggleButtonInstance = topButtonsContainer.querySelector('button:nth-child(2)');
    toggleButtonInstance.textContent = "展开";
    let isExpanded = false;
    toggleButtonInstance.addEventListener("click", () => {
        isExpanded = !isExpanded;
        outlinePanel.style.height = isExpanded ? "400px" : `${topButtonsContainer.offsetHeight}px`;
        toggleButtonInstance.textContent = isExpanded ? "折叠" : "展开";
    });

    enableDragging(outlinePanel, topButtonsContainer);

    return { outlinePanel, outlineContent };
};

// 创建顶部按钮容器的函数
const createTopButtonsContainer = () => {
    const topButtonsContainer = document.createElement("div");
    Object.assign(topButtonsContainer.style, {
        position: "absolute",
        top: "0",
        left: "0",
        right: "0",
        height: "20px",
        backgroundColor: "#f1f1f1",
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
        padding: "5px",
    });

    const showOutlineButton = document.createElement("button");
    showOutlineButton.textContent = "思源大纲";
    Object.assign(showOutlineButton.style, {
        padding: "5px",
        background: "#007bff",
        color: "#fff",
        border: "none",
        borderRadius: "5px",
        cursor: "pointer",
    });

    const toggleButtonElement = document.createElement("button");
    toggleButtonElement.textContent = "展开";
    Object.assign(toggleButtonElement.style, {
        padding: "5px",
        background: "#ccc",
        border: "none",
        borderRadius: "5px",
        cursor: "pointer",
    });

    topButtonsContainer.appendChild(showOutlineButton);
    topButtonsContainer.appendChild(toggleButtonElement);
    return topButtonsContainer;
};

// 创建字体大小调整按钮的函数
const createFontButtonsContainer = (outlineContent) => {
    const fontButtonsContainer = document.createElement("div");
    Object.assign(fontButtonsContainer.style, {
        display: "flex",
        gap: "5px",
    });

    const decreaseFontSizeButton = document.createElement("button");
    decreaseFontSizeButton.textContent = "-";
    Object.assign(decreaseFontSizeButton.style, {
        width: "20px",
        height: "20px",
        fontSize: "16px",
        border: "none",
        background: "#ccc",
        borderRadius: "50%",
    });
    decreaseFontSizeButton.addEventListener("click", () => {
        const currentSize = parseFloat(outlineContent.style.fontSize);
        outlineContent.style.fontSize = `${Math.max(currentSize - 1, 10)}px`;
    });

    const increaseFontSizeButton = document.createElement("button");
    increaseFontSizeButton.textContent = "+";
    Object.assign(increaseFontSizeButton.style, {
        width: "20px",
        height: "20px",
        fontSize: "16px",
        border: "none",
        background: "#ccc",
        borderRadius: "50%",
    });
    increaseFontSizeButton.addEventListener("click", () => {
        const currentSize = parseFloat(outlineContent.style.fontSize);
        outlineContent.style.fontSize = `${Math.min(currentSize + 1, 24)}px`;
    });

    fontButtonsContainer.appendChild(decreaseFontSizeButton);
    fontButtonsContainer.appendChild(increaseFontSizeButton);

    return fontButtonsContainer;
};

// 实现面板拖动功能的函数
const enableDragging = (outlinePanel, topButtonsContainer) => {
    let isDragging = false;
    let offsetX, offsetY;

    topButtonsContainer.style.cursor = "move";

    topButtonsContainer.addEventListener("mousedown", (e) => {
        if (isDragging) return;
        isDragging = true;
        offsetX = e.clientX - Number(outlinePanel.style.left.replace('px', ''));
        offsetY = e.clientY - Number(outlinePanel.style.top.replace('px', ''));

        const onMouseMove = (e) => {
            if (!isDragging) return;
            let newX = e.clientX - offsetX;
            let newY = e.clientY - offsetY;

            const minX = 20;
            const minY = 20;
            const maxX = window.innerWidth - outlinePanel.offsetWidth - 20;
            const maxY = window.innerHeight - outlinePanel.offsetHeight - 20;

            newX = Math.max(minX, Math.min(newX, maxX));
            newY = Math.max(minY, Math.min(newY, maxY));

            outlinePanel.style.left = `${newX}px`;
            outlinePanel.style.top = `${newY}px`;
        };

        const onMouseUp = () => {
            isDragging = false;
            document.removeEventListener("mousemove", onMouseMove);
            document.removeEventListener("mouseup", onMouseUp);
        };

        document.addEventListener("mousemove", onMouseMove);
        document.addEventListener("mouseup", onMouseUp);
    });
};

// ========== 主函数 ==========
const main = () => {
    const { outlinePanel, outlineContent } = createOutlinePanel();
    document.body.appendChild(outlinePanel);

    const fontButtonsContainer = createFontButtonsContainer(outlineContent);
    const topButtonsContainer = outlinePanel.querySelector('div');
    topButtonsContainer.appendChild(fontButtonsContainer);

    document.addEventListener('click', (e) => handleClick(e, outlineContent));
};

main();
  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    28441 引用 • 119746 回帖
  • 代码片段

    代码片段分为 CSS 与 JS 两种代码,添加在 [设置 - 外观 - 代码片段] 中,这些代码会在思源笔记加载时自动执行,用于改善笔记的样式或功能。

    用户在该标签下分享代码片段时需在帖子标题前添加 [css] [js] 用于区分代码片段类型。

    285 引用 • 1983 回帖
2 操作
cxg318 在 2025-02-27 01:25:05 更新了该帖
cxg318 在 2025-02-24 00:11:08 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • Floria233

    这个很棒啊,感谢大大。就我的理解,这个设计是不是和 ob 的 floating index(好像是这个)插件相似?就是可以使大纲悬浮起来——就动图与说明看起来,似乎是这个样子?

    (我就说嘛,ob 能做到的东西,思源大差不差也都能做到类似的,就是这边开发者人手缺乏罢了,扼腕……)

    这个我所能想到的第一个优点就是节省面板位置,因为插件那么多,而软件本体除了编辑器之外的其他部分又那么小,可谓寸土寸金,参考我的话,左边是自带文档树 + 书签两个部件,右边是二级文档树这个部件,这三个是长期固定,常用的浮动面板是 knote 和小记,偶尔需要查看大纲,就得按快捷键切换了(因为不是每个文档都有大纲,需要用的时候才会固定,不用的时候就不希望它占用位置)

    因为文档跳转频繁,文档树与其浮动还是贴边吧,而浮动的大纲面板可以按照需要跳出来,最能辅助正文查看又能灵活调取(不至于总是放在某个地方),唯有它浮动起来是最合适的 😁

    我的体验是,它是一个有则最好,无则稍憾(思源本体可以凑合)而绝非鸡肋的功能设计,ob,flowus(notion 不知道)还有有道云(印象不知道)这种都有特别给出这个设计啊,所以它绝对是有必要的

    目前我所想到的优化方向,1 设计上,应该是这个面板先跟主题风格和谐一致,除此之外,2 这个 UI 的信息界面,大大要不要参考动图右边那个大纲插件设计?给每个大标题旁边都用数字标注其下小标题的数量之类的?

    另,有道云的悬浮大纲,在不用时会变成一道道长短不一的横线,这个看起来很简洁,但可以的话,请大大千万别做成这样,因为我用过几次,每次都需要鼠标浮过去它才会显现,不够直观还增加了操作步骤。

    也不希望完全参照 ob 的那个插件——只能在一个固定位置悬浮(这个当大纲标题太长时,偶尔会和正文重合起来),就是动图中这种自由拖动我感觉挺好的 👍(不过它是只能在编辑区拖动?还是可以在整个界面拖动呢?后者感觉更好一点)

    其实这个面板,只要存在了就是它最大的意义,其他都是锦上添花(来自小白的看法)

    1 回复
  • cxg318
    作者

    思源本体内任意位置拖动的

  • 感觉挺好的工具,有很多场景会需要。

  • zzshang

    鸡肋

  • Floria233

    用不用取决于使用者,开不开发取决于开发者。

    使用者对开发者所做的某个非强制不影响使用体验的功能缺乏感知,就是楼下这种了。

    很多开发者做某个东西,最开始都是基于自己的某些目的,有的是练手,有的是自用,有的是为爱发电,大大感觉有必要就做吧,真正在需要者的使用场景里,还是相当实用滴。

    有句糙话是,我可以不用,你不能没有(捂脸),大概就是这种情况。关键是看大大的动图,已经做到一定可用性了,做好了之后一定会有人需要的。👍

    1 回复
  • cxg318 2 评论
    作者

    再修修,如果大家有一些好的点子,就加上去,到时候成形了,代码放出来

    1 回复
    大大,这些点子感觉可以参考 AI,之前用 joplin 时总觉得文档树很难用,为此还特意去论坛反馈,使用 AI 帮助整理说辞时,AI 给出了更多我都没想到的优化设计方案(乍一看确实也很合理),到了那份上,感觉不是满足使用者需求,而是在考验开发者水平了 😂
    Floria233
    我搜索了一下,给个示例,并非冒犯,只为给大大你参考,要是看不惯 AI 可以忽视。我个人是觉得它里面所给出的某些方向似乎特别难。
    Floria233
  • Floria233

    针对笔记软件中悬浮目录功能的优化方向,可以从以下多个维度进行深入改进,提升用户体验和效率:

    这个是 AI 所写,个人感想使用【】


    一、交互体验优化

    1. 动态折叠与智能展开

      • 层级感知折叠:根据用户当前阅读位置(如标题层级),自动折叠非相关层级目录(如仅展示当前章节的 2-3 级标题)。【这个看动图好像可以做到】
      • 手势交互:支持双指捏合展开/折叠目录层级,长按目录项直接拖拽调整文档结构。【这个涉及到的知识是不是有点难???二级文档树到现在都没法自由拖动排序,可能就是这个原因】
      • 焦点跟随:滚动正文时,悬浮目录自动高亮并居中显示当前章节,同时折叠非相邻章节。【这个感觉比较好用】
    2. 快捷操作集成

      • 右键菜单增强:在目录项右键添加「跳转到子标题」「复制标题链接」「快速拆分/合并章节」等高频操作。【这个在我看来,似乎有点繁琐,不过如果加上好像也挺好用,但这应当是个很大的工作量】
      • 拖拽多选:支持框选或 Shift 多选目录项,批量调整章节顺序或层级(类似文件管理器逻辑)。【这个背后原理不太懂,也许很难???它这个和文档树有点类似,选了目录就直接调动整个文档界面的内容,这个真的有必要吗???这种使用频率似乎不够高……】

    二、视觉与信息呈现优化

    1. 三维视觉反馈

      • 深度指示:通过阴影、缩进层级和颜色渐变区分标题层级(如一级标题深色加粗,次级标题逐渐淡化)。【UI 方面的,这个感觉不错】
      • 阅读进度可视化:在目录项右侧添加细进度条,显示该章节的阅读完成比例(通过文字密度分析估算)。【思源可以联动到这份上吗?估计也不容易】
    2. 动态内容预览

      • Hover 卡片:鼠标悬停目录项时,弹出缩略图或关键句预览,避免频繁跳转。【感觉复杂,悬浮需求有必要吗?如果可以开发出来可能会好用,但可能会影响思源性能,造成卡顿,这是我的揣测】
      • 关联标签:自动标记含图片、表格、代码块的章节,通过图标直观展示内容类型。【同上】

    三、功能增强

    1. 智能目录管理

      • 自动语义重组:通过 NLP 识别内容主题,建议合并/拆分章节(如检测到连续多个短段落讨论同一主题时提示合并)。【复杂】
      • 版本对比:记录目录结构历史版本,支持与早期版本对比并恢复。【这个感觉很没必要,然而这个真是 AI 才能提出来,一般使用者想不到这个】
    2. 跨文档联动

      • 多文档目录聚合:在协同场景下,悬浮目录可同时显示多个关联文档的目录(如项目文档 + 会议纪要),支持跨文档跳转。【这个是啥?有点不懂】
      • 书签同步:将目录项与文档内自定义书签绑定,实现混合导航。【无法想象,从来没有见过哪个目录导航能做到这份上,但不明觉厉】

    四、场景化适配

    1. 创作模式优化

      • 大纲模式融合:进入写作模式时,悬浮目录自动切换为可编辑大纲,支持直接修改标题文本或升降级。【这个感觉不错,不过话说回来,感觉工作量不小】
      • 思维导图联动:点击目录项右侧图标,一键生成以当前章节为中心的局部思维导图。【同上】
    2. 移动端专属设计

      • 摇杆式导航:在窄屏下将目录转换为底部半屏摇杆,滑动控制滚动速度,点击快速定位。【不懂】
      • 语音导航:支持语音指令如「跳转到第三节」「展开所有二级标题」。【感觉这个是真鸡肋】

    五、性能与底层优化

    1. 实时响应机制【这个是给开发者的建议,不说了】

      • 增量加载:超长文档中仅渲染可视区域目录项,滚动时动态加载(类似虚拟列表技术)。
      • 差异更新:通过 Diff 算法仅更新变动的目录节点,避免整体重绘导致的卡顿。
    2. 智能预判

      • 滚动惯性预测:根据滚动速度预加载即将到达的目录节点,确保流畅的高亮切换。

    六、用户自定义体系

    1. 个性化规则引擎
      • 条件过滤:设置「自动隐藏所有空章节」「仅显示含 TODO 标签的节点」等自定义过滤规则。
      • 样式模板库:提供不同场景的目录主题(如学术论文模式、会议记录模式),支持导出分享。

    示例方案:智能写作助手模式

    • 场景:用户撰写技术文档时,悬浮目录右侧常驻 AI 面板:【这个是接入了 AI 的目录,和现在大大做的目录,不是一回事啊,同理,感觉这个不容易做到】
      1. 自动检测「缺少结论段」并红色警示
      2. 推荐插入「代码示例」「流程图」等模块按钮
      3. 实时显示章节字数统计与建议优化点(如「当前段落术语密度过高」)

    通过以上优化,悬浮目录可从被动导航工具升级为主动创作助手,同时兼顾效率型用户的深度需求和新手用户的易用性。建议采用 A/B 测试逐步推进,优先落地高 ROI 功能(如目录搜索、拖拽调整),再迭代智能功能。【这个应该是给开发者的建议,ROI 这种术语都出来了,哈哈哈】

    1 回复
  • att88 2 评论

    我个人可太需要这个了好嘛!之前论坛找了好几次是否有悬浮大纲悬浮文档树,结果都是没有。思源大纲虽说可以四处移动,但是高宽还是受到主页面限制,有时候大纲没几行内容还占了一大片,老按快捷键开关也很麻烦。就很希望能像 ps 那样工具面板都能页面外悬浮,能专注页面本身不被视觉中心外的东西干扰。感谢作者!希望尽快上架

    文档树原来就是可以悬浮的,但悬浮大纲这个是真没有
    Floria233
    请问文档树怎么悬浮?
    att88
  • cxg318 1 评论
    作者

    扔给我这么多,直接整不会了。😂
    ai 确实能帮忙很多,代码基本是 ai 写的,但要修一些小细节,对于我这等代码小白来说,还是很难的。
    可能一个小问题,扔给 kimi,豆包,deepseek,他们几十次都没法给一个满意的结果,
    最后,还得手动进代码查找修改。
    这里也有很大可能是喂给他们的关键词不够准确。
    思源是一个让人折腾停不下来的软件,但要学的东西实在太多了******

    1 操作
    cxg318 在 2025-02-23 00:39:03 更新了该回帖
    不要吓到了,可以做的就去做,不可以做的就去学或者就不做,躺平也没关系,AI 给的方案感觉是作为商业产品开发,方方面面都考虑到了,但 20% 的功能可以满足使用者 80% 的需求,28 定理无处不在,剩下顶多精益求精。😄
    Floria233
  • LitBearPibs

    大佬赶紧上架吧,插件市场怎么搜到,指个路子啊

  • cxg318
    作者

    7.双击段落里的块(包括段落标题),大纲对应的颜色改变

    screenshots.gif

  • cxg318
    作者

    js 代码片段放出,有需要的拿去用,也欢迎修改的更好。
    肯定有一些 bug,先暂时用着吧。😋

    1 回复
  • att88

    js 代码用了好像没起作用?上面 docker 栏中也没找到相应图标,是不是我哪里使用姿势不对

    1 回复
  • cxg318
    作者

    代码片段有一个是 css,一个是 js,不要填错了

    1 回复
  • att88

    确实填的是 js。代码中有一段 // 解码HTML实体的函数 我这边报错,应该是大大代码块粘贴的时候 html 直接给解析了。改回去后我现在运行正常。感谢!!

    image.png

    1 回复
  • cxg318
    作者

    确实,粘贴时自动被系统解析了。只能粘贴完代码,手动改一下了。因为这是发贴的系统自动改的。

  • NieJianYing

    丸辣,手机伺服用不了,会从网页跳到桌面程序

  • cxg318
    作者

    更新 20250227

  • mark-j

    能像少数派网页端的 toc 一样就好了

    1 回复
  • cxg318
    作者

    可以用人工智能加工一下,基础已经做好了,改一下也简单的。

请输入回帖内容 ...

推荐标签 标签

  • ngrok

    ngrok 是一个反向代理,通过在公共的端点和本地运行的 Web 服务器之间建立一个安全的通道。

    7 引用 • 63 回帖 • 667 关注
  • OpenShift

    红帽提供的 PaaS 云,支持多种编程语言,为开发人员提供了更为灵活的框架、存储选择。

    14 引用 • 20 回帖 • 687 关注
  • Shell

    Shell 脚本与 Windows/Dos 下的批处理相似,也就是用各类命令预先放入到一个文件中,方便一次性执行的一个程序文件,主要是方便管理员进行设置或者管理用的。但是它比 Windows 下的批处理更强大,比用其他编程程序编辑的程序效率更高,因为它使用了 Linux/Unix 下的命令。

    126 引用 • 83 回帖 • 1 关注
  • 开源

    Open Source, Open Mind, Open Sight, Open Future!

    421 引用 • 3610 回帖
  • Redis

    Redis 是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。从 2010 年 3 月 15 日起,Redis 的开发工作由 VMware 主持。从 2013 年 5 月开始,Redis 的开发由 Pivotal 赞助。

    285 引用 • 248 回帖
  • 互联网

    互联网(Internet),又称网际网络,或音译因特网、英特网。互联网始于 1969 年美国的阿帕网,是网络与网络之间所串连成的庞大网络,这些网络以一组通用的协议相连,形成逻辑上的单一巨大国际网络。

    99 引用 • 367 回帖
  • 阿里云

    阿里云是阿里巴巴集团旗下公司,是全球领先的云计算及人工智能科技公司。提供云服务器、云数据库、云安全等云计算服务,以及大数据、人工智能服务、精准定制基于场景的行业解决方案。

    85 引用 • 324 回帖
  • 负能量

    上帝为你关上了一扇门,然后就去睡觉了....努力不一定能成功,但不努力一定很轻松 (° ー °〃)

    89 引用 • 1251 回帖 • 378 关注
  • IDEA

    IDEA 全称 IntelliJ IDEA,是一款 Java 语言开发的集成环境,在业界被公认为最好的 Java 开发工具之一。IDEA 是 JetBrains 公司的产品,这家公司总部位于捷克共和国的首都布拉格,开发人员以严谨著称的东欧程序员为主。

    182 引用 • 400 回帖
  • TGIF

    Thank God It's Friday! 感谢老天,总算到星期五啦!

    293 引用 • 4496 回帖 • 687 关注
  • 友情链接

    确认过眼神后的灵魂连接,站在链在!

    25 引用 • 373 回帖 • 6 关注
  • GraphQL

    GraphQL 是一个用于 API 的查询语言,是一个使用基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。GraphQL 并没有和任何特定数据库或者存储引擎绑定,而是依靠你现有的代码和数据支撑。

    4 引用 • 3 回帖 • 10 关注
  • 服务

    提供一个服务绝不仅仅是简单的把硬件和软件累加在一起,它包括了服务的可靠性、服务的标准化、以及对服务的监控、维护、技术支持等。

    41 引用 • 24 回帖 • 3 关注
  • RabbitMQ

    RabbitMQ 是一个开源的 AMQP 实现,服务器端用 Erlang 语言编写,支持多种语言客户端,如:Python、Ruby、.NET、Java、C、PHP、ActionScript 等。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。

    49 引用 • 60 回帖 • 342 关注
  • 钉钉

    钉钉,专为中国企业打造的免费沟通协同多端平台, 阿里巴巴出品。

    15 引用 • 67 回帖 • 236 关注
  • MySQL

    MySQL 是一个关系型数据库管理系统,由瑞典 MySQL AB 公司开发,目前属于 Oracle 公司。MySQL 是最流行的关系型数据库管理系统之一。

    695 引用 • 538 回帖 • 1 关注
  • Hexo

    Hexo 是一款快速、简洁且高效的博客框架,使用 Node.js 编写。

    22 引用 • 148 回帖 • 27 关注
  • JavaScript

    JavaScript 一种动态类型、弱类型、基于原型的直译式脚本语言,内置支持类型。它的解释器被称为 JavaScript 引擎,为浏览器的一部分,广泛用于客户端的脚本语言,最早是在 HTML 网页上使用,用来给 HTML 网页增加动态功能。

    736 引用 • 1307 回帖 • 2 关注
  • 阿里巴巴

    阿里巴巴网络技术有限公司(简称:阿里巴巴集团)是以曾担任英语教师的马云为首的 18 人,于 1999 年在中国杭州创立,他们相信互联网能够创造公平的竞争环境,让小企业通过创新与科技扩展业务,并在参与国内或全球市场竞争时处于更有利的位置。

    43 引用 • 221 回帖 • 10 关注
  • TextBundle

    TextBundle 文件格式旨在应用程序之间交换 Markdown 或 Fountain 之类的纯文本文件时,提供更无缝的用户体验。

    1 引用 • 2 回帖 • 90 关注
  • Unity

    Unity 是由 Unity Technologies 开发的一个让开发者可以轻松创建诸如 2D、3D 多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

    27 引用 • 7 回帖 • 94 关注
  • MongoDB

    MongoDB(来自于英文单词“Humongous”,中文含义为“庞大”)是一个基于分布式文件存储的数据库,由 C++ 语言编写。旨在为应用提供可扩展的高性能数据存储解决方案。MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似 JSON 的 BSON 格式,因此可以存储比较复杂的数据类型。

    91 引用 • 59 回帖
  • 资讯

    资讯是用户因为及时地获得它并利用它而能够在相对短的时间内给自己带来价值的信息,资讯有时效性和地域性。

    56 引用 • 85 回帖 • 1 关注
  • OnlyOffice
    4 引用 • 40 关注
  • 爬虫

    网络爬虫(Spider、Crawler),是一种按照一定的规则,自动地抓取万维网信息的程序。

    106 引用 • 275 回帖
  • FlowUs

    FlowUs.息流 个人及团队的新一代生产力工具。

    让复杂的信息管理更轻松、自由、充满创意。

    1 引用 • 1 关注
  • RYMCU

    RYMCU 致力于打造一个即严谨又活泼、专业又不失有趣,为数百万人服务的开源嵌入式知识学习交流平台。

    4 引用 • 6 回帖 • 56 关注