WeKan 的 Home 板块:登录直达看板的设计与实现

发布时间:2026/9/13 4:49:31
WeKan 的 Home 板块:登录直达看板的设计与实现 WeKan 的 Home 板块登录直达看板的设计与实现【免费下载链接】wekanThe Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . PR source translation to imports/i18n/data/en.i18n.json, other translations at https://app.transifex.com/wekan/wekan项目地址: https://gitcode.com/GitHub_Trending/we/wekanWeKan 的 All Boards全部看板页面内有一个Home主页板块被标记为 Home 的看板会在用户登录后自动打开替代默认的 All Boards 页面。本文基于 docs/Features/Board/Home.md 这篇设计文档结合 WeKan 仓库中的 URL 规则模块、看板列表组件、服务端 Meteor 方法与测试用例完整讲清 Home 板块的交互规则、HTML5 拖放细节与后端约束读完后你能理解“一个标记字段如何支撑一套登录直达机制”并知道如何在源码中定位其每一环。Home 是什么一个标记不是一个位置Home 板块的底层数据是一个一直存在的用户档案字段profile.defaultBoardId。它最早可以通过 All Boards 多选面板Multi-Selection中的Set as Home board (opened after login)写入但那时没有任何界面能“读出”当前 Home 是谁——这个设置是只写的想确认选了哪个看板只能注销再登录。现在的做法是把 Home 做成All Boards 页面的一个板块在左侧菜单中与 Starred收藏、Templates模板并列成一行回答“到底哪个看板是我的 Home”这个问题。Home 与 Starred 的本质区别在于数量任意多个看板都可以被星标看板中至多一个是 Home或者一个都没有因为登录只能打开一个看板。和星标一样Home 是打在某个看板上的标记mark而不是存放看板的位置把某个看板设为 Home 不会把它从 Remaining未分组或它所在的 workspace 移走它仍留在原地同时也会出现在 Home 板块里取消 Home 也完全不影响它的本来位置。字段定义models/users.js用户集合上围绕该字段封装了三个方法models/users.jsgetDefaultBoardId()返回profile.defaultBoardId没有则返回nullisDefaultBoard(boardId)判断某个看板是否就是 HometoggleDefaultBoard(boardId)是则$unset否则$set——供多选面板的切换行使用。字段本身在用户文档 schema 中亦有声明profile.defaultBoardId位于 models/users.js 的 schema 定义中。左侧菜单中的 Home 行位置与排序规则Home 行的位置由纯函数 menuSectionOrder() 决定位于 models/lib/allBoardsUrls.jsfunction menuSectionOrder(hasStarredBoards) { const boardLists hasStarredBoards ? [SECTION_STARRED, SECTION_REMAINING] : [SECTION_REMAINING, SECTION_STARRED]; return [...boardLists, SECTION_HOME, SECTION_TEMPLATES, SECTION_ARCHIVE]; }规则要点均被 tests/homeBoard.test.cjs 逐条断言Home 位于两个看板列表Starred/Remaining之下、Templates 与 Archive 之上顺序固定为[..., home, templates, archive]Home 永远不是第一行页面打开时落在的第一行是 Starred有星标时或 Remaining无星标时由defaultSection(hasStarredBoards)决定。理由很实际——登录后用户已经在Home 看板里此时点开 All Boards 的语义是“看看我的看板”用他刚刚离开的那一个看板作答毫无意义无论有没有看板在 Home 里这一行都显示作为拖放目标的容器必须先于内容存在。行内的图标、文案与计数Home 行的元数据在 client/components/boards/boardsList.js 的menuSections()中定义icon: fa-home、labelKey: home复用 i18n 里早已存在的home键已随 WeKan 的全部语言翻译以及专属类js-home-menu——因为这一行本身还是拖放目标。行尾的计数显示0或1计数从homeBoardId()读profile.defaultBoardId只有当该看板确实属于当前用户可见的看板列表时allBoards.some((b) b._id id)才计 1。之所以要查“看板还在不在”是因为一个已删除或已归档的 Home 看板会把过期 id 留在档案里一行显示1却底下空无一物看起来就是坏了测试同时断言homeBoardId()只读用户文档、不查看板集合tests/homeBoard.test.cjs 中 “Home is read from the user document, not from the boards” 用例若计数依赖看板订阅菜单就会在订阅追平前反复重排界面会“抖”。地址Home 与其他板块使用同形地址/allboards/home由同一套 URL 模块生成SECTION_HOME homemodels/lib/allBoardsUrls.js。测试确认allBoardsPath(home, [])恰好产出/allboards/home且手输该地址会解析为自身而不是回落到默认板块。更多地址规则见 All Boards URLs 文档。Home 里没有“Add Board”瓦片Home 板块不提供创建看板的入口在这里新建的看板不会成为登录后打开的那个瓦片等于许下它兑现不了的承诺。空 Home 显示一行提示文本i18n 键home-board-emptyDrag only one board here to open it after login这句提示在拖放开始之前就把“只收一个”的限制说清了比一片空白更有用。拖放交互设 Home 与撤 Home拖入一次拖放替换而非切换把任意看板拖到 Home 行上它即成为登录后打开的看板。All Boards 的看板图标本来就能拖到 Remaining、Archive、workspace 等行上Home 行只是同一列里多了一个目标手势不学新东西。对应的 drop 处理在 client/components/boards/boardsList.js 的drop .js-home-menu中直接Meteor.call(setDefaultBoard, boardIds[0], ...)——替换Home 只装一个拖入谁就是谁不管原来是谁。它刻意不做 toggle一个有时设、有时清的拖放取决于拖拽过程中读不到的状态你不可能在拖动时记起 Home 现在是谁与拖放相对的替代入口是多选面板的Set as Home board行它调用toggleDefaultBoard仍然是切换语义——因为那里你是点击一个看得见当前状态的具体看板client/components/boards/allBoardsSidebar.js多选拖放不会静默取第一个boardIds.length ! 1时弹Please select only one boardi18n 键select-only-one-board不做任何改动且保留当前选择以便继续收窄随后直接return不触及 Home。测试明确断言“拒绝分支必须先于setDefaultBoard调用出现”。拖出只能落在 Remove 目标上从 Home 拖出看板时它恰好只能落在一个地方——拖拽期间出现的Remove 目标条其余所有目标一律拒绝。这个交互借鉴 Android 启动器launcher从桌面拿起图标时顶部浮现 Remove 条图标拖上去时它变红松手即移除桌面快捷方式——而应用本身还在抽屉里。这里同理看板仍在 Remaining 或它的 workspace 中只是不再充当登录后打开的那个。Remove 条的呈现纪律client/components/boards/boardsList.jade client/components/boards/boardsList.js client/components/boards/boardsList.css只在 Home 看板真正被拖起时绘制。由draggingFromHome new ReactiveVar(false)驱动dragstart时置truedragend时清false覆盖“放下/取消/丢到空白处”三种收尾模板侧由 helpershowsHomeRemoveTarget()判断“当前选中 Home 板块 draggingFromHome.get()”后渲染li.home-remove-target带垃圾桶图标与文案i18n 键home-board-remove。一个在手势可行时才出现的可操作元素是自解释的一个永远躺在那里的垃圾桶是没人敢按的按钮横跨看板区整行宽度CSSflex-basis: 100%——它是靶子需要瞄准的靶子必然被瞄偏只有图标悬在其上时才变红静止状态不含红色.home-remove-target规则里没有#c0392b.home-remove-target.is-over下才有——红色是对“在这里松手会发生什么”的回答而不是对一个无人触碰的看板的常设警告先问后做drop 处理先confirm(TAPi18n.__(home-board-remove-confirm))确认文案明确说明看板本身不会被删除之后才Meteor.call(clearDefaultBoard, boardId)点“否”什么都不发生。测试断言 confirm 出现的位置必须先于方法调用。所有其他目标在 dragover 阶段就拒绝Remaining、Starred、Templates、Archive 和 workspace 树在dragover中都会检查“这块看板是否来自 Home”若是则不调用evt.preventDefault()——这正是 HTML5 拖放协议里“拒绝”的表达方式光标在板子还在空中时就示意不可放而不是放下之后才安静地什么都不发生。相应 drop 处理器drop .js-select-menu、drop .js-open-archived-board、drop .workspace-node里也各有一句if (isDragFromHome(evt)) return;兜底防某些浏览器仍投递 drop。为什么标记放在“类型名”里HTML5 拖放有个硬约束dragover阶段不能调用getData()。拖放数据存储在 drop 之前处于保护模式暴露的只有类型名列表。因此“来自 Home”这个事实必须放在类型名本身里// client/components/boards/boardsList.js const DRAG_FROM_HOME application/x-board-from-home; // dragstart在 Home 中拿起看板时 evt.originalEvent.dataTransfer.setData(DRAG_FROM_HOME, 1); // dragover 可读的判定 return Array.prototype.indexOf.call(types, DRAG_FROM_HOME) ! -1;该类型在dragstart写入其值从不被读取——类型的存在本身就是消息。tests/homeBoard.test.cjs 专门断言isDragFromHome从dataTransfer.types读取、源码中不出现getData(、且每个其他目标的 dragover 拒绝都发生在preventDefault()之前。文档还记录了一个设计教训早期版本允许把看板从 Home 拖到其他任意行时“路过即清除 Home”结果每次拖放都变成一次 Home 拖放——把看板归档进 workspace 时可能意外失去 Home。最终原则是一个手势一个目的地目的地自己说明它将做什么代码中已不存在clearHomeIfDraggedFromHome这类“顺路清除”函数。服务端强制的规则客户端手势之外的最终守门在服务端方法server/models/users.jssetDefaultBoardasync setDefaultBoard(boardId) { check(boardId, String); if (!this.userId) throw new Meteor.Error(not-logged-in, User must be logged in); const board await Boards.findOneAsync({ _id: boardId, archived: false, members.userId: this.userId, }); if (!board) throw new Meteor.Error(board-not-found, Board not found); await Users.updateAsync(this.userId, { $set: { profile.defaultBoardId: boardId }, }); }只接受调用者是成员且未归档的看板。否则用户每次登录都会被重定向到一个打不开、拒绝渲染的看板查询条件与看板 publication 应用的成员测试一致check(boardId, String)必须先于任何return执行audit-argument-checks 包会检查所有参数都被 check 过语义是无条件替换——与拖放 UI 的“替换而非切换”一致。方法注释里直接点明这里若用toggleDefaultBoard的切换语义会引入拖拽者看不见的状态依赖。clearDefaultBoardasync clearDefaultBoard(boardId) { check(boardId, String); if (!this.userId) throw new Meteor.Error(not-logged-in, User must be logged in); await Users.updateAsync( { _id: this.userId, profile.defaultBoardId: boardId }, { $unset: { profile.defaultBoardId: } }, ); }更新选择器额外匹配profile.defaultBoardId: boardId只有当传入的看板确实就是该用户的 Home 时才清除。从别的列表里拖出某个看板不可能产生“顺手清掉某人 Home”的副作用。谁来调用这些方法setDefaultBoard与clearDefaultBoard都由显式手势触发拖放到 Home 行 / 拖到 Remove 目标并确认。没有任何自动逻辑会写这里——特别地Sandstorm 的 auto-open打开 grain 的单个看板见 models/lib/sandstormAutoOpen.js只负责打开不允许顺带决定该用户从哪个看板开始tests/sandstormAutoOpen.test.cjs 有负向守卫断言该文件中不出现setDefaultBoard。登录后的重定向字段被读取的终点在路由层config/router.js登录后解析user.getDefaultBoardId()存在则将该会话重定向到对应看板一次/会话。这也解释了文档开头的设计动机——“每天只在一个看板里干活的人早上打开 WeKan 就直接在板子里而不是从列表开始点同一个瓦片”。相关文档与延伸阅读All Boards — Home 所在页面的整体说明All Boards URLs —/allboards/home所在的地址体系Archive — 看板可拖入的另一个板块Archive and Delete — 归档一个看板到底做了什么实现文件索引文件路径类型说明models/lib/allBoardsUrls.js纯.js模块SECTION_HOME常量、menuSectionOrder()Home 位于两个看板列表之下、allBoardsPath()、sectionTitleKey()标题与菜单高亮共用home键client/components/boards/boardsList.js客户端组件Home 行、0/1 计数、板块过滤sel home时只列那一个看板、DRAG_FROM_HOME、draggingFromHome、全部设/清 Home 的拖放路径client/components/boards/boardsList.jade.jade模板菜单行、Remove 条home-remove-target 垃圾桶图标 home-board-remove文案、空 Home 提示client/components/boards/boardsList.css.cssRemove 条整行铺满flex-basis: 100%、静止不红、.is-over时#c0392bclient/components/boards/allBoardsSidebar.js客户端组件多选面板里独立的Set as Home board行调用toggleDefaultBoard切换语义models/users.js用户集合getDefaultBoardId、isDefaultBoard、toggleDefaultBoardserver/models/users.js服务端方法setDefaultBoard替换、成员校验与clearDefaultBoard只清自己的 Homeconfig/router.js路由登录后每会话一次的 Home 重定向tests/homeBoard.test.cjsNode 测试排序、计数、拖放、Remove 条与服务端拒绝的全套断言tests/defaultBoard.test.cjsNode 测试toggleDefaultBoard的档案更新语义与鉴权守卫【免费下载链接】wekanThe Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . PR source translation to imports/i18n/data/en.i18n.json, other translations at https://app.transifex.com/wekan/wekan项目地址: https://gitcode.com/GitHub_Trending/we/wekan创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考