WeKan 截止日期(Due Date)机制详解:颜色编码、倒计时与截止日变更追踪
【免费下载链接】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 为每张卡片提供了 received(接收)、start(开始)、due(截止)、end(完成)四类日期,围绕其中最具管理价值的“截止日期”,实现了一套由颜色编码、倒计时文案和变更次数追踪组成的可视化提醒体系。本文基于docs/Features/Date/Due-Date.md这一功能文档,结合 WeKan 仓库中的颜色判定逻辑、卡片模板与数据模型源码,完整讲解这套机制的设计意图、实现方式与验证手段。读完本文,你将能够准确理解 WeKan 卡片截止日期的展示规则从何而来,并在定制或排查 UI 问题时快速定位到对应源码。
一、卡片日期体系:四个日期各有其位
功能文档首先给出了一张简洁的日期语义表,它定义了 WeKan 卡片日期字段的四种用途:
- received— bug 或任务被发现(接收)的时间;
- start— 工作开始的时间;
- due— 工作应当完成(截止)的时间;
- end— 工作实际完成的时间。
这四个日期在数据层都落在卡片(或关联的板)文档上。从 卡片模型 可以看到:
getDue()/setDue(dueAt)读写dueAt字段;- 若当前卡片是“关联卡片”(linked card)或“关联板”(linked board),读写会透明地转发到真实卡片/板的文档上,保证跨看板引用的日期始终一致;
unsetDue()通过$unset移除dueAt字段,卡片上未设置截止日期时该字段为null。
编辑入口方面,卡片详情中的 due 区块在 cardDetails.jade 中按$eq field "dueDate"渲染:已有截止日期时展示cardDueDate徽章组件,未设置且当前用户可修改卡片时展示“+”号添加按钮(且受currentBoard.allowsDueDate板级开关控制,并屏蔽 Worker 角色)。点击后打开editCardDueDate弹层,其初始化逻辑在 cardDate.js:
Template.editCardDueDatePopup.onCreated(function () { const card = Template.currentData(); setupDatePicker(this, { defaultTime: '1970-01-01 17:00:00', initialDate: card.getDue() ? card.getDue() : undefined, storeDate(date, currentCard) { return currentCard.setDue(date); }, deleteDate(currentCard) { return currentCard.unsetDue(); }, }); });其中storeDate/deleteDate分别对接上文提到的setDue/unsetDue,完成了“弹层选择 → 模型写库”的完整链路。此外,cardDate.js 中的formatCardDateForDisplay还会根据用户个人设置把日期显示为 Jalali(波斯太阳历)或按用户偏好的公历格式渲染——这属于纯显示层转换,存储始终保持原生Date(公历)。
二、截止日期颜色编码:红 / 琥珀 / 灰的判定规则
功能文档的核心段落描述了截止日期徽章的颜色语义:
红色(red)= 已逾期(overdue);琥珀色(amber)= 48 小时之内到期;灰色(grey)= 距今超过 48 小时才到期。且列表/迷你卡(minicard)与打开的卡片详情上显示的颜色永远一致。
文档同时提到卡片已有 end 日期的情形,源码把这一边界条件补充得更完整。颜色判定被抽成一个无 DOM 依赖的纯函数 dueDateClass,被卡片详情徽章与迷你卡徽章共用,这正是“两个视图颜色永远一致”这一承诺的实现基础:
export function dueDateClass(dueDate, now, endDate) { const due = new Date(dueDate); const nowVal = new Date(now); if (endDate) { const end = new Date(endDate); if (end.getTime() < due.getTime()) { return 'completed-early'; // 提前完成 } return 'completed'; // 按期或逾期完成 } const diffMs = due.getTime() - nowVal.getTime(); const hoursDiff = diffMs / (1000 * 60 * 60); if (hoursDiff < 0) { return 'overdue'; // 红色:已逾期 } else if (hoursDiff <= 48) { return 'due-soon'; // 琥珀色:48 小时内到期 } return 'not-due'; // 灰色:48 小时以后到期 }完整的规则集可以整理为下表(CSS 类名即徽章样式选择器):
| 条件 | 返回类名 | 视觉语义 |
|---|---|---|
| 卡片已有 end 日期,且 end 早于 due | completed-early | 提前完成 |
| 卡片已有 end 日期,且 end 不早于 due | completed | 已完成 |
| due 在当前时间之前 | overdue | 红色,已逾期 |
| due 在 48 小时之内(含恰好 48 小时) | due-soon | 琥珀色,临近到期 |
| due 在 48 小时之后 | not-due | 灰色,尚不紧急 |
两处值得注意的实现细节:
- 48 小时边界是闭区间。
hoursDiff <= 48意味着恰好 48 小时后的到期日仍算“琥珀色”。这一边界曾被专门做回归测试(见下文测试小节,对应 issue #6000:“超过 48 小时约 1 分钟的将来日期必须显示为灰色”)。 now是作为参数传入的。徽章模板通过 dateNowTicker 订阅一个周期更新的“当前时间”变量(cardDateOnCreated中subscribeDateNowTicker()),因此一张长期停留的卡片徽章会在跨过 48 小时线时由灰变琥珀、在跨过到期时刻后变红,无需刷新页面。
调用侧见 cardDate.js 与 cardDate.js:cardDueDate(卡片详情)和minicardDueDate(迷你卡)两个模板的classes()helper 均返回`due-date ${dueDateClass(theDate, nowVal, endAt)}`——同一个函数、同一份输入,输出必然一致,从结构上杜绝了两个视图颜色漂移的可能。
三、倒计时文案:“N 天后到期 / N 天前逾期”
在颜色之外,徽章还会在日期后追加一段相对倒计时文案,例如 “Jun 15 (3 days left)” 或 “Jun 15 (Due today)”。这段文案由 dueCountdown 生成:
export function dueCountdown(dueDate, now) { const due = new Date(dueDate); const nowVal = new Date(now); const startOfDay = date => new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime(); const dayMs = 24 * 60 * 60 * 1000; const diffDays = Math.round((startOfDay(due) - startOfDay(nowVal)) / dayMs); if (diffDays === 0) { return { key: 'due-today', days: 0 }; } else if (diffDays > 0) { return { key: 'due-days-left', days: diffDays }; } return { key: 'due-days-overdue', days: -diffDays }; }关键设计点:
- 按“日历日”计算而非 24 小时差值:先把两个日期都归零到当天 00:00 再相减。因此“今天稍后到期”的卡片显示 “Due today”,而不是 “0 days left”。
- 返回 i18n 键而不是文案本身:
days恒为非负数,方向信息由key(due-today/due-days-left/due-days-overdue)表达。调用点再用TAPi18n.__(key, { count: days })查翻译,见 cardDate.js 中的dueCountdownText。这样dueDateColor.js保持零国际化依赖、易于单测。 - 与颜色共用同一模块:源码注释明确说明倒计时与徽章颜色“always agree”(总是相互一致),即
dueCountdown与dueDateClass放在同一文件中共同维护。
当卡片已有 end 日期时,showDate/showTitlehelper 会省略倒计时后缀、只展示 end 日期本身(cardDate.js),避免“已完成却显示 N 天前逾期”的误导性文案。
四、截止日期变更次数:来自活动历史的问责视图
功能文档的最后一节描述了“due date changed N times”(截止日期已修改 N 次)计数:它显示在卡片详情的截止日期旁,数据来源于卡片的活动历史(activity history),目的是让“截止日被改过多少次”可见,便于团队对自己承诺的日期保持问责。
实现上由三部分组成:
- 每次变更都会记入活动日志。卡片模型 的注释指出:每一次截止日期的设置/修改都会由服务端
before.update钩子写入一条activityType为'a-dueAt'的活动记录。 - 只读计数方法。模型提供
getDueDateChangeCount():
getDueDateChangeCount() { const cardId = this.isLinkedCard() ? this.linkedId : this.getRealId(); const activities = ReactiveCache.getActivities({ cardId, activityType: 'a-dueAt', }); return Array.isArray(activities) ? activities.length : 0; }它从ReactiveCache中按cardId+activityType: 'a-dueAt'取出活动列表并计数;对关联卡片会先解析出真实卡片 ID,保证跨板引用场景下计数正确。 3.模板层渲染。全局注册的 dueDateChangeCount helper 在卡片对象不可用时返回 0,从而让模板自然隐藏该行;cardDetails.jade 中:
if dueDateChangeCount .card-details-due-date-changes(title="{{_ 'due-date-changes'}}") | {{_ 'due-date-changed-times' dueDateChangeCount}}仅在计数非 0 时显示,文案键due-date-changed-times已随多语言文件(如 en 源文件 及各语种翻译)全量覆盖。
这条链路“update 钩子 → 活动记录 → ReactiveCache 查询 → 模板 helper → jade 渲染”全部是响应式的:任何人再次修改截止日期,活动集合变化会自动驱动详情面板中的计数更新,无需任何手动刷新逻辑。
五、一致性保障:单元测试与回归
文档强调“minicard 与卡片详情颜色必须一致”“恰好 48 小时算琥珀、超过即灰色”等精确行为,这些约束在仓库中有对应的自动化验证:
- dueDateColor.tests.js:针对
dueDateClass的纯函数单测,覆盖“过去日期 → overdue”“48 小时内 → due-soon”“恰好 48 小时 → due-soon”“48 小时 + 1 分钟 → not-due(#6000 回归)”等边界; - dueDateCountdown.test.cjs:针对倒计时文案逻辑的独立测试;
- 组件层测试(如 cardDate.js 中注释提到的 #6615 数据上下文问题)验证了详情视图传入
{ card, canModifyCard }而迷你卡直接继承 Card 对象时,两套调用形态都不会在响应式重跑中互相串扰。
这些测试的存在意味着:如果你在二次开发中调整了 48 小时阈值或颜色语义,client/lib/tests/dueDateColor.tests.js会立即暴露与文档承诺不符的行为。
六、小结
WeKan 的截止日期功能以三个文档描述的特性为主线——四日期语义(received/start/due/end)、红/琥珀/灰三档颜色编码、截止日变更次数追踪——并在源码中落实为一套“纯函数判定 + 共享模板 + 活动日志计数”的清晰结构:
- 颜色规则集中在 client/lib/dueDateColor.js,被详情与迷你卡两视图共用,48 小时阈值以小时差闭区间判定;
- 倒计时按日历日计算并以 i18n 键输出,保证多语言下“Due today / N days left / N days overdue”语义准确;
- 变更次数依赖服务端对
a-dueAt活动的记录,通过 models/cards.js 的只读计数方法暴露给 cardDetails.jade 渲染。
理解这条从文档语义到dueDateClass、dueCountdown、getDueDateChangeCount的实现映射,是掌握 WeKan 卡片日期体系、并在定制部署中正确扩展日期展示行为的关键。
【免费下载链接】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),仅供参考