十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

WeKan Table Page 统一表格页设计:一份模板驱动所有分页表格的实现剖析

WeKan Table Page 统一表格页设计:一份模板驱动所有分页表格的实现剖析 WeKan Table Page 统一表格页设计一份模板驱动所有分页表格的实现剖析【免费下载链接】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 开源看板基于 Meteor 构建中的docs/Features/Page/Table.md设计文档展开深入剖析其一份设计、一份实现的统一表格页架构标题、状态行、控件行搜索 分页与数据表格如何由一个模板与一套纯函数驱动覆盖 Admin Panel 的全部报表、People 各分页、Translation 与 All Boards/Public Boards。读完你将掌握tablePage模板的结构、pageInfo/buildRows/buildHeader等纯辅助函数的实现原理、服务端limit/skip分页与计数方法的协作方式以及如何用三步为 WeKan 新增一个全新的表格页。设计目标消灭十个长得很像却各自漂移的页面WeKan 过去存在大量重复实现的分页表格Files、Rules、Boards、Cards、Impersonation、Recovery 以及 Security、Speed、Tests、CPU usage 四条事件流曾经是十份几乎相同的页面——同样的标题 搜索 上一页/下一页标记、同样的 currentPage/totalPages 辅助逻辑、同样的点击与键盘事件处理器只是每次用不同的js-前缀重新敲一遍。它们逐渐漂移一个布局修复需要在十个地方各改一次见 tests/tablePage.test.cjs 对这一历史问题的说明。该设计文档确立的核心原则是单一来源Single Source所有分页表格共有的东西——布局、控件、分页规则——只定义一次放在一个模板里。页级设计文档只描述这张表与其它表的差异并链接回本文档绝不重述布局、控件或分页规则。页面间只差一个列清单一页与另一页唯一的区别就是它的列定义column spec。共享类名 共享处理器因为类名全局共享事件处理器也共享——正在操作的是哪个页面由当前打开的是哪个报表决定而不是每页一个专属类。因此新增一个表格页不会新增任何事件处理器、任何标记、任何 CSS。相关文件一览以下文件构成了整个表格页体系路径均从仓库根目录出发文件路径类型职责client/components/settings/tablePage.jade.jade模板唯一的表格页标题、可选状态行、控件、表格。行顺序在此定义一次client/components/settings/tablePage.css.css样式布局全宽、等宽列、单元格换行、≤800px 时菜单与表格上下堆叠。不负责按钮颜色client/components/main/paginationControls.css.css样式全应用所有上一页/下一页分页器的颜色含表格页跟随用户主题色回退到 WeKan 默认蓝client/components/forms/forms.css.css样式全局button规则分页样式表必须出规格压过它修改前务必阅读paginationControls.css中的注释models/lib/tablePage.js.js纯函数模块pageInfo()、buildRows()、buildHeader()、columnWidthPercent()等。无 DOM、无数据库可单测client/components/settings/adminProblems.js.jsBlaze 模板逻辑每个 Admin Panel 表格的列规格、分页状态、订阅与共享控件处理器client/components/settings/adminProblems.jade.jade模板Admin Panel / Problems侧边菜单以及它渲染哪个页面client/components/settings/adminProblems.css.css样式只放不共享的东西CPU 状态卡片、侧边菜单分隔线client/components/settings/translationBody.jade.jade模板Admin Panel / Settings / Translation面板、交互行与 New 表头client/components/settings/translationBody.js.jsBlaze 模板逻辑Translation 的列规格、分页状态与共享控件处理器的份额server/publications/translation.js.js发布器Translation一页自定义翻译字符串limit/skipclient/features/settings.js.js导入清单把模板、样式表与逻辑注册进客户端 bundle。从未被 import 的文件根本不会加载server/publications/attachments.js.js发布器 方法Files Report一页附件以及getAttachmentsReportCountserver/publications/rules.js.js发布器 方法Rules Report一页规则以及getRulesReportCountserver/publications/boards.js.js发布器 方法Boards Report一页看板以及getBoardsReportCountserver/publications/cards.js.js发布器 方法Cards Report 与 Broken cards各一页卡片以及getCardsReportCount/getBrokenCardsReportCountserver/publications/impersonationReport.js.js发布器 方法Impersonation Report一页事件以及getImpersonationReportCountserver/publications/recoveryReport.js.js发布器 方法Recovery一页恢复事件以及getRecoveryReportCountmodels/eventLog.js.js模型 方法Security / Speed / Tests / CPU usage 事件流eventLogPage、eventLogCount以及让它们保持快速的{stream, at}复合索引tests/tablePage.test.cjs.cjsNode 测试覆盖以上所有内容的唯一测试套件纯函数、模板、本文档承诺的布局规则、主题化分页器、服务端分页与索引支撑、一份实现保证并校验上表每个路径仍然存在值得一提tests/tablePage.test.cjs运行时node tests/tablePage.test.cjs会直接读取docs/Features/Page/Table.md断言文档中列出的每个文件都存在、每个报表名称都出现在文档里、相关文件表没有重复条目tests/tablePage.test.cjs。也就是说设计文档本身就是测试的保护对象——文档撒谎路径失效、页面名缺失测试就会失败。不使用此设计的页面设计文档明确记录了哪些带分页表格的页面不基于此设计构建以及原因避免后人重新读代码推导也让缺口可见而非被遗忘表格名称菜单路径不使用的原因People —— 其剩余的两个非表格面板Admin Panel / PeopleLocked users 与 Shared templates不是表格前者是带 Save 按钮的数字锁定设置表单后者是作用域的复选框列表没有可渲染的行集合强行套用只会给表单外面裹一层表格。People 的四个表格面板Domains、Organizations、Teams、People已全部转换并列入下文Roles 也新增了自己的表格Roles Status同样列在下文People 已完全转换它的四个表格面板都通过本设计渲染其搜索框、过滤下拉框、操作按钮和总数都是共享控件行的一部分。原来的独立 Search 按钮已移除——每个表格页都在按 Enter 时搜索测试在 tests/tablePage.test.cjs 中断言-search-button已不存在、保留keydown .js-table-page-search的 Enter 搜索。使用此设计的页面Admin Panel菜单路径即到达该页面的点击路径。表格名称菜单路径描述PeopleAdmin Panel / People / People每个用户邮箱、管理员标记、激活状态、锁定状态、创建日期。交互行rowTemplate与两个模板表头新建用户行与全选复选框TeamsAdmin Panel / People / Teams每个团队及其详情与按团队的功能开关。与 Organizations 同构一个rowTemplate加三个headerTemplate列OrganizationsAdmin Panel / People / Organizations每个组织及其详情与按组织的功能开关。交互行因此提供rowTemplate三个表头携带全选对以headerTemplate提供DomainsAdmin Panel / People / Domains每个使用中的邮箱域名及其用户数。第一个转换到本设计的 People 面板Roles StatusAdmin Panel / People / Roles每个看板角色可做什么——可见哪些卡片、能否评论、创建与编辑、能否修改看板设置。只读无rowTemplate、无操作、无可编辑项因为角色能力是代码属性而非设置。它位于上方复选框列表的 Save 按钮之下跟随该列表的工作副本管理员在保存前即可看到改动后果。由 models/lib/boardRoleCapabilities.js 渲染与权限判定使用的同一张表见 Board rolesSecurityAdmin Panel / Problems / Security事件日志中的安全事件被拦截的上传、被拒绝的 URL 协议、认证失败。每事件一行最新在前SpeedAdmin Panel / Problems / Speed慢操作事件——什么太慢、在哪、持续多久TestsAdmin Panel / Problems / Tests服务器上记录的测试运行事件CPU usageAdmin Panel / Problems / CPU usage过去的高 CPU 时段。额外带状态行实时 CPU 百分比、核心数与负载均值因为表格本身是历史而非当前状态Broken cardsAdmin Panel / Problems / Broken cards没有 board、swimlane 或 list或类型不是卡片类型的卡片。列规格与旁边报表相同——它过去跑在全局搜索结果列表上这正是它曾是这里控件集不同的唯一报表的原因Files ReportAdmin Panel / Problems / Files Report每个附件文件名、大小、MIME 类型以及附件 / 看板 / 卡片 idRules ReportAdmin Panel / Problems / Rules Report每条自动化规则及其看板、动作类型、触发类型Boards ReportAdmin Panel / Problems / Boards Report每个看板及其 id、权限、归档状态、成员、组织与团队Cards ReportAdmin Panel / Problems / Cards Report每张卡片及其看板、泳道、列表、成员与经办人Impersonation ReportAdmin Panel / Problems / Impersonation Report谁冒充了谁、在哪个看板、何时、为何RecoveryAdmin Panel / Problems / Recovery数据库恢复事件含严重级别、数据库与详情TranslationAdmin Panel / Settings / Translation自定义翻译字符串语言、源文本、译文。交互行Edit 链接与 ⋯ 菜单与一个模板表头New 链接WeKan 其它位置表格名称菜单路径描述All Boards表格视图/board→ 视图菜单 → Table选中区块的每个看板Edit、看板标题与看板描述。可编辑——这正是它与下方 Public Boards 的区别Edit 单元格打开与 Swimlanes 视图相同的看板标题弹窗。其行携带看板颜色见 All-BoardsPublic Boards/public任何人都可打开的看板标题与描述每页十条。只读——行的唯一动作是打开看板——页面除表格外别无其它。其行携带看板颜色与背景图以便像 All Boards 一样被识别见 Public卡片、看板与成员历史已设计、尚未实现——见 History。表格名称菜单路径描述Card historyCard / 汉堡菜单 / History该卡片的每次变更可恢复Member historyMember settings / History一个用户的变更跨调用者可见的看板Board historyBoard Settings / History看板上及其内部所有内容的每次变更Swimlane historySwimlane 菜单 / History泳道及其列表与卡片的变更List historyList 菜单 / History列表及其卡片的变更布局固定行序定义一次行顺序自上而下固定在 client/components/settings/tablePage.jade 中定义一次标题Title——页面是什么。仅当页面提供时才渲染titleKey或title。在Admin Panel 内部它不提供那里每个面板的标题来自左侧激活菜单项由外围区块统一渲染见 Left Menu。若表格页再打印自己的标题同一词会出现两次——这正是 People、Organizations、Teams、Domains、Translation 与 Problems 报表都不传标题的原因。标题无论哪种情况都带.admin-pane-title保证表格面板与表单面板的标题同字号同位置。状态行Status可选——表格本身无法显示的实时状态如 CPU usage 页的当前 CPU 百分比。由statusTemplate指定的具名模板渲染没有则渲染无状态行。控件行Controls——起始侧是搜索框末尾侧是对分页‹ page X / N ›。表格Table——表头行加每条记录一行。无论空否都始终渲染多个页面把控件放在表头——Organizations、Teams、People、Translation 的 New 链接、全选对——如果没行就隐藏表格会隐藏创建第一条记录的唯一途径。空面板显示表头和 no items 消息在表格下方而不是取而代之。从模板源码可见这一顺序由简单的块级 DOM 顺序保证tablePage.jade配合 CSS 中的flex-direction: columntablePage.css——没有使用任何 flexorder重排因此 DOM 顺序就是视觉顺序LTR 与 RTL 皆然。宽度行为宽窗口——区块左侧菜单保持自身宽度表格在旁填充剩余空间。窄窗口≤ 800px——左侧菜单全宽置顶表格在其下方。这是对旧始终并排规则的有意覆盖手机上一行并排会让表格只剩几十像素宽右列不可达。800px 是 WeKan 其它地方使用的同一个手机断点tablePage.css。表格是width: 100%且table-layout: fixed所以所有列获得相同宽度百分比表格永远不可能比面板更宽。这正是让表格右侧保持在浏览器窗口内而非跑到右边缘之外的关键。单元格文本换行overflow-wrap: anywhere能把超长无断点的 id、URL 或文件名断在列内而不是把列撑宽——长值换到第二行而非在列边缘被截断。与宽度一样换行也带!important应用级table td { white-space: nowrap }规则曾把它关掉。列可借助nowrap退出用于日期时间列同样!important保证退出仍然生效tablePage.css。只有表格自身的 wrapper 允许横向滚动且只作为最后手段页面本身从不横向滚动。本设计之外不得强制任何宽度。两个管理样式表曾对裸table选择器设置min-width: 1200px !important; width: max-content !important——其中一个peopleBody.css选择器里连页面都没有因此影响 WeKan 所有表格其!important压过了本页布局。结果正是本设计要预防的故障右列滚出屏幕。现在两者都已用:not(.table-page-table)隔离不再给任何管理表格强加下限。这些规则还让短表格缩水width: max-content下一个两列表格只占面板三分之一而非填满。因此.table-page-table上的width、max-width与table-layout都带!important——一个可被其它文件夹样式表悄悄覆盖的保证不是保证。测试 tests/tablePage.test.cjs 专门钉死三个管理样式表不得再出现min-width: 1200px或width: max-content且.table-page-table的三个关键属性必须!important。控件行统一高度控件行共享同一高度34px且无 margin设置在tablePage.css中搜索框、过滤框、每个操作按钮与上一页/下一页对对齐到同一条上下边缘。forms.css里的全局button规则是display: block; margin-bottom: 14px为堆叠的表单按钮而写——保留该 margin 的控件会比不保留的居中更高这正是 People 的 Unlock all users 曾经比旁边的 Teams 低一截的原因。因此表格页在 tablePage.css 中一次性为行内所有控件清零 margin、统一 34px 高度且分页器选择器被完整拼出.table-page-controls .table-page-pagination button避免与paginationControls.css的同优先级规则在加载顺序上谁后到谁赢。数据加载只加载一页绝不加载整个集合表格页一次加载一页行从不加载整个集合。这是设计文档的核心数据约定也是多份报表此前性能问题的根源models/lib/tablePage.js中的pageInfo(total, page, perPage)返回{ page, totalPages, hasPrev, hasNext, skip, limit }。同一个调用同时供给订阅与 page X / N 计数器所以取什么与显示什么不可能漂移源码见 models/lib/tablePage.js包含对 page 的 clamp行被删后越界的页码解析到真实页而不是空视图。发布器在服务端应用limit/skip只发送那一页因此只有那些行到达 minimongo。客户端渲染那一页不得对已经分页的结果再次切片。adminProblems.js中的loadReport用同一pageInfo计算limit/skip后Meteor.subscribe(...)client/components/settings/adminProblems.js测试用const { limit, skip } pageInfo(的正则断言了这一点tests/tablePage.test.cjs。总行数来自独立的计数方法在页面打开或搜索变化时调用——不是每次点击上一页/下一页时。总数不会因为你翻页而变化每次翻页都重新计数等于给每次点击增加第二次服务器往返——这正是翻一个很大的 Cards Report 感觉慢的原因loadReport的recount参数与countedFor缓存见 client/components/settings/adminProblems.js。发布器示例——Translation 发布器server/publications/translation.js接收(query, limit, skip 0)校验参数类型用safeSelector拒绝带执行操作符的注入式选择器仅对管理员返回getTranslations(safeQuery, { limit, skip, sort: { modifiedAt: -1 }, fields: {...} })——一页、服务端分页、仅管理员。skip是 Translation 变成共享表格页时新加的——此前它靠无限滚动一次长一个窗口翻回第 1 页时前面每一页都还在内存里。另一个重要实现细节客户端从 minimongo 读回服务器命名的那一页。由于accounts发布会把登录用户自己的记录也放进 minimongo一个裸find(query)分不清哪些属于本页——这正是 Admin Panel / People 曾经在全部 578 页上都显示管理员本人的原因。解决方式是docsByIds(ids, docs)models/lib/tablePage.js服务器用publishReportPage(this, report-files, docs)把本页 id 写进一个索引集合见各发布器的调用客户端按 id 顺序取回——$in返回顺序不定尚未到达的 id 会被略过而非渲染成空行。Cards 与 Boards 报表的客户端读取因此走reportPageResults(collection, reportId)client/components/settings/adminProblems.js而不是展示 minimongo 里所有东西。每页行数——全应用一个数字TABLE_PAGE_ROWS_PER_PAGEWeKan 每个分页页面一次加载十行。该数字只存在一处即models/lib/tablePage.js中的TABLE_PAGE_ROWS_PER_PAGE 10models/lib/tablePage.js每个页面都从这里读取——包括那些不基于本设计、自己画分页器的页面。自己写数字的页面就是 bug无论它用哪个模板位置读取方式每个表格页pageInfo的默认值TABLE_PAGE_ROWS_PER_PAGEAdmin Panel / Problems —— 所有报表与事件流REPORTS_PER_PAGE、EVENTS_PER_PAGEAdmin Panel / People —— People、Organizations、Teams、DomainsusersPerPage、orgsPerPage、teamsPerPage、domainsPerPageAdmin Panel / Settings / TranslationTABLE_PAGE_ROWS_PER_PAGE搜索页CardSearchPaged全局搜索、到期卡片、破损卡片resultsPerPage归档看板ARCHIVED_BOARDS_PER_PAGE两个刻意的例外二者都没有分页器因此不算分页页面活动流是无限滚动其页大小是活游标成本决策见 client/components/activities/activities.js 中的注释以及models/lib/domainTablePage.js中的PER_PAGE_MAX——它是客户端可请求数量的上限不是页大小。同模块的PER_PAGE_DEFAULT就是同一个十显式写出而非 import因为纯 Node 测试require()该文件tests/tablePage.test.cjs把两者钉在一起tests/tablePage.test.cjs。列一页与另一页唯一的区别页与页的差异只在列清单{ label | labelKey, value(doc), align: end, nowrap: true, cls, userId(doc), data(doc) }label是字面量labelKey是 i18n 键。value(doc)返回单元格文本。缺失字段渲染为空单元格绝不渲染字符串undefined——这正是若干手写报表曾打印出来的内容models/lib/tablePage.js 的cellText显式处理。userId(doc)提供时把单元格渲染为链接打开与 Admin Panel / People 相同的 Edit user 弹窗。align: end右对齐大小、计数nowrap让日期时间保持一行data(doc)设置data-sev为高/严重级别着色。buildHeader(columns)与buildRows(docs, columns)把这个清单变成模板迭代的对象。两者都是models/lib/tablePage.js中的纯函数且带单测所以一行永远不可能比表头少一个单元格——手写表格的列错位就是这么来的tests/tablePage.test.cjs 用中间文档缺两个字段的用例钉死。buildRows实际输出的单元格远比文本丰富模板据其渲染出多种非文本单元格见 tablePage.jade用户单元格——显示头像或首字母缩写而非用户名名字作为title悬停仍可识别点击打开 Edit user 弹窗。多个用户如一个办公室的登录者会分组在一格内并显示登录次数。附件单元格——图片缩略图或扩展名块带预览与下载按钮。位置单元格——带国家旗帜与城市名点击后从与卡片位置相同的十一个地图提供方中选择打开地图仅当有经纬度坐标时才渲染为地图链接城市名不是坐标纯文本即可。图标与数据属性——如 Recovery 报表的状态图标✓/⚠/与严重级别着色。表头同样可以携带控件headerTemplate/headerData让th仍属于共享模板、只有内容来自页面——全选对与 add row 表单就是这样放进表头的。交互行与交互表头有些面板无法用文本单元格表达其行携带行内复选框与编辑链接表头携带控件。两个插槽即可覆盖无需每页重造表格rowTemplate——模板名加docs数据上下文数组。页面拥有自己的tr此时由它负责匹配列数行永不比表头短的保证只适用于cells形式。列上的headerTemplate/headerData——th仍属于共享模板只是其内容来自页面。其余一切——布局、分页、搜索、主题化分页器、总数——两种形式共享。优先使用列规格只有面板确实交互时才用这两个插槽。例如 People 的四个面板走rowTemplate: peopleRow/orgRow/teamRow与各headerTemplateTranslation 走rowTemplate: translationRow加headerTemplate: newTranslationRow测试断言于 tests/tablePage.test.cjs。控件行搜索、过滤、操作、总数、分页控件行按阅读顺序依次是搜索框、任意过滤、任意操作、总数、推到末尾的分页。过滤、操作与总数默认开启——没有开启它们的开关。提供过滤或操作的页面就会渲染出来总数在页面知道一个时总是显示。它们来自 Admin Panel / People原来手写在该页自己的标记里由于对任何表格都通用故放在这里filters: buildFilters([{ id, labelKey, options: [{ value, labelKey|label }] }], current) actions: buildActions([{ id, labelKey, icon, cls }]) total: number // 非零时显示 totalLabelKey: people-number // 其前的可选标签过滤是一个select页面读取data-filter与选中值。匹配current的选项被选中以字符串比较因此数值也能匹配buildFilters源码见 models/lib/tablePage.js。操作是一个按钮页面读取data-action得知按了哪个。它以主题色填充是操作而非导航且不带任何自身几何——曾有一个页面给操作按钮加了带 margin 与自定高度的类结果它比行内其它控件高一截。People 的 Unlock all users 与 Add / Remove Teams 就是这样声明为共享操作的client/components/settings/adminProblems.js 中 Recovery 与 Boards 的过滤/操作配置。总数统计整个结果集而非本页——正因如此它放在控件行里并color: inherit继承颜色暗色主题下仍然可读。三个控件永远存在且每页类名相同.js-table-page-search—— 输入并按 Enter。搜索重置到第 1 页并重新计数。.js-table-page-prev/.js-table-page-next—— 在两端通过hasPrev/hasNext禁用。.js-table-page-edit-user—— 用户名单元格打开 Edit user 弹窗。因为类名共享处理器也共享正在被操作的是哪个页面由哪个报表正打开标识而非每页专属类。在 client/components/settings/adminProblems.js 中可以看到这套共享处理器click .js-table-page-prev、click .js-table-page-next、keydown .js-table-page-searchEnter 触发event.stopPropagation由专用面板自行处理各自只有一个通过tmpl.activeReport.get()分派。goPrevPage/goNextPage/runSearch都会先查reportConfig(tmpl)[reportId]无配置则return——这样事件流报表自带分页状态的点击不会冒泡进共享分页器tests/tablePage.test.cjs 专门守卫这一点。模板中的控件行顺序也被测试锁定tests/tablePage.test.cjssearch → filters → actions → total → pagination且不存在任何if (filtersEnabled)之类的启用开关。主题表格页从不自造颜色表格页从不发明颜色。其按钮与计数器跟随当前生效的主题用户主题——Member Settings → Change color 在:root上设置--theme-accent以及--theme-accent-2。所有主题化元素立即跟随仅对该用户生效。WeKan 默认——未选自定义颜色时--theme-accent未设置每条规则通过var(--theme-accent, #01628c)回退到 WeKan 默认蓝#01628c。上一页/下一页按钮、操作按钮与 3 / 42 计数器在整个应用中只在一个地方定义样式——client/components/main/paginationControls.css——Admin Panel 表格、People / Organizations / Teams / Domains、All Boards、看板 Table 视图、归档看板与 Cron 设置表格全部共享它。表格页通过使用共享类名.table-page-pagination、.table-page-page-info获得该外观自己不加任何东西。不要在页面样式表里重述这些颜色。该文件刻意逐一写出:hover、:focus、:active和:active:hover因为 client/components/forms/forms.css 中的全局button规则用--theme-accent配黑色 / 深灰回退设置了同样的状态特异性相等或更高button { background: var(--theme-accent, #000) } button:focus { background: var(--theme-accent, #222) } button:active { background: var(--theme-accent, #111) } button:active:hover { background: #e6e6e6 }只抄一部分——比如只抄基础状态和:hover——看起来正确直到按钮被点击然后输给button:focus/button:active:hover在按钮保持焦点期间页面上留下一个黑色或灰色按钮。这正是颜色只放一个文件、覆盖每个状态、而tablePage.css只承担布局的原因。paginationControls.css头部注释完整解释了这段历史并说明按钮使用--theme-accent-fill支持渐变主题而非直接读--theme-accent的原因。页面其余部分继承标题与单元格文本取周围文本颜色计数器color: inherit在明暗主题下都可读而非钉死一个硬编码灰色。测试逐条断言共享分页样式表必须覆盖.table-page-pagination button的全部状态与.table-page-page-info颜色必须来自var(--theme-accent, #01628c)且tablePage.css中任何.table-page-pagination规则都不得包含background/color:/border:tests/tablePage.test.cjs。RTL 与日期布局是纯块顺序加逻辑属性margin-inline-start、text-align: start因此dirrtl下自动镜像无需重复标记。分页器靠margin-inline-start: auto推到行尾tablePage.cssRTL 下自动换到另一侧。日期时间列使用应用配置的日期格式辅助函数因此表格页与 UI 其余部分一致。例如各报表列通过formatDateForDisplay(date, true, value value.toLocaleString())格式化见adminProblems.js中的formatDate/formatEventAt。性能底座索引支撑的分页设计文档强调分页保持服务端、索引支撑。源码中可见两层保障报表发布按索引排序——Cards Report 发布器按{ boardId: 1, createdAt: -1 }排序索引字段而非未索引的{ boardId: 1, sort: 1 }测试用正则钉死tests/tablePage.test.cjs。事件流走复合索引——models/eventLog.js 中await ensureIndex(EventLog, { stream: 1, at: -1 })支撑eventLogPage的流内倒序读取eventLogCount(stream, search)与eventLogPage(stream, limit, skip, search)是管理员专用方法models/eventLog.js。客户端eventStreamReport通过Meteor.call(eventLogCount, ...)与Meteor.call(eventLogPage, stream, EVENTS_PER_PAGE, (page-1)*EVENTS_PER_PAGE, search, ...)取一页client/components/settings/adminProblems.js。测试断言两个索引都存在tests/tablePage.test.cjs。添加一个表格页设计文档给出的步骤只有三步添加列清单。在对应页面模块里写columns: [{ labelKey, value, align, nowrap, userId, ... }]——报表类页面加入REPORT_TABLESclient/components/settings/adminProblems.jsPeople 类页面加入peopleBody.js的规格。指向一个尊重limit/skip的发布器与一个计数方法。发布器用publishReportPage(this, report-id, docs)写本页索引、应用limit/skip计数方法如getBoardsReportCount(searchTerm , permission all)返回全量总数参见 server/publications/boards.js、server/publications/cards.js 等。添加菜单项。在PROBLEMS_MENU数据数组client/components/settings/adminProblems.js或对应的左菜单数据中加入带 id 的条目。别无其它不需要模板、CSS、处理器、分页代码。因为类名与处理器全局共享新增页面自动获得搜索、分页、主题、总数与空态消息又因为菜单现在是一份数据leftMenuData(PROBLEMS_MENU, ...)菜单项与激活态也自动工作。Broken cards 报表是一个极好的迁移案例它曾是 Problems 菜单里唯一控件集不同的条目——没有搜索框、没有总数、没有 page X / N只有自己的上一页/下一页——因为它在全局搜索机制CardSearchPaged上运行。现在它只是一个放在REPORT_TABLES里的列规格走brokenCardsReport发布器与getBrokenCardsReportCount计数方法与旁边报表完全相同tests/tablePage.test.cjs 完整守卫了这次迁移。adminProblems.js中的reportPageResults(Cards, report-broken)确保展示的是发布器命名的那一页而不是 minimongo 里所有卡片——后者正是该报表曾显示无尽长一页的原因。测试如何保证一份实现tests/tablePage.test.cjs 是整个设计的综合守卫覆盖六个层面纯函数正确性——pageInfo的分页窗口与计数器一致、越界页码 clamp、空表仍是 1/1columnWidthPercent等宽且不产生 NaN%buildRows每行单元格数恒等于列数、缺失字段渲染空串buildFilters的字符串比较选中逻辑tests/tablePage.test.cjs。模板承诺——行序title → status → controls → table、空表仍渲染表头让 New 链接可达、共享控件类各出现一次tests/tablePage.test.cjs。布局规则——全宽等宽换行列、min-width: 0、仅 wrapper 横向滚动、800px 断点堆叠tests/tablePage.test.cjs。一份实现——旧报表的私有控件类全部消失、每个报表都tablePage(tablePageData)、共享控件每个最多三个处理器、pageInfo同时供订阅与计数器、每个分页页面都读同一个TABLE_PAGE_ROWS_PER_PAGEtests/tablePage.test.cjs。文档即真值——文档列出的每个报表名、每个文件路径都存在且不重复tests/tablePage.test.cjs。主题与颜色边界——分页器被共享主题样式覆盖、tablePage.css不得重述按钮颜色、任何管理样式表不得再强制 1200px 宽度tests/tablePage.test.cjs。这套测试还揭示了若干有趣的历史 bugOrganizations/Teams/People 的tablePage(orgTablePageData)曾把 helper 注册在父模板上而 Blaze 只会在当前模板找导致表格静默消失测试 tests/tablePage.test.cjs 逐个解析tablePage(name)调用并断言 helper 就在该模板Teams 曾有一个上一页按钮但没有处理器——翻回上一页是死的合并为按打开面板作用域的一组处理器后修复tests/tablePage.test.cjs。小结WeKan 的表格页设计把分页表格抽象成一个维度——列清单——从而把十几个管理报表从十份各自漂移的副本收敛为一份模板 一份样式 一套纯函数 N 个列规格。它的核心约定可以概括为五条一个模板定义全部布局与行序纯函数同时喂给订阅与计数器以保证不分叉发布器服务端分页、客户端永不二次切片全应用一个每页行数TABLE_PAGE_ROWS_PER_PAGE 10颜色全部收敛到paginationControls.css并由--theme-accent驱动。如果你要为 WeKan 新增一个分页页面照着本文三步即可而 tests/tablePage.test.cjs 会替你守住院子里的每一条规则。【免费下载链接】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),仅供参考
返回列表