WeKan 的 Home 板块:登录直达看板的设计与实现
2026/9/13 4:49:25 网站建设 项目流程

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/wekan

WeKan 的 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.js):

  • getDefaultBoardId():返回profile.defaultBoardId,没有则返回null
  • isDefaultBoard(boardId):判断某个看板是否就是 Home;
  • toggleDefaultBoard(boardId):是则$unset,否则$set——供多选面板的切换行使用。

字段本身在用户文档 schema 中亦有声明('profile.defaultBoardId'位于 models/users.js 的 schema 定义中)。

左侧菜单中的 Home 行

位置与排序规则

Home 行的位置由纯函数 menuSectionOrder() 决定(位于 models/lib/allBoardsUrls.js):

function 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 逐条断言:

  1. Home 位于两个看板列表(Starred/Remaining)之下、Templates 与 Archive 之上,顺序固定为[..., 'home', 'templates', 'archive']
  2. Home 永远不是第一行:页面打开时落在的第一行是 Starred(有星标时)或 Remaining(无星标时),由defaultSection(hasStarredBoards)决定。理由很实际——登录后用户已经Home 看板里,此时点开 All Boards 的语义是“看看我的看板”,用他刚刚离开的那一个看板作答毫无意义;
  3. 无论有没有看板在 Home 里,这一行都显示:作为拖放目标的容器必须先于内容存在。

行内的图标、文案与计数

Home 行的元数据在 client/components/boards/boardsList.js 的menuSections()中定义:icon: 'fa-home'labelKey: 'home'(复用 i18n 里早已存在home键,已随 WeKan 的全部语言翻译),以及专属类js-home-menu——因为这一行本身还是拖放目标。

行尾的计数显示01

  • 计数从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 = 'home',models/lib/allBoardsUrls.js)。测试确认allBoardsPath('home', [])恰好产出/allboards/home,且手输该地址会解析为自身而不是回落到默认板块。更多地址规则见 All Boards URLs 文档。

Home 里没有“Add Board”瓦片

Home 板块不提供创建看板的入口:在这里新建的看板不会成为登录后打开的那个,瓦片等于许下它兑现不了的承诺。空 Home 显示一行提示文本(i18n 键home-board-empty):

Drag 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 board(i18n 键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):

  1. 只在 Home 看板真正被拖起时绘制。由draggingFromHome = new ReactiveVar(false)驱动:dragstart时置truedragend时清false(覆盖“放下/取消/丢到空白处”三种收尾);模板侧由 helpershowsHomeRemoveTarget()判断“当前选中 Home 板块 &&draggingFromHome.get()”后渲染li.home-remove-target,带垃圾桶图标与文案(i18n 键home-board-remove)。一个在手势可行时才出现的可操作元素是自解释的;一个永远躺在那里的垃圾桶是没人敢按的按钮;
  2. 横跨看板区整行宽度(CSSflex-basis: 100%)——它是靶子,需要瞄准的靶子必然被瞄偏;
  3. 只有图标悬在其上时才变红:静止状态不含红色(.home-remove-target规则里没有#c0392b),.home-remove-target.is-over下才有——红色是对“在这里松手会发生什么”的回答,而不是对一个无人触碰的看板的常设警告;
  4. 先问后做: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-menudrop .js-open-archived-boarddrop .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 专门断言:isDragFromHomedataTransfer.types读取、源码中不出现getData(、且每个其他目标的 dragover 拒绝都发生在preventDefault()之前。

文档还记录了一个设计教训:早期版本允许把看板从 Home 拖到其他任意行时“路过即清除 Home”,结果每次拖放都变成一次 Home 拖放——把看板归档进 workspace 时可能意外失去 Home。最终原则是:一个手势,一个目的地,目的地自己说明它将做什么;代码中已不存在clearHomeIfDraggedFromHome这类“顺路清除”函数。

服务端强制的规则

客户端手势之外的最终守门在服务端方法(server/models/users.js):

setDefaultBoard

async 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的切换语义会引入拖拽者看不见的状态依赖。

clearDefaultBoard

async 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”的副作用。

谁来调用这些方法

setDefaultBoardclearDefaultBoard都由显式手势触发(拖放到 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_HOMEdraggingFromHome、全部设/清 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#c0392b
client/components/boards/allBoardsSidebar.js客户端组件多选面板里独立的Set as Home board行,调用toggleDefaultBoard(切换语义)
models/users.js用户集合getDefaultBoardIdisDefaultBoardtoggleDefaultBoard
server/models/users.js服务端方法setDefaultBoard(替换、成员校验)与clearDefaultBoard(只清自己的 Home)
config/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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询