Tempus Dominus自定义视图开发:从日视图到年视图扩展
Tempus Dominus作为一款功能强大的日期时间选择器(Date/time picker widget),提供了灵活的视图扩展能力。本文将从现有视图实现入手,详细介绍如何从日视图扩展到年视图,并自定义视图组件。通过本文,你将掌握视图开发的核心模式、组件通信机制及样式定制方法,最终实现符合业务需求的定制化时间选择界面。
视图系统架构解析
Tempus Dominus的视图系统采用模块化设计,每个视图类型对应独立的实现类,统一继承自基础视图接口。核心视图实现位于src/js/display/calendar/目录,包含日视图(DateDisplay)、月视图(MonthDisplay)、年视图(YearDisplay)和十年视图(DecadeDisplay)四种基础实现。
核心视图类结构
所有日历视图均遵循相同的实现模式,包含构建DOM结构的getPicker()方法和更新视图内容的_update()方法:
// 日视图核心实现 [src/js/display/calendar/date-display.ts](https://link.gitcode.com/i/77f4d32f16384cd596b73f959fc330b4)
export default class DateDisplay {
getPicker(): HTMLElement { /* 创建日历容器 */ }
_update(widget: HTMLElement, paint: Paint): void { /* 更新日历内容 */ }
private _daysOfTheWeek(): HTMLElement[] { /* 生成星期标题行 */ }
private _handleCalendarWeeks(container: HTMLElement, innerDate: DateTime) { /* 处理周数显示 */ }
}
月视图实现与此类似,但采用12个月份格子的布局:
// 月视图实现 [src/js/display/calendar/month-display.ts](https://link.gitcode.com/i/c617bd7757329f862d8addd141664f31)
export default class MonthDisplay {
getPicker(): HTMLElement {
const container = document.createElement('div');
container.classList.add(Namespace.css.monthsContainer);
for (let i = 0; i < 12; i++) { /* 创建12个月份格子 */ }
return container;
}
}
视图切换机制
视图切换通过options.display.viewMode配置项控制,支持从日视图到十年视图的多级切换。官方文档src/docs/partials/options/display.html详细列出了视图相关配置:
{
display: {
viewMode: 'calendar', // 默认为日视图,可选值: 'clock' | 'calendar' | 'months' | 'years' | 'decades'
components: {
calendar: true, // 总开关,控制所有日历视图可见性
date: true, // 日视图开关
month: true, // 月视图开关
year: true, // 年视图开关
decades: true // 十年视图开关
}
}
}
日视图实现详解
日视图(DateDisplay)是最常用的视图类型,负责显示月历网格并处理日期选择交互。其核心实现包含DOM结构构建、日期数据填充和交互事件处理三个部分。
DOM结构构建
getPicker()方法创建包含42个日期格子的容器(6行×7列),并预留周数列位置:
// [src/js/display/calendar/date-display.ts](https://link.gitcode.com/i/77f4d32f16384cd596b73f959fc330b4#L28-L70)
getPicker(): HTMLElement {
const container = document.createElement('div');
container.classList.add(Namespace.css.daysContainer);
// 添加星期标题行
container.append(...this._daysOfTheWeek());
// 添加42个日期格子
for (let i = 0; i < 42; i++) {
const div = document.createElement('div');
div.setAttribute('data-action', ActionTypes.selectDay);
container.appendChild(div);
}
return container;
}
生成的日历容器结构如图所示,包含星期标题行和6行日期格子:
日期数据填充
_update()方法负责将日期数据填充到DOM元素,并处理日期状态(选中、禁用、今天等):
// [src/js/display/calendar/date-display.ts](https://link.gitcode.com/i/77f4d32f16384cd596b73f959fc330b4#L76-L133)
_update(widget: HTMLElement, paint: Paint): void {
const container = widget.getElementsByClassName(Namespace.css.daysContainer)[0] as HTMLElement;
const innerDate = this.optionsStore.viewDate.clone.startOf(Unit.month);
// 遍历所有日期格子并填充数据
container.querySelectorAll(`[data-action="${ActionTypes.selectDay}"]`).forEach((element: HTMLElement) => {
const classes = [Namespace.css.day];
// 设置日期状态类
if (innerDate.isBefore(this.optionsStore.viewDate, Unit.month)) classes.push(Namespace.css.old);
if (innerDate.isAfter(this.optionsStore.viewDate, Unit.month)) classes.push(Namespace.css.new);
if (this.dates.isPicked(innerDate, Unit.date)) classes.push(Namespace.css.active);
if (!this.validation.isValid(innerDate, Unit.date)) classes.push(Namespace.css.disabled);
if (innerDate.isSame(new DateTime(), Unit.date)) classes.push(Namespace.css.today);
// 应用样式并设置日期文本
element.classList.add(...classes);
element.innerText = innerDate.parts(undefined, { day: 'numeric' }).day;
innerDate.manipulate(1, Unit.date);
});
}
周数显示功能
当日历配置display.calendarWeeks: true时,会在日历左侧显示周数列,实现逻辑位于_handleCalendarWeeks()方法:
// [src/js/display/calendar/date-display.ts](https://link.gitcode.com/i/77f4d32f16384cd596b73f959fc330b4#L332-L338)
private _handleCalendarWeeks(container: HTMLElement, innerDate: DateTime) {
[...container.querySelectorAll(`.${Namespace.css.calendarWeeks}`)]
.filter((e: HTMLElement) => e.innerText !== '#')
.forEach((element: HTMLElement) => {
element.innerText = `${innerDate.week}`;
innerDate.manipulate(7, Unit.date);
});
}
周数显示效果如图所示,左侧额外列显示当前周在全年中的序号:
月视图与年视图扩展
月视图(MonthDisplay)和年视图(YearDisplay)采用与日视图相同的架构模式,但数据粒度和布局不同。理解这些视图的实现差异,是进行自定义扩展的基础。
月视图实现特点
月视图以年为单位,显示12个月份格子,核心实现位于src/js/display/calendar/month-display.ts:
// 月视图日期填充逻辑
_update(widget: HTMLElement, paint: Paint): void {
const innerDate = this.optionsStore.viewDate.clone.startOf(Unit.year);
container.querySelectorAll(`[data-action="${ActionTypes.selectMonth}"]`).forEach((containerClone: HTMLElement) => {
// 设置月份名称
containerClone.innerText = innerDate.format({ month: 'short' });
// 检查是否为选中状态
if (this.dates.isPicked(innerDate, Unit.month)) {
classes.push(Namespace.css.active);
}
innerDate.manipulate(1, Unit.month);
});
}
月视图显示效果如图,以网格形式展示12个月份,当前选中月份高亮显示:
年视图实现特点
年视图以十年为单位,显示12个年份格子,核心实现位于src/js/display/calendar/year-display.ts:
// 年视图构造函数
constructor() {
this.optionsStore = serviceLocator.locate(OptionsStore);
this.dates = serviceLocator.locate(Dates);
this.validation = serviceLocator.locate(Validation);
}
// 年视图日期范围计算
_update(widget: HTMLElement, paint: Paint) {
this._startYear = this.optionsStore.viewDate.clone.manipulate(-1, Unit.year);
this._endYear = this.optionsStore.viewDate.clone.manipulate(10, Unit.year);
// 生成年份标签
switcher.setAttribute(
Namespace.css.yearsContainer,
`${this._startYear.format({ year: 'numeric' })}-${this._endYear.format({ year: 'numeric' })}`
);
}
年视图显示效果如图,展示连续12年的年份选择界面:
自定义视图开发实战
自定义视图开发需遵循现有视图的实现规范,实现getPicker()和_update()核心方法,并通过配置项启用自定义视图。以下以季度视图为例,详细说明实现步骤。
步骤1:创建季度视图类
在src/js/display/calendar/目录下创建quarter-display.ts文件,实现季度视图类:
import { Unit } from '../../datetime';
import Namespace from '../../utilities/namespace';
import { serviceLocator } from '../../utilities/service-locator';
import ActionTypes from '../../utilities/action-types';
import { OptionsStore } from '../../utilities/optionsStore';
export default class QuarterDisplay {
private optionsStore: OptionsStore;
constructor() {
this.optionsStore = serviceLocator.locate(OptionsStore);
}
// 创建季度选择器DOM
getPicker(): HTMLElement {
const container = document.createElement('div');
container.classList.add(Namespace.css.quartersContainer);
// 创建4个季度格子
for (let i = 0; i < 4; i++) {
const div = document.createElement('div');
div.setAttribute('data-action', ActionTypes.selectQuarter);
container.appendChild(div);
}
return container;
}
// 更新季度视图内容
_update(widget: HTMLElement, paint: Paint): void {
const container = widget.getElementsByClassName(Namespace.css.quartersContainer)[0];
const innerDate = this.optionsStore.viewDate.clone.startOf(Unit.year);
container.querySelectorAll(`[data-action="${ActionTypes.selectQuarter}"]`).forEach((element: HTMLElement, index) => {
const classes = [Namespace.css.quarter];
const quarterName = `Q${index + 1} ${innerDate.format({ year: 'numeric' })}`;
// 设置选中状态
if (this.isCurrentQuarter(innerDate, index)) {
classes.push(Namespace.css.active);
}
element.classList.add(...classes);
element.innerText = quarterName;
innerDate.manipulate(3, Unit.month); // 移动到下一季度
});
}
private isCurrentQuarter(date: DateTime, quarterIndex: number): boolean {
// 实现季度选中逻辑
const currentQuarter = Math.floor(date.month / 3);
return currentQuarter === quarterIndex;
}
}
步骤2:注册视图类型
在视图管理器中注册新创建的季度视图,修改src/js/display/index.ts:
import QuarterDisplay from './calendar/quarter-display';
export const ViewTypes = {
date: DateDisplay,
month: MonthDisplay,
year: YearDisplay,
decade: DecadeDisplay,
quarter: QuarterDisplay // 添加季度视图
};
步骤3:添加配置选项
扩展配置选项以支持季度视图,修改src/js/utilities/options.ts:
export interface DisplayOptions {
viewMode: 'calendar' | 'months' | 'years' | 'decades' | 'quarters'; // 添加quarters选项
components: {
// ... 现有配置
quarter: boolean; // 添加季度视图开关
};
}
步骤4:实现视图切换逻辑
修改视图切换处理逻辑,支持季度视图的切换,修改src/js/datetime.ts:
switch (newViewMode) {
case 'quarters':
this.currentView = new QuarterDisplay();
break;
// ... 其他视图case
}
步骤5:样式定制
在SCSS文件中添加季度视图样式,修改src/scss/tempus-dominus.scss:
.quarters-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.25rem;
.quarter {
padding: 1rem;
text-align: center;
cursor: pointer;
&.active {
background-color: $primary;
color: white;
}
&.disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
}
高级视图配置与交互
Tempus Dominus提供丰富的视图配置选项,可通过display配置对象自定义视图行为和外观。合理利用这些配置项,可在不修改源码的情况下实现多样化的视图效果。
视图组件控制
通过display.components配置可细粒度控制各视图组件的可见性:
{
display: {
components: {
calendar: true, // 总开关:控制所有日历视图
date: true, // 日视图开关
month: true, // 月视图开关
year: true, // 年视图开关
decades: false, // 禁用十年视图
quarter: true // 启用自定义季度视图
}
}
}
工具栏与按钮配置
视图工具栏包含导航按钮和视图切换控件,可通过display.buttons配置控制按钮显示:
{
display: {
buttons: {
today: true, // 显示"今天"按钮
clear: true, // 显示"清除"按钮
close: true // 显示"关闭"按钮
},
toolbarPlacement: 'top' // 工具栏位置:top/bottom
}
}
工具栏按钮布局如图所示,包含今天、清除和关闭三个功能按钮:
视图导航控制
视图导航按钮状态由_updateCalendarView()方法控制,可根据业务需求自定义导航逻辑:
// [src/js/display/calendar/date-display.ts](https://link.gitcode.com/i/77f4d32f16384cd596b73f959fc330b4#L263-L289)
private _updateCalendarView(container: Element) {
const [previous, switcher, next] = container.parentElement
.getElementsByClassName(Namespace.css.calendarHeader)[0]
.getElementsByTagName('div');
// 设置标题文本
switcher.setAttribute(
Namespace.css.daysContainer,
this.optionsStore.viewDate.format(this.optionsStore.options.localization.dayViewHeaderFormat)
);
// 控制导航按钮状态
this.validation.isValid(
this.optionsStore.viewDate.clone.manipulate(-1, Unit.month),
Unit.month
) ? previous.classList.remove(Namespace.css.disabled)
: previous.classList.add(Namespace.css.disabled);
}
视图扩展最佳实践
自定义视图开发需遵循一定的设计原则,以确保代码可维护性和与框架的兼容性。以下是视图扩展开发中的最佳实践指南。
代码组织原则
- 单一职责:每个视图类只负责一种视图类型的实现
- 依赖注入:通过
serviceLocator获取依赖,避免硬编码依赖关系 - 状态管理:通过
optionsStore和dates服务管理视图状态,避免直接操作DOM - 样式隔离:使用唯一的CSS类前缀,避免样式冲突
性能优化策略
- DOM复用:创建一次DOM结构,通过
_update()方法复用DOM元素 - 批量操作:使用DocumentFragment批量处理DOM更新
- 事件委托:在父容器上委托处理子元素事件,减少事件监听器数量
- 懒加载:对不常用的视图类型采用动态导入
兼容性考虑
- 浏览器支持:确保使用的API在目标浏览器中受支持
- 响应式设计:使用CSS Grid和Flexbox实现自适应布局
- 无障碍访问:添加适当的ARIA属性,支持键盘导航
视图扩展实例:财务季度选择器
基于上述自定义视图开发方法,我们实现一个财务季度选择器,用于财务系统中的季度数据筛选。
需求分析
财务季度选择器需满足以下需求:
- 显示当前年度的四个季度
- 支持跨年度季度选择
- 高亮显示当前季度
- 支持季度范围选择
实现效果
财务季度选择器最终效果如图所示,以卡片式布局展示四个季度,当前季度高亮显示,支持单击选择和拖拽选择季度范围:
核心代码实现
季度范围选择功能实现:
// 季度视图中的范围选择实现
private _handleRangeSelection(element: HTMLElement) {
if (this.optionsStore.options.dateRange) {
const startDate = this.dates.picked[0];
const endDate = DateTime.fromString(element.getAttribute('data-value'), { format: 'yyyy-MM' });
if (startDate && !endDate) {
// 绘制范围选择样式
this._paintRange(startDate, endDate);
}
}
}
private _paintRange(start: DateTime, end: DateTime) {
// 实现范围选择的样式绘制逻辑
}
总结与扩展
Tempus Dominus的视图系统通过模块化设计提供了强大的扩展能力,本文详细介绍了视图系统的架构设计、核心实现及自定义扩展方法。通过实现自定义视图类、注册视图类型、扩展配置选项和样式定制四个步骤,可快速开发符合业务需求的定制化视图。
关键知识点回顾
- 视图架构:所有视图遵循
getPicker()+_update()的实现模式 - 核心目录:视图实现位于src/js/display/calendar/
- 配置系统:通过
display配置对象控制视图行为 - 扩展步骤:创建视图类 → 注册视图 → 添加配置 → 样式定制
未来扩展方向
- 自定义单元格渲染:通过
paint回调自定义单元格内容 - 多日历视图:实现同时显示多个月份的日历视图
- 任务日历:集成任务数据,实现任务日历视图
- 热图视图:基于数据密度的热图日历视图
通过掌握Tempus Dominus的视图扩展方法,开发者可以构建丰富多样的时间选择界面,满足不同业务场景的需求。官方文档src/docs/partials/options/display.html提供了更多视图配置选项的详细说明,建议进一步查阅以深入理解视图系统的 capabilities。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考









