TREK Collections 完全指南:在自托管旅行规划器中打造跨行程的地点收藏库

TREK Collections 完全指南:在自托管旅行规划器中打造跨行程的地点收藏库 TREK Collections 完全指南在自托管旅行规划器中打造跨行程的地点收藏库【免费下载链接】TREKA self-hosted travel/trip planner with real-time collaboration, interactive maps, PWA support, SSO, budgets, packing lists, and more.项目地址: https://gitcode.com/GitHub_Trending/nomad22/TREK导读TREK 是一款自托管旅行规划应用其核心数据模型围绕“行程Trip”组织而Collections收藏集则是一个独立于任何行程的全局地点库你可以在多条命名列表中保存“挪威自驾心愿单”“里斯本最佳咖啡店”“Someday”之类的地点清单为每个地点标记 Idea / Want to go / Visited 状态并通过融合fusion机制把列表分享给其他用户实时协作。本文将以 wiki/Collections.md 为主线结合 TREK 仓库中的前端状态层、API 客户端、服务端权限模型与共享 Zod Schema 源码系统讲解 Collections 的概念模型、完整操作流程、去重与复制语义、自定义标签机制以及服务端权限强制策略帮助你从管理员、普通用户和二次开发者三个视角吃透这一全局 Addon。Collections 是什么与行程地点截然相反的存储哲学在 TREK 中保存到某个行程的地点只存在于该行程内部随行程而生、随行程而灭。Collections 恰好相反它是一个归属于你个人、与任何行程无关的服务器级地点图书馆。规划 A 行程时偶然发现的好地方保存进收藏列表后规划 B 行程时它依然在那里。文档特别强调了一个关键设计地点在行程与收藏列表之间是“复制”而非“引用”copied, never linked。也就是说收藏列表里编辑一个地点永远不会影响行程里的同名地点反之亦然。这一语义可以从服务端实现中得到印证——复制操作在server/src/services/collectionsService.ts的copyToTrip中执行的是INSERT INTO places直接把源字段名称、描述、分类、备注、价格、坐标、图片、标签物化进目标行程收藏地点与行程地点之间不存在外键关联。管理员前置条件Collections 是一个全局 Addon需要在管理面板中启用见 Admin-Addons。Addon 的注册信息记录在 server/src/db/seeds.ts其 ID 为collections见 server/src/addons.ts 中的ADDON_IDS.COLLECTIONS。关闭 Addon 会隐藏导航入口并让整组 API 路由返回 404但不会删除任何数据重新开启即可恢复访问。界面入口与访问方式当管理员启用 Addon 后主导航会出现Collections入口移动端表现为底部 Tab打开后是三栏布局左侧是你的列表栏rail中间是当前激活列表的地点右侧是地图Dashboard 上会增加一个 Collections Widget把你的列表以紧凑徽章形式展示出来实现在 client/src/components/Dashboard/CollectionsWidget.tsx仅调用list()一次以复用服务端算好的place_count避免 N1 查询。每个用户都可以在 Display-Settings 中单独隐藏该 Widget不影响其他用户。前端页面的状态全部收敛在 client/src/store/collectionStore.ts基于 Zustand它维护了collections、activeId、places、members、labels、incomingInvites以及各种筛选/选择状态数据获取统一走 client/src/api/collections.ts 封装的 Axios 调用所有请求体都通过satisfies与trek/shared的 Zod Schema 保持单一事实来源。列表Lists管理创建列表从左侧栏的New list动作创建。一个列表包含以下字段见 shared/src/collection/collection.schema.ts 中的collectionCreateRequestSchema字段约束说明name必填1~120 字符列表名称description可选最长 2000 字符列表描述color可选#RGB或#RRGGBB列表主色未提供时服务端默认#6366f1靛蓝icon可选最长 40 字符图标名默认Bookmarkcover_image可选最长 500 字符封面图可上传本地图片或从 Unsplash 搜索links数组最多 30 条每个链接由label≤120 字符和url组成url强制匹配^https?://从 Schema 层直接封死javascript:/data:等危险协议上传封面走POST /api/addons/collections/:id/cover控制器限制在 server/src/nest/collections/collections.controller.ts最大 20MB仅允许jpg/jpeg/png/gif/webp且拒绝 SVG文件以 UUID 重命名存入uploads/covers目录。更换封面时服务端还会回收旧封面文件路径被严格限定在 covers 目录内见collectionsService.ts的deleteOldCollectionCover。编辑与删除列表的Edit按钮仅 owner 可见可编辑上述所有字段、调整排序sort_order。删除列表会级联删除其中的收藏地点——从源码看DELETE FROM collections依靠数据库外键 CASCADE 一并清掉 members、places、tags、labels 关联删除前服务端会先快照所有已接受/待接受成员并向他们广播collections:deleted事件。All saved 视图All saved是一个内建视图相当于把“你拥有或共同拥有co-own的所有列表”做并集让你能跨整个地点库一次性搜索和批量操作。前端实现上它是一个哨兵值all见 client/src/store/collectionStore.ts 的ALL_SAVED进入时并行拉取每个可见列表再合并同时因为标签是按列表隔离的跨列表的“All saved”视图中标签筛选会被禁用。地点状态Place Status一键循环每个收藏地点携带一个状态支持一次点击循环切换。TREK 的状态枚举定义在共享 Schema 中COLLECTION_STATUSES [idea, want, visited]Idea— 记下了但尚未下定决心Want to go— 进入候选短名单Visited— 去过了。前端把状态渲染元数据图标、i18n 文案、徽章颜色集中在 client/src/pages/collections/collectionsModel.ts 的STATUS_META中Idea 用灰调圆圈、Want 用主题色书签、Visited 用绿色对勾。循环顺序由nextStatus实现idea → want → visited → ideaUI 组件是 client/src/components/Collections/StatusBadge.tsx。服务端对应POST /places/:pid/status接口。需要特别留意的是状态是 Collections 独有的概念把地点复制进行程时不会携带状态——批量从行程复制进列表时状态固定初始化为idea见saveFromTripPlaces的 INSERT 语句硬编码idea。分类Categories复用全站分类体系收藏地点可以挂接分类category使用的正是 TREK 全局管理员定义的那套分类见 Admin-Categories。分类的颜色与图标会显示在地点头像、地点详情和列表行上并且可以按分类筛选列表。从collectionPlaceSchema可以看到地点通过category_id引用分类服务端查询时LEFT JOIN categories把category_name/color/icon水合进返回结构见collectionsService.ts的hydratePlaces。添加地点三条路径1. 搜索并添加Search and add点击打开搜索选中结果后在保存前一次性设定名称、分类、状态、Markdown 描述和链接。请求体为collectionSavePlaceRequestSchema除了基本字段还支持google_place_id、google_ftid、osm_id、price、currency、website、phone等完整的地点属性以及tag_ids挂接全站标签和force“重复也照样添加”。2. 从行程保存单个地点地点检查器place inspector和行程地点右键菜单都提供Save to collection可以把该地点在“你拥有的各列表”中切换进出。前端对应SaveToCollectionModal与CollectionPicker见 client/src/components/Collections/SaveToCollectionModal.tsx请求走POST /places/from-trip——注意这个接口的 DTO只传collection_id、source_trip_id、source_place_id和force服务端自行读取行程地点并按字段映射复制见saveFromTripPlace。3. 从行程批量添加在行程地点列表进入多选模式勾选若干地点后使用选择栏的Save to collection一次性把多个地点复制进选定的列表。请求走POST /places/from-trip-manysource_place_ids上限 1000服务端saveFromTripPlaces用单条 SQL 预编译语句循环插入一次权限检查、一次 WebSocket 通知性能优于逐条保存。重复项按名称或坐标会被自动跳过返回{ copied, skipped }结构。去重语义Dedup去重逻辑是理解整个系统行为的关键实现位于collectionsService.ts有名称时按trim().toLowerCase()归一化后精确匹配列表内已存地点无名称但有坐标时按**坐标容差 0.0001约 11 米**匹配命中后返回{ duplicate: true, duplicateOf }除非请求带force: true强制执行。findMembership供地点检查器的“已收藏”角标使用还提供了一个值得注意的细节仅靠名称不会判定为同一地点——因为“Starbucks”这类重名会造成大量误报只有在具备google_place_id、google_ftid或坐标近邻信号时才会判定“已保存”。地点详情Place Detail点击已存地点弹出详情面板包含封面照片地点没有自带图片时自动抓取分类该列表的标签labels实时状态控件一键循环Markdown 描述与链接编辑、复制进行程、从列表移除三个动作。编辑面板client/src/components/Collections/CollectionPlaceDetail.tsx同样允许你给地点指派标签。注意collectionPlaceUpdateRequestSchema里对状态字段做了一个精妙的处理用.removeDefault()剥离内层.default(idea)使得请求中缺省 status 时解析为 undefined保持原样而不是被注入成idea同时保留.catch(idea)兜底非法值——这是一个值得在 API 设计中借鉴的 Zod 技巧。筛选与批量操作地点列表上方是紧凑的筛选栏client/src/components/Collections/CollectionFilterBar.tsx按状态筛选按分类筛选按标签筛选Select多选开关。筛选/搜索的核心是一个纯函数filterPlacescollectionsModel.ts它同时驱动列表和地图保证两者始终同步——标签筛选采用 OR 语义地点携带任一选中标签即命中搜索覆盖 name / address / notes。mappablePlaces会过滤出有有效坐标的地点用于地图渲染。多选模式下你可以Select all全选当前筛选结果Assign label给所有选中地点批量打标签Copy to trip把选中地点复制进任意行程携带名称、描述、分类、备注、价格、坐标、照片和标签Move或Duplicate到另一个列表Delete删除所选。其中 Copy to trip 的服务端实现在copyToTrip会先校验目标行程的可编辑权限place_edit再对每一个源地点做可见性检查防止跨用户数据渗出然后对目标行程建立去重集、逐条复制并把收藏地点上的全站标签一并复制进place_tags。自定义标签Custom Labels列表内部的二次分组全站分类是共享的但每个列表还可以定义自己的标签。比如在 “Germany 2026” 列表中创建Berlin、Hamburg、Ostsee三个标签对地点做分类之外的二次分组。标签只属于创建它的那一个列表并共享给列表上的所有成员。管理从筛选栏进入标签管理器client/src/components/Collections/LabelManager.tsx可创建、重命名、换色、删除标签。服务端限制collectionsService.ts每个列表最多50个标签MAX_LABELS_PER_COLLECTION标签名按大小写不敏感去重重复返回 409颜色必须为#RGB/#RRGGBB格式名称 1~60 字符。指派在详情面板给单个地点指派或在选择栏用Assign label批量指派。批量指派接口POST /labels/assign以及unassign会把地点按列表分组、逐组做权限检查并且只应用属于该列表的标签跨列表传参的标签会被静默过滤。筛选在筛选栏选择一个或多个标签即可把地点列表和地图同时收窄到携带其中任一标签的地点。权限边界非常清晰管理和指派标签需要编辑权Editor 或 Admin按标签筛选则所有成员包括 Viewer都可使用。另外把地点移动到另一个列表会丢弃其标签——因为标签归属于源列表updatePlace在检测到collection_id变更时会先DELETE掉该地点的全部标签关联。分享列表Fusion与成员角色列表默认私有。owner 可以通过邀请其他用户来分享列表机制与 TREK 的 Vacay 融合fusion类似被邀请的用户接受邀请后才能看到列表之后所有变更通过 WebSocket实时同步。服务端 WebSocket 通知由notifyCollectionUsers负责向 owner 和所有已接受成员广播collections:updated事件同时通过X-Socket-Id请求头把发起方自己排除在广播之外避免自己改自己还要刷新的冗余消息。邀请流程的数据结构在collectionMemberSchema成员带status: pending | accepted与role字段owner 本身不算成员行buildMembers会把 owner 合成一条is_owner: true、role 为admin的记录。成员角色表分享时 owner 为每位成员分配权限角色且可随时更改角色可以做什么Viewer查看列表、把地点复制进自己的行程、按标签筛选不能改动列表Editor默认新增与编辑地点管理并指派列表标签AdminEditor 的全部能力外加删除地点Owner 始终拥有完整控制权。owner 可以移除成员成员也可以自行退出共享列表leave。邀请相关接口还有一条防枚举设计控制器注释明确指出/invite、/invite/cancel、/:id/available-users都在控制器层做了显式 owner 检查。服务端强制权限文档强调“权限在服务端强制执行角色只能做它被允许的事”。从collectionsService.ts的代码可以清晰看到三层守卫assertAccess不可见则返回404而不是 403——刻意隐藏列表的存在性防止枚举assertCanEditviewer 编辑时返回403“You have read-only access to this list”assertCanDelete非 owner/admin 删除时返回403“Only an admin can delete places from this list”。此外整个路由组挂在CollectionsAddonGuardserver/src/nest/collections/collections-addon.guard.ts之下Addon 未启用时无论是否登录都返回404且守卫声明在JwtAuthGuard之前以保证“Addon 检查优先于 401”。前端 UI 上的按钮显隐只是体验层真正的安全边界始终在服务端。与行程的复制往返完整字段清单收藏地点 ↔ 行程地点的双向复制是最核心的使用场景字段映射完全对称。根据saveFromTripPlace与copyToTrip的实现往返携带的字段为name, description, lat, lng, address, category_id, price, currency, notes, image_url, google_place_id, google_ftid, osm_id, website, phone外加方向相关的附加信息行程 → 收藏记录source_trip_id/source_place_id溯源provenance状态初始化为idea可附加全站标签收藏 → 行程复制全站标签到行程地点去重集基于目标行程的既有地点。两次复制都是“物化副本”因此你可以在两个世界各自独立编辑同一地点而互不干扰——这正是 Collections 文档开篇承诺的语义。相关文档Addons-Overview — Addon 总览与完整清单Admin-Addons — 管理员如何开关 AddonAdmin-Categories — 全站分类管理Dashboard-Widgets — Dashboard Widget 配置Vacay — 融合fusion共享机制的同源实现核心源码索引前端 API 封装 client/src/api/collections.ts、状态层 client/src/store/collectionStore.ts、纯逻辑模型 client/src/pages/collections/collectionsModel.ts、服务端路由 server/src/nest/collections/collections.controller.ts、业务实现 server/src/services/collectionsService.ts、共享 Schema shared/src/collection/collection.schema.ts。【免费下载链接】TREKA self-hosted travel/trip planner with real-time collaboration, interactive maps, PWA support, SSO, budgets, packing lists, and more.项目地址: https://gitcode.com/GitHub_Trending/nomad22/TREK创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考