Tolaria 的 Neighborhood 模式:基于 entity 选区的笔记关系浏览如何实现

发布时间:2026/9/13 21:11:36
Tolaria 的 Neighborhood 模式:基于 entity 选区的笔记关系浏览如何实现 Tolaria 的 Neighborhood 模式基于 entity 选区的笔记关系浏览如何实现【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolariaTolaria 是一个基于 Markdown 文件库Vault的桌面知识管理应用其侧边栏笔记列表除了常规筛选外还内置了一种关系浏览模式选定一篇笔记后列表会围绕它展示所有关联分组。本文基于架构决策记录 0069Neighborhood mode for note-list relationship browsing结合仓库中的类型定义、Hook 与工具函数源码讲清楚 Neighborhood 模式的产品定义、状态模型、键盘/指针交互语义以及关系分组的构建与渲染实现读完后你可以理解“从源码结构看”这套关系浏览是如何以最小的状态改动嵌入现有选区体系的。背景模糊的 entity 选区与不匹配的关系浏览在正式化之前Tolaria 已经存在一个关系浏览状态隐藏在SidebarSelection.kind entity这一内部判别值之后但产品语言与交互模型并不清晰。ADR 中记录了三个具体问题作为“源笔记”source note的选中项被渲染成一张特殊卡片而不是普通笔记行分组后的关系结果在多个 section 之间做了去重掩盖了“一篇笔记合法地属于多个分组”这一图谱事实Cmd-click 的行为更像遗留的“在别处打开”而不是一次明确的图导航动作。新的笔记列表流程需要一个显式的产品概念——围绕某篇源笔记浏览其“邻居”Neighborhood并且键盘语义要与鼠标流程保持一致。团队还明确要求当一篇笔记确实属于多个分组时列表要保留图谱真相而不是把重叠关系折叠掉。决策将 entity 选区正式定义为 Neighborhood 模式ADR 的核心决策可以概括为两点。第一Tolaria 将SidebarSelection.kind entity正式定义为 Neighborhood 模式。笔记列表把当前选中的笔记视为邻域源并使用“标准激活笔记行的样式”将其置顶不再使用特殊卡片关系分组先展示 outgoing直接关系分组再展示 inverse/backlink 分组空分组保持可见、计数为0同一篇笔记在多条关系同时成立时允许出现在多个分组中。第二Neighborhood 导航是一个独立的“转向”pivot动作。普通点击与普通Enter只是打开聚焦的笔记不会替换当前邻域而 Cmd/Ctrl-click 与 Cmd/Ctrl-Enter会打开笔记并把整个列表转向pivot到那篇笔记自己的 Neighborhood。为什么复用 entity 而不是新增 neighborhood 变体ADR 列出了三个候选方案最终选择复用现有entity选区方案评估复用现有entity选区作为 Neighborhood 模式选定状态模型保持局部化避免再造一个几乎相同的笔记列表模式且侧边栏选中任意其他目标即可退出 Neighborhood。代价内部代码仍沿用历史性的entity命名新增neighborhood选区变体内部命名更清晰但会复制同一份源笔记负载并迫使整个应用的选区处理大范围改动产品收益却很小保留旧的隐式 entity 浏览行为短期工程量最低但产品术语不一致、去重分组与非 pivot 的 Cmd-click 等交互问题会一直保留从源码结构看这个“局部化”的取舍直接体现在选区类型的定义上。src/types.ts#L291-L296 中的SidebarSelection是一个五分支联合类型export type SidebarSelection | { kind: filter; filter: SidebarFilter } | { kind: sectionGroup; type: string } | { kind: folder; path: string; rootPath?: string } | { kind: entity; entry: VaultEntry } | { kind: view; filename: string; rootPath?: string }Neighborhood 模式就是其中{ kind: entity; entry: VaultEntry }这一分支——载荷是完整的VaultEntry因此不需要额外的状态容器。ADR 也在后果部分明确提示内部代码仍使用entity判别值未来的重构应把 “entity selection” 和 “Neighborhood mode” 视为同一概念除非更大规模的导航重设计有理由引入新的选区形态。pivot 的三种动作enter / switch / exitNeighborhood 的进入逻辑集中在纯函数 resolveNeighborhoodSelection 中export function resolveNeighborhoodSelection( currentSelection: SidebarSelection, entry: VaultEntry, ): NeighborhoodSelectionUpdate { const nextSelection: SidebarSelection { kind: entity, entry } if (selectionsEqual(currentSelection, nextSelection)) { return { action: exit, selection: { kind: filter, filter: all } } } return { action: currentSelection.kind entity ? switch : enter, selection: nextSelection, } }从源码结构看一次 pivot 会被归约为三种动作enter当前不是 entity 选区首次进入 Neighborhoodswitch当前已是某篇笔记的 Neighborhood转向另一篇笔记嵌套邻域exit对当前邻域源再次 pivot则退出模式并回到all筛选。与动作配套的是一层选区历史栈。pushNeighborhoodHistory 在选区发生实际变化时把“当前选区”压栈selectionsEqual判断当前与目标相同则不压栈popNeighborhoodHistory 做 LIFO 弹出。这使得连续 pivot 多篇笔记后可以逐层回退回退目标是“转向前的那个选区”——它可能是一个folder、filter或另一个entity。Hook 层的封装React 侧由 src/hooks/useNeighborhoodSelection.ts 提供四个 Hook 完成接线useNeighborhoodEntryL56-L81pivot 的统一入口。它先调用resolveNeighborhoodSelection发出neighborhood_mode_toggled遥测事件携带action再按动作分支exit时弹出历史栈并恢复之前的选区否则把当前选区压入历史并切换到新的 entity 选区。两次setSelection都带preserveNeighborhoodHistory: true选项避免历史栈被后续清洗逻辑误清。useSelectionSanitizerL83-L104当外部如侧边栏把选区改成非 entity 形态时清空neighborhoodHistoryRef并把列表筛选重置为open。这正是 ADR 所说“侧边栏导航是退出 Neighborhood 的路径”的实现依据。useNeighborhoodHistoryBackL106-L119弹栈回退无历史时返回false。useNeighborhoodEscapeL121-L148把键盘语义对齐到鼠标流程。Escape 的触发条件由 shouldProcessNeighborhoodEscape 精确定义键为Escape、未带 meta/ctrl/alt 修饰键、未被默认处理、当前选区kind entity且未被阻断。处理顺序上有一个细节值得注意如果焦点落在编辑器表面.editor__blocknote-container或.cm-editorEscape 只负责失焦并把焦点拉回笔记列表容器[data-testidnote-list-container]不消费历史栈焦点在其他可编辑元素时直接忽略只有列表本身持有焦点时Escape 才执行onBack()回退一层邻域。这保证了“普通按键保持当前邻域、修饰键组合才转向”的键盘语义不会与编辑器的快捷键冲突。指针侧的 pivot 判定鼠标侧的修饰键判定在 src/components/note-list/noteListUtils.tsfunction usesCommandModifier(event: PickReact.MouseEvent, metaKey | ctrlKey): boolean { return event.metaKey || event.ctrlKey }即 macOS 的 Cmd 与 Windows/Linux 的 Ctrl 走同一条分支与 ADR 中 “Cmd/Ctrl-click” 的表述一致不带修饰键的普通点击只打开笔记不触发 pivot。关系分组的构建outgoing 优先、inverse 在后、重叠保留列表内容来自 buildRelationshipGroups它以entity邻域源和全库allEntries为输入构建RelationshipGroup[]export function buildRelationshipGroups( entity: VaultEntry, allEntries: VaultEntry[], ): RelationshipGroup[] { const b new GroupBuilder(entity.path, allEntries) const rels entity.relationships if (entity.isA Type) { b.filterAndAdd(Instances, (e) e.isA entity.title) } // Direct relationships first — all keys from entity.relationships take // priority so that reverse/computed groups (Children, Events, Referenced by) // only show *additional* entries not already covered by a direct property. Object.keys(rels) .filter((k) k.toLowerCase() ! type) .sort((a, b) a.localeCompare(b)) .forEach((key) { b.addFromRefs(key, (Reflect.get(rels, key) as string[] | undefined) ?? []) }) for (const group of collectInverseRelationshipGroups(entity, allEntries)) { b.add(group.label, group.entries) } b.add(Backlinks, findBacklinks(entity, allEntries).sort(sortByModified)) return b.groups }从源码结构看分组顺序与 ADR 的 “outgoing first, inverse/backlink after” 完全对应Type 特例如果邻域源是一篇 Type 文档先加入Instances分组所有isA等于其标题的笔记直接关系outgoing取entity.relationships的全部键排除type按字母序加入。这些键来自笔记 frontmatter 中的属性引用是“主动声明”的关系逆关系inversecollectInverseRelationshipGroups扫描全库反查产出诸如Children、Events、Referenced by以及Belongs to、Related to等计算型分组Backlinks最后追加findBacklinks找到的反向链接组按修改时间排序。去重策略也在这里体现得比较微妙注释明确说明直接关系的键拥有优先权逆关系/计算型分组只展示“尚未被直接属性覆盖的额外条目”——也就是说去重只发生在计算组与直接组之间。没有被直接属性覆盖的条目仍然会同时出现在多个计算型分组中例如一篇笔记既出现在Referenced by又出现在Children。这正是 ADR 所坚持的“保留图谱真相”一篇笔记合法属于多个分组时同一笔记会在多个分组中各出现一次而不是被去重到只剩一处。分组渲染空组可见、计数恒显、组内独立排序分组的 UI 由 RelationshipGroupSection 渲染每个分组是一个可折叠的 section头部左侧是分组名与group.entries.length计数右侧是排序下拉。由于该组件对“非折叠状态下的空数组”不做额外隐藏空分组依然渲染出标题行并显示计数0与 ADR “keeps empty groups visible with count 0” 的决策一致。组内排序默认按modified降序且每个分组拥有独立的SortConfigsortPrefs[group.label] ?? { option: modified, direction: desc }还可通过extractSortableProperties支持按笔记的自定义属性排序。邻域源本身的置顶也回归了常规渲染路径ADR 的 Context 批评了旧的“特殊卡片”做法决策改为“使用标准激活笔记行样式置顶”。列表侧的行渲染通过renderItem回调统一产出置顶项与分组内项共用同一套行组件差别只是选中态样式。退出路径与整体状态流转综合上面的源码一次完整的 Neighborhood 使用闭环是在侧边栏某笔记上 Cmd/Ctrl-click或 Cmd/Ctrl-Enter→useNeighborhoodEntry解析出enter或switch当前选区压入历史栈选区变为{ kind: entity, entry }列表以该笔记为邻域源按buildRelationshipGroups渲染分组可在分组间继续 pivot 形成多层邻域Escape列表持焦时逐层回退或再次 pivot 当前邻域源触发exit回all筛选点击侧边栏任意其他目标filter/section/folder/view→useSelectionSanitizer判定新选区非 entity清空历史栈并重置列表筛选模式随之退出。第 4 步是 ADR “Options considered” 中选中方案的关键收益由于模式完全建立在既有选区联合之上退出不需要专门的状态位或命令任何侧边栏导航天然就是退出动作。验证与回归测试覆盖点该模式的测试分布在三层均可在仓库中直接查证src/hooks/useNeighborhoodSelection.test.ts验证 enter/switch/exit 分支、历史栈行为与 Escape 路由src/utils/neighborhoodHistory.test.ts对resolveNeighborhoodSelection、pushNeighborhoodHistory、popNeighborhoodHistory、selectionsEqual等纯函数的单测src/components/NoteList.keyboard.test.tsx 与 src/components/note-list/RelationshipGroupSection.test.tsx覆盖键盘流与分组渲染含计数与折叠的组件级行为。结论一个“零新增状态”的产品概念ADR 0069 的价值在于把一个既有的、命名混乱的选区分支升级为产品一等概念同时把交互语义补齐产品、测试与文档现在统一以 Neighborhood 指代这种笔记列表浏览模式列表保留重叠的关系证据键盘浏览方向键 Enter 保持邻域、Cmd/Ctrl-Enter 转向与指针流程一致侧边栏导航保留为唯一的退出路径。实现上它没有引入第二套状态——SidebarSelection联合类型、选区历史栈和纯函数式的分组构建器src/utils/neighborhoodHistory.ts、src/utils/noteListHelpers.ts共同支撑了 ADR 中 “keeps the state model localized” 的承诺也留下了一个明确的遗留约定内部entity判别值与产品术语 “Neighborhood mode” 长期共存除非导航体系被整体重设计否则二者应视为同一概念。【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolaria创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询