const notes = [
{
id: "plainleaf-plan",
title: "Plainleaf 改造计划",
path: "项目/Plainleaf/Plainleaf 改造计划",
updated: "今天 10:42",
favorite: true,
tags: ["产品", "设计", "笔记"],
excerpt: "保留普通 Markdown 文件,同时把搜索、整理和移动记录体验做得更自然。",
content: `
Plainleaf 会保留 SilverBullet 最重要的基础:每一篇笔记仍然是 NAS 中可以直接打开的 Markdown 文件。
目标不是复制 Obsidian,而是让记录、查找和整理变得自然,同时保留查询与自动化能力。
第一阶段目标
- 把文件、搜索、待办和标签放进同一个稳定侧栏。
- 保留现有
CodeMirror 6 编辑器和 Markdown 能力。
- 让普通用户不需要理解命令、元页面和内部标签。
- 为手机设计专用的随手记和查找流程。
数据原则
界面只是工作层,文件才是长期资产。即使以后停止使用 Plainleaf,也可以通过 Obsidian、VS Code 或普通文本编辑器继续使用全部内容。
下一步
交互 Demo 确认后,再将新工作区接到现有页面索引和文件操作上。
`,
},
{
id: "zspace",
title: "极空间部署",
path: "知识库/Docker/极空间部署",
updated: "昨天 22:16",
favorite: true,
tags: ["Docker", "NAS", "部署"],
excerpt: "极空间负责运行正式环境,Mac 用于开发和构建镜像。",
content: `
极空间负责运行正式环境,Mac 用于开发和构建镜像。笔记目录通过卷挂载到容器的 /space。
部署结构
- 固定镜像版本,避免使用 latest。
- 笔记目录和应用配置分别备份。
- 公网访问统一经过 HTTPS 与反向代理。
更新流程
在本地完成构建和验证后,推送自定义镜像,再由极空间 Compose 切换版本。
`,
},
{
id: "daily",
title: "2026-08-12",
displayTitle: "今天的日记",
path: "日记/2026/08/2026-08-12",
updated: "今天 08:30",
favorite: false,
tags: ["日记"],
excerpt: "今天继续整理个人笔记系统,希望数据保持开放,同时让 Web 端变得更易用。",
content: `
今天继续整理个人笔记系统,希望数据保持开放,同时让 Web 端变得更易用。
今日事项
- 确认 Plainleaf 的工作区方向。
- 完成桌面与手机交互 Demo。
- 整理正式开发任务。
记录
文件树应该承担定位,而不是让用户记住所有命令。高级能力可以继续通过高级工具进入。
`,
},
{
id: "docker-network",
title: "Docker 容器网络",
path: "知识库/Docker/Docker 容器网络",
updated: "8 月 10 日",
favorite: false,
tags: ["Docker", "网络"],
excerpt: "同一个 Compose 项目中的服务应通过服务名通信。",
content: `
同一个 Compose 项目中的服务应通过服务名通信,而不是依赖宿主机端口或孤立容器名称。
服务发现
容器加入同一网络后,可以使用 service:port 作为内部地址。
排查顺序
- 确认两个服务属于同一 Docker 网络。
- 检查服务名能否解析。
- 检查目标端口是否监听在 0.0.0.0。
`,
},
{
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: `
临时想法先进入收件箱,稍后再归档到项目、知识库或日记。
待整理
- 浏览器剪藏入口。
- 手机语音快速记录。
- Markdown 文件历史对比。
`,
},
];
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 `
${icon("search")}
${state.searchQuery ? `` : ""}
${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("")}` : ""}
`;
}
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")}
`;
}
function advancedRows() {
const tools = [
["terminal-square", "命令面板", "运行自动化和不常用操作"],
["network", "关系图", "查看笔记之间的链接"],
["blocks", "扩展库", "管理兼容扩展"],
["file-cog", "配置", "调整高级行为"],
["files", "系统页面", "查看默认隐藏的内部页面"],
];
return tools.map(([toolIcon, title, subtitle]) => `
`).join("");
}
function moreSidebar() {
return `
`;
}
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 `
设置
日常写作选项放在前面,高级能力需要时再打开。
写作体验
显示系统内容Library、Std 和 meta 页面默认隐藏
高级工具
${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();