const notes = [ { id: "plainleaf-plan", title: "Plainleaf 改造计划", path: "项目/Plainleaf/Plainleaf 改造计划", updated: "今天 10:42", favorite: true, tags: ["产品", "设计", "笔记"], excerpt: "保留普通 Markdown 文件,同时把搜索、整理和移动记录体验做得更自然。", content: `

Plainleaf 会保留 SilverBullet 最重要的基础:每一篇笔记仍然是 NAS 中可以直接打开的 Markdown 文件。

目标不是复制 Obsidian,而是让记录、查找和整理变得自然,同时保留查询与自动化能力。

第一阶段目标

数据原则

界面只是工作层,文件才是长期资产。即使以后停止使用 Plainleaf,也可以通过 Obsidian、VS Code 或普通文本编辑器继续使用全部内容。

下一步

交互 Demo 确认后,再将新工作区接到现有页面索引和文件操作上。

`, }, { id: "zspace", title: "极空间部署", path: "知识库/Docker/极空间部署", updated: "昨天 22:16", favorite: true, tags: ["Docker", "NAS", "部署"], excerpt: "极空间负责运行正式环境,Mac 用于开发和构建镜像。", content: `

极空间负责运行正式环境,Mac 用于开发和构建镜像。笔记目录通过卷挂载到容器的 /space。

部署结构

更新流程

在本地完成构建和验证后,推送自定义镜像,再由极空间 Compose 切换版本。

`, }, { id: "daily", title: "2026-08-12", displayTitle: "今天的日记", path: "日记/2026/08/2026-08-12", updated: "今天 08:30", favorite: false, tags: ["日记"], excerpt: "今天继续整理个人笔记系统,希望数据保持开放,同时让 Web 端变得更易用。", content: `

今天继续整理个人笔记系统,希望数据保持开放,同时让 Web 端变得更易用。

今日事项

记录

文件树应该承担定位,而不是让用户记住所有命令。高级能力可以继续通过高级工具进入。

`, }, { id: "docker-network", title: "Docker 容器网络", path: "知识库/Docker/Docker 容器网络", updated: "8 月 10 日", favorite: false, tags: ["Docker", "网络"], excerpt: "同一个 Compose 项目中的服务应通过服务名通信。", content: `

同一个 Compose 项目中的服务应通过服务名通信,而不是依赖宿主机端口或孤立容器名称。

服务发现

容器加入同一网络后,可以使用 service:port 作为内部地址。

排查顺序

`, }, { id: "tool-review", title: "笔记工具对比", path: "收件箱/笔记工具对比", updated: "8 月 11 日", favorite: false, tags: ["调研", "笔记"], excerpt: "对比 Blossom、Blinko、Notesnook、SilverBullet 和其他文件原生笔记工具。", content: `

对比了 Blossom、Blinko、Notesnook、SilverBullet 和其他文件原生笔记工具。

核心结论

数据库优先的产品通常拥有更现代的成品体验,但文件原生模型更符合长期迁移与多工具使用的要求。

选择

以 SilverBullet 为文件与能力内核,在其上构建 Plainleaf 的现代工作区。

`, }, { id: "ideas", title: "产品灵感收件箱", path: "收件箱/产品灵感收件箱", updated: "8 月 9 日", favorite: false, tags: ["灵感", "产品"], excerpt: "临时想法先进入收件箱,稍后再归档到项目、知识库或日记。", content: `

临时想法先进入收件箱,稍后再归档到项目、知识库或日记。

待整理

`, }, ]; const tree = [ { type: "folder", name: "收件箱", children: ["tool-review", "ideas"] }, { type: "folder", name: "项目", children: [{ type: "folder", name: "Plainleaf", children: ["plainleaf-plan"] }] }, { type: "folder", name: "知识库", children: [{ type: "folder", name: "Docker", children: ["zspace", "docker-network"] }] }, { type: "folder", name: "日记", children: [{ type: "folder", name: "2026", children: [{ type: "folder", name: "08", children: ["daily"] }] }] }, ]; const tasks = [ { id: "task-demo", title: "完成桌面与手机交互 Demo", source: "Plainleaf 改造计划", noteId: "plainleaf-plan", group: "today" }, { id: "task-backup", title: "确认笔记目录的备份策略", source: "极空间部署", noteId: "zspace", group: "today" }, { id: "task-mobile", title: "整理手机端随手记流程", source: "2026-08-12", noteId: "daily", group: "today" }, { id: "task-network", title: "记录反向代理网络结构", source: "Docker 容器网络", noteId: "docker-network", group: "overdue" }, ]; const state = { view: "notes", currentNoteId: null, settings: false, searchQuery: "Plainleaf", searchScope: "all", selectedTag: "产品", readMode: false, offline: false, expandedFolders: new Set(["收件箱", "项目", "项目/Plainleaf", "知识库", "知识库/Docker", "日记"]), completedTasks: new Set(), }; const byId = (id) => document.getElementById(id); const currentNote = () => notes.find((note) => note.id === state.currentNoteId); function icon(name, className = "") { return ``; } function refreshIcons() { if (window.lucide) window.lucide.createIcons(); } function escapeHtml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function noteName(note) { return note.displayTitle || note.title; } function renderTreeNode(node, depth = 0, parentPath = "") { if (typeof node === "string") { const note = notes.find((item) => item.id === node); return ` `; } const path = parentPath ? `${parentPath}/${node.name}` : node.name; const expanded = state.expandedFolders.has(path); return `
${node.children.map((child) => renderTreeNode(child, depth + 1, path)).join("")}
`; } function notesSidebar() { return `
${tree.map((node) => renderTreeNode(node)).join("")}
`; } function searchableText(note, scope) { if (scope === "title") return note.title; if (scope === "content") return `${note.excerpt} ${note.content.replace(/<[^>]+>/g, " ")}`; return `${note.title} ${note.path} ${note.tags.join(" ")} ${note.excerpt} ${note.content.replace(/<[^>]+>/g, " ")}`; } function highlight(value, query) { const safe = escapeHtml(value); const trimmed = query.trim(); if (!trimmed) return safe; const escapedQuery = trimmed.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return safe.replace(new RegExp(`(${escapedQuery})`, "ig"), "$1"); } function filteredNotes() { const query = state.searchQuery.trim().toLocaleLowerCase("zh-CN"); if (!query) return [...notes]; return notes.filter((note) => searchableText(note, state.searchScope).toLocaleLowerCase("zh-CN").includes(query)); } function searchResult(note) { return ` `; } function searchSidebar() { const results = filteredNotes(); const resultContent = results.length ? results.map(searchResult).join("") : `
${icon("file-search")} 没有找到“${escapeHtml(state.searchQuery)}”

可以换个关键词,或直接创建一篇同名笔记。

`; return `
${resultContent}
`; } function taskItem(task) { if (state.completedTasks.has(task.id)) return ""; return `
${escapeHtml(task.title)}
`; } function tasksSidebar() { const active = tasks.filter((task) => !state.completedTasks.has(task.id)); const today = active.filter((task) => task.group === "today"); const overdue = active.filter((task) => task.group === "overdue"); return `
今天${today.length}
${today.length ? today.map(taskItem).join("") : '
今天都完成了

新的待办会自动出现在这里。

'}
${overdue.length ? `
较早${overdue.length}
${overdue.map(taskItem).join("")}
` : ""}
无日期0
`; } function tagMap() { const counts = new Map(); for (const note of notes) { for (const tag of note.tags) counts.set(tag, (counts.get(tag) || 0) + 1); } return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0], "zh-CN")); } function tagColor(index) { return ["", "coral", "blue", "yellow"][index % 4]; } function tagsSidebar() { const tags = tagMap(); const matches = notes.filter((note) => note.tags.includes(state.selectedTag)); return `
${tags.map(([tag, count], index) => ` `).join("")}
${matches.map(searchResult).join("")}
`; } function settingsSidebar() { return `

调整写作体验和文件行为。高级能力集中放在底部,不影响日常记事。

界面主题明亮
自动保存修改后自动写入文件
默认位置新笔记保存到收件箱
${icon("chevron-right")}
${advancedRows()}
`; } function advancedRows() { const tools = [ ["terminal-square", "命令面板", "运行自动化和不常用操作"], ["network", "关系图", "查看笔记之间的链接"], ["blocks", "扩展库", "管理兼容扩展"], ["file-cog", "配置", "调整高级行为"], ["files", "系统页面", "查看默认隐藏的内部页面"], ]; return tools.map(([toolIcon, title, subtitle]) => ` `).join(""); } function moreSidebar() { return `
${advancedRows()}
`; } function renderSidebar() { const content = byId("sidebarContent"); if (state.settings) content.innerHTML = settingsSidebar(); else if (state.view === "search") content.innerHTML = searchSidebar(); else if (state.view === "tasks") content.innerHTML = tasksSidebar(); else if (state.view === "tags") content.innerHTML = tagsSidebar(); else if (state.view === "more") content.innerHTML = moreSidebar(); else content.innerHTML = notesSidebar(); document.querySelectorAll(".primary-nav-item").forEach((button) => { button.classList.toggle("is-active", !state.settings && button.dataset.view === state.view); }); byId("settingsButton").classList.toggle("is-active", state.settings); byId("taskCount").textContent = tasks.filter((task) => !state.completedTasks.has(task.id)).length; refreshIcons(); if (state.view === "search" && !state.settings) { const input = byId("sidebarSearchInput"); if (input && document.activeElement?.id !== "sidebarSearchInput") { requestAnimationFrame(() => { input.focus(); input.setSelectionRange(input.value.length, input.value.length); }); } } } function renderBreadcrumb(parts) { byId("breadcrumb").innerHTML = parts.map((part, index) => `${index ? icon("chevron-right") : ""}${escapeHtml(part)}`).join(""); } function homePage() { const activeTasks = tasks.filter((task) => !state.completedTasks.has(task.id)).slice(0, 4); return `

2026 年 8 月 12 日 · 星期三

今天想记点什么?

从一条想法开始,剩下的可以晚点再整理。

最近编辑

${notes.slice(0, 5).map((note) => ` `).join("")}

今天待办

${activeTasks.map((task) => `
${escapeHtml(task.title)}${escapeHtml(task.source)}
`).join("")}
`; } function documentPage(note) { return `

${escapeHtml(note.title)}

${escapeHtml(note.updated)} 更新·${escapeHtml(note.path.split("/").slice(0, -1).join(" / "))}
${note.content}
${note.content}
`; } function settingsPage() { const tools = [ ["terminal-square", "命令面板", "运行自动化和不常用操作"], ["network", "关系图", "查看笔记之间的链接"], ["blocks", "扩展库", "管理兼容扩展"], ["file-cog", "配置", "编辑 Plainleaf 的高级行为"], ["files", "系统页面", "查看默认隐藏的内部页面"], ]; return `

设置

日常写作选项放在前面,高级能力需要时再打开。

写作体验

正文宽度适合长时间阅读的 760px
自动保存修改后自动写回 Markdown 文件
显示系统内容Library、Std 和 meta 页面默认隐藏

文件与同步

默认新建位置收件箱
当前空间Markdown 文件已同步

高级工具

${tools.map(([toolIcon, title, subtitle]) => ` `).join("")}
`; } function renderWorkspace() { const content = byId("workspaceContent"); if (state.settings) { content.innerHTML = settingsPage(); renderBreadcrumb(["设置"]); byId("readModeButton").disabled = true; } else if (!state.currentNoteId) { content.innerHTML = homePage(); renderBreadcrumb(["首页"]); byId("readModeButton").disabled = true; } else { const note = currentNote(); content.innerHTML = documentPage(note); renderBreadcrumb(note.path.split("/")); byId("readModeButton").disabled = false; } byId("readModeButton").classList.toggle("is-active", state.readMode); byId("wordCount").textContent = state.currentNoteId ? "426 字 · Markdown" : "Markdown"; refreshIcons(); } function renderAll() { renderSidebar(); renderWorkspace(); } function openNote(noteId, closeMobile = true) { state.currentNoteId = noteId; state.settings = false; state.readMode = false; renderAll(); if (closeMobile) closeSidebar(); } function openHome() { state.currentNoteId = null; state.settings = false; state.view = "notes"; state.readMode = false; renderAll(); closeSidebar(); } function switchView(view, openMobile = false) { state.view = view; state.settings = false; renderSidebar(); if (openMobile) openSidebar(); } function openSettings(openMobile = false) { state.settings = true; state.view = "settings"; renderAll(); if (openMobile) openSidebar(); } function openSidebar() { byId("app").classList.add("sidebar-open"); } function closeSidebar() { byId("app").classList.remove("sidebar-open"); } function openNewNote(preset = "blank") { byId("newNoteModal").classList.add("is-visible"); byId("newNoteModal").setAttribute("aria-hidden", "false"); selectQuickCreate(preset); requestAnimationFrame(() => byId("newNoteTitle").focus()); } function closeNewNote() { byId("newNoteModal").classList.remove("is-visible"); byId("newNoteModal").setAttribute("aria-hidden", "true"); } function selectQuickCreate(preset) { document.querySelectorAll("[data-quick-create]").forEach((button) => button.classList.toggle("is-selected", button.dataset.quickCreate === preset)); const titleInput = byId("newNoteTitle"); const folder = byId("newNoteFolder"); if (preset === "capture") { titleInput.value = "随手记 11:20"; folder.value = "收件箱"; } else if (preset === "daily") { titleInput.value = "2026-08-12"; folder.value = "日记/2026/08"; } else { titleInput.value = ""; folder.value = "收件箱"; } titleInput.focus(); titleInput.select(); } let toastTimer; function showToast(message) { clearTimeout(toastTimer); byId("toastText").textContent = message; byId("toast").classList.add("is-visible"); toastTimer = setTimeout(() => byId("toast").classList.remove("is-visible"), 2200); } function completeTask(taskId) { const row = document.querySelector(`[data-task-row="${taskId}"], [data-home-task="${taskId}"]`); if (row) row.classList.add("is-complete"); setTimeout(() => { state.completedTasks.add(taskId); renderAll(); showToast("待办已完成,Markdown 已更新"); }, 170); } function toggleOffline() { state.offline = !state.offline; byId("offlineBanner").classList.toggle("is-visible", state.offline); byId("syncText").textContent = state.offline ? "修改已保存在本机" : "文件已同步"; if (state.view === "more") renderSidebar(); showToast(state.offline ? "已切换到离线演示" : "连接已恢复"); } function setSaving() { const saveState = byId("saveState"); saveState.classList.add("is-saving"); saveState.innerHTML = `${icon("circle-dot-dashed")}保存中`; refreshIcons(); clearTimeout(setSaving.timer); setSaving.timer = setTimeout(() => { saveState.classList.remove("is-saving"); saveState.innerHTML = `${icon("check")}已保存`; refreshIcons(); }, 700); } document.querySelectorAll(".primary-nav-item").forEach((button) => { button.addEventListener("click", () => switchView(button.dataset.view)); }); byId("homeButton").addEventListener("click", openHome); byId("newNoteButton").addEventListener("click", () => openNewNote("blank")); byId("mobileNewButton").addEventListener("click", () => openNewNote("capture")); byId("settingsButton").addEventListener("click", () => openSettings(false)); byId("openSidebarButton").addEventListener("click", openSidebar); byId("closeSidebarButton").addEventListener("click", closeSidebar); byId("mobileScrim").addEventListener("click", closeSidebar); document.querySelectorAll("[data-mobile-view]").forEach((button) => { button.addEventListener("click", () => { const view = button.dataset.mobileView; state.settings = false; switchView(view, true); }); }); byId("sidebarContent").addEventListener("click", (event) => { const noteButton = event.target.closest("[data-open-note]"); if (noteButton) return openNote(noteButton.dataset.openNote); const homeButton = event.target.closest("[data-open-home]"); if (homeButton) return openHome(); const folderButton = event.target.closest("[data-folder]"); if (folderButton) { const folder = folderButton.dataset.folder; if (state.expandedFolders.has(folder)) state.expandedFolders.delete(folder); else state.expandedFolders.add(folder); return renderSidebar(); } const scopeButton = event.target.closest("[data-search-scope]"); if (scopeButton) { state.searchScope = scopeButton.dataset.searchScope; return renderSidebar(); } if (event.target.closest("[data-clear-search]")) { state.searchQuery = ""; return renderSidebar(); } if (event.target.closest("[data-create-from-search]")) return openNewNote("blank"); const taskButton = event.target.closest("[data-complete-task]"); if (taskButton) return completeTask(taskButton.dataset.completeTask); const tagButton = event.target.closest("[data-select-tag]"); if (tagButton) { state.selectedTag = tagButton.dataset.selectTag; return renderSidebar(); } const moreTarget = event.target.closest("[data-more-target]"); if (moreTarget) { if (moreTarget.dataset.moreTarget === "settings") openSettings(true); else switchView(moreTarget.dataset.moreTarget, true); return; } const toolButton = event.target.closest("[data-advanced-tool]"); if (toolButton) return showToast(`${toolButton.dataset.advancedTool}已放在高级工具中`); const sidebarAction = event.target.closest("[data-sidebar-action]"); if (sidebarAction?.dataset.sidebarAction === "collapse") { state.expandedFolders.clear(); return renderSidebar(); } if (sidebarAction?.dataset.sidebarAction === "recent") { showToast("最近编辑按时间排列在首页"); return; } if (sidebarAction?.dataset.sidebarAction === "favorites") { showToast("已显示 2 篇收藏笔记"); return; } if (event.target.closest("[data-new-folder]")) return showToast("新建文件夹入口"); if (event.target.closest("[data-toggle-offline]")) return toggleOffline(); const switchButton = event.target.closest("[data-toggle-switch]"); if (switchButton) switchButton.classList.toggle("is-on"); }); byId("sidebarContent").addEventListener("input", (event) => { if (event.target.id !== "sidebarSearchInput") return; state.searchQuery = event.target.value; const selectionStart = event.target.selectionStart; renderSidebar(); const input = byId("sidebarSearchInput"); input.focus(); input.setSelectionRange(selectionStart, selectionStart); }); byId("workspaceContent").addEventListener("click", (event) => { const noteButton = event.target.closest("[data-open-note]"); if (noteButton) return openNote(noteButton.dataset.openNote, false); const taskButton = event.target.closest("[data-complete-task]"); if (taskButton) return completeTask(taskButton.dataset.completeTask); const homeAction = event.target.closest("[data-home-action]"); if (homeAction) return openNewNote(homeAction.dataset.homeAction === "capture" ? "capture" : "blank"); const viewButton = event.target.closest("[data-switch-view]"); if (viewButton) return switchView(viewButton.dataset.switchView, window.innerWidth <= 720); const advancedButton = event.target.closest("[data-advanced-tool]"); if (advancedButton) return showToast(`${advancedButton.dataset.advancedTool}会打开现有高级能力`); const switchButton = event.target.closest("[data-toggle-switch]"); if (switchButton) switchButton.classList.toggle("is-on"); }); byId("workspaceContent").addEventListener("input", (event) => { const note = currentNote(); if (!note) return; if (event.target.id === "pageTitleInput") note.title = event.target.value; if (event.target.id === "editorBody") note.content = event.target.innerHTML; setSaving(); }); byId("readModeButton").addEventListener("click", () => { if (!state.currentNoteId) return; state.readMode = !state.readMode; renderWorkspace(); }); byId("pageSearchButton").addEventListener("click", () => showToast("页内搜索会在正文顶部打开")); byId("moreButton").addEventListener("click", (event) => { event.stopPropagation(); const popover = byId("morePopover"); const visible = popover.classList.toggle("is-visible"); popover.setAttribute("aria-hidden", visible ? "false" : "true"); }); document.addEventListener("click", (event) => { if (!event.target.closest("#morePopover") && !event.target.closest("#moreButton")) { byId("morePopover").classList.remove("is-visible"); byId("morePopover").setAttribute("aria-hidden", "true"); } }); byId("morePopover").addEventListener("click", (event) => { const button = event.target.closest("button"); if (!button) return; showToast(`${button.textContent.trim()}入口已确认`); byId("morePopover").classList.remove("is-visible"); }); document.querySelectorAll("[data-close-modal]").forEach((button) => button.addEventListener("click", closeNewNote)); document.querySelectorAll("[data-quick-create]").forEach((button) => button.addEventListener("click", () => selectQuickCreate(button.dataset.quickCreate))); byId("newNoteForm").addEventListener("submit", (event) => { event.preventDefault(); const title = byId("newNoteTitle").value.trim() || "未命名笔记"; const folder = byId("newNoteFolder").value; const id = `demo-${Date.now()}`; notes.unshift({ id, title, path: `${folder}/${title}`, updated: "刚刚", favorite: false, tags: [], excerpt: "新创建的 Markdown 笔记。", content: "


", }); closeNewNote(); openNote(id, false); showToast(`已在${folder}创建笔记`); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closeNewNote(); closeSidebar(); byId("morePopover").classList.remove("is-visible"); } if ((event.metaKey || event.ctrlKey) && event.key.toLocaleLowerCase() === "k") { event.preventDefault(); switchView("search", window.innerWidth <= 720); } }); renderAll();